-[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · **العربية** · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · **العربية** · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: هندسة عكسية لأي شيء
@@ -9,10 +9,10 @@
**وجدت ميزة تعجبك؟ افهم طريقة عملها حتى مستوى الشيفرة الثنائية.**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#current-status)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
-
+
انضم إلى مجتمع الهندسة العكسية
Discord · أسئلة وأجوبة · مشاركة النتائج
@@ -67,7 +67,7 @@ npx rea-agents setup
اختر الوكلاء، وراجع التغييرات المقترحة، ثم وافق عليها. يضيف الإعداد خادم MCP الخاص بـ REA وتعليمات سير العمل المطابقة، مع الاحتفاظ بنسخ احتياطية من الإعدادات الموجودة. أعد تشغيل وكيلك بعد ذلك.
-يدعم الإعداد Claude Code وCodex وCursor وGemini CLI وGrok Build و[وكلاء آخرين](docs/installation.md#supported-agents). راجع [التثبيت والإعداد](docs/installation.md) لتهيئة موفّري التحليل وتسجيل MCP يدويًا.
+يدعم الإعداد Claude Code وCodex وCursor وGemini CLI وGrok Build و[وكلاء آخرين](../installation.md#supported-agents). راجع [التثبيت والإعداد](../installation.md) لتهيئة موفّري التحليل وتسجيل MCP يدويًا.
### اطلب من وكيلك
@@ -94,7 +94,7 @@ npm install --global rea-agents
rea --help
```
-لتحليل الشيفرة الأصلية، هيّئ موفّرًا أولًا. راجع [دليل CLI وEvidence](docs/cli.md) لأوامر تحليل الشيفرة الأصلية، واختيار الموفّر، واللقطات، والاستخدام في السكربتات.
+لتحليل الشيفرة الأصلية، هيّئ موفّرًا أولًا. راجع [دليل CLI وEvidence](../cli.md) لأوامر تحليل الشيفرة الأصلية، واختيار الموفّر، واللقطات، والاستخدام في السكربتات.
### تحديث REA
@@ -120,9 +120,9 @@ npx rea-agents@latest setup
يستدعي وكيلك REA عبر MCP لفحص الهدف وتتبع الشيفرة ذات الصلة. يعيد REA النتائج مع أدلتها. يستخدمها الوكيل لطرح أسئلة متابعة، أو شرح السلوك، أو كتابة تنفيذ واختباره. تستخدم أوامر CLI سير العمل نفسه.
-
+
-[افتح الشكل بحجمه الكامل](website/public/assets/figures/rea-investigation-flow.svg).
+[افتح الشكل بحجمه الكامل](../../website/public/assets/figures/rea-investigation-flow.svg).
@@ -130,37 +130,37 @@ npx rea-agents@latest setup
يتطلب REA إصدار Node.js 22.x (>=22.19) أو 24.x (>=24.11) أو 26+، إضافة إلى npm. تعتمد الأدوات الإضافية والأنظمة المضيفة المدعومة على الهدف:
-| الهدف | ما يعيده REA | المتطلبات والدليل |
-| ------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
-| الملفات الثنائية للشيفرة الأصلية | شيفرة شبه برمجية، وتعليمات تجميع، وسلاسل نصية، ورموز، واستدعاءات، ومراجع | Hopper أو Ghidra أو IDA؛ [تحليل الشيفرة الأصلية](https://rea.tools/ar/guides/native/) |
-| بنية ELF دون تشغيل | الأقسام والمقاطع والرموز ومعلومات إعادة التموضع الأصلية، وآليات الحماية المحتملة المستدلّ عليها بالفحص الثابت | pwntools يوفّره المستدعي على Linux x64؛ [تشخيص الملفات الثنائية](docs/binary-diagnostics.md) |
-| شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا على Linux x64؛ [دليل EVM دون تشغيل](docs/evm-bytecode.md) |
-| أعطال Linux المسجّلة | سجلات note الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي على Linux x64؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](docs/recorded-crashes.md) |
-| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](https://rea.tools/ar/guides/javascript/) |
-| المواقع | بنية الصفحة، والسكربتات، وملاحظات الشبكة، ولقطات الشاشة المطلوبة | متصفح من عائلة Chrome؛ [تحليل المتصفح](https://rea.tools/ar/guides/browser/) |
-| تسجيلات الشبكة المحفوظة | الطلبات، والاستجابات، ومحتويات الحمولة المتاحة، ومواضعها في المصدر | HAR؛ وmitmdump على Linux لتسجيلات mitmproxy بصيغتها الأصلية؛ [دليل تسجيلات الشبكة](docs/web-network-captures.md) |
-| التقاط الحزم | نقاط النهاية وDNS وTLS SNI وتوزيع البروتوكولات وطلبات HTTP | tshark وcapinfos يوفّرهما المُستدعي؛ [دليل pcap](docs/pcap-analysis.md) |
-| تجميعات .NET | البيانات الوصفية، وتعليمات CIL، واعتماديات الشيفرة الأصلية المعلنة، ومقارنات البناء | فحص ثابت؛ [دليل الشيفرة المُدارة](docs/managed-code-analysis.md) |
-| حزم Android APK | تصريحات ملف manifest، والأصناف، والدوال الناتجة عن فك الترجمة، والمراجع | JADX دون واجهة رسومية وJDK كامل على Linux/macOS/Windows x64؛ [دليل Android](docs/android-analysis.md) |
-| أجهزة Android | الأجهزة والحزم والعمليات والسجلات والدامبات ولقطات الشاشة ونقل APK/الملفات | adb يوفّره المُستدعي (محاكي أو جهاز)؛ [دليل ADB](docs/adb-device-analysis.md) |
-| مشاريع JEB | وحدات المشروع من المحرك وبصمات المصنّفات والكود الزائف للأنواع/الدوال | عميل JEB يبدأه المُستدعي ويقدّم MCP، ويُختار عبر REA_JEB_MCP_URL؛ [دليل JEB](docs/jeb-analysis.md) |
-| موارد Android | البيان المفكوك الترميز وجداول السلاسل واللغات وحقائق الإصدار | مُشغّل Apktool يوفّره المُستدعي؛ [دليل الموارد](docs/apktool-resource-analysis.md) |
-| تطبيقات Flutter | بصمة لقطة Dart وهشوش المكتبات لكل ABI وأصل بناء المحرك | لا شيء (تحليل APK ساكن)؛ [دليل Flutter](docs/flutter-analysis.md) |
-| البرامج الثابتة | المناطق، ونتائج الاستخراج، وما يُمرّر إلى تحليل الشيفرة الأصلية | Binwalk / Unblob على Linux؛ [دليل البرامج الثابتة](docs/firmware-analysis.md) |
-| الحزم المسجلة | حقول مختارة وقيم متكررة ومرشحات وقواعد فك الترميز | Linux: TShark 4.x + prlimit; [دليل التقاط الحزم](docs/packet-captures.md) |
-| الإشارات المنطقية المسجلة | تعليقات مفكك الترميز ونطاقات العينات ومعلومات التشخيص | Linux: sigrok-cli 0.7.2 + prlimit; [دليل الإشارات](docs/signal-recordings.md) |
-| الحزم والموارد | قوائم الملفات، والبصمات، وملفات plist، وبنية حزم Apple، والموارد المستخرجة | [دليل تحليل الملفات وJavaScript](docs/javascript-artifact-reconstruction.md)، [تطبيقات Apple](docs/apple-application-analysis.md) |
-| سلوك العمليات | مخرجات الطرفية، والتفاعلات، وملاحظات الخروج ونظام الملفات، ومقارنات التشغيل | Linux/macOS مع PTY أصلي؛ [تسجيل سلوك العمليات](docs/process-capture.md) |
+| الهدف | ما يعيده REA | المتطلبات والدليل |
+| ------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
+| الملفات الثنائية للشيفرة الأصلية | شيفرة شبه برمجية، وتعليمات تجميع، وسلاسل نصية، ورموز، واستدعاءات، ومراجع | Hopper أو Ghidra أو IDA؛ [تحليل الشيفرة الأصلية](https://rea.tools/ar/guides/native/) |
+| بنية ELF دون تشغيل | الأقسام والمقاطع والرموز ومعلومات إعادة التموضع الأصلية، وآليات الحماية المحتملة المستدلّ عليها بالفحص الثابت | pwntools يوفّره المستدعي على Linux x64؛ [تشخيص الملفات الثنائية](../binary-diagnostics.md) |
+| شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا على Linux x64؛ [دليل EVM دون تشغيل](../evm-bytecode.md) |
+| أعطال Linux المسجّلة | سجلات note الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي على Linux x64؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](../recorded-crashes.md) |
+| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](https://rea.tools/ar/guides/javascript/) |
+| المواقع | بنية الصفحة، والسكربتات، وملاحظات الشبكة، ولقطات الشاشة المطلوبة | متصفح من عائلة Chrome؛ [تحليل المتصفح](https://rea.tools/ar/guides/browser/) |
+| تسجيلات الشبكة المحفوظة | الطلبات، والاستجابات، ومحتويات الحمولة المتاحة، ومواضعها في المصدر | HAR؛ وmitmdump على Linux لتسجيلات mitmproxy بصيغتها الأصلية؛ [دليل تسجيلات الشبكة](../web-network-captures.md) |
+| التقاط الحزم | نقاط النهاية وDNS وTLS SNI وتوزيع البروتوكولات وطلبات HTTP | tshark وcapinfos يوفّرهما المُستدعي؛ [دليل pcap](../pcap-analysis.md) |
+| تجميعات .NET | البيانات الوصفية، وتعليمات CIL، واعتماديات الشيفرة الأصلية المعلنة، ومقارنات البناء | فحص ثابت؛ [دليل الشيفرة المُدارة](../managed-code-analysis.md) |
+| حزم Android APK | تصريحات ملف manifest، والأصناف، والدوال الناتجة عن فك الترجمة، والمراجع | JADX دون واجهة رسومية وJDK كامل على Linux/macOS/Windows x64؛ [دليل Android](../android-analysis.md) |
+| أجهزة Android | الأجهزة والحزم والعمليات والسجلات والدامبات ولقطات الشاشة ونقل APK/الملفات | adb يوفّره المُستدعي (محاكي أو جهاز)؛ [دليل ADB](../adb-device-analysis.md) |
+| مشاريع JEB | وحدات المشروع من المحرك وبصمات المصنّفات والكود الزائف للأنواع/الدوال | عميل JEB يبدأه المُستدعي ويقدّم MCP، ويُختار عبر REA_JEB_MCP_URL؛ [دليل JEB](../jeb-analysis.md) |
+| موارد Android | البيان المفكوك الترميز وجداول السلاسل واللغات وحقائق الإصدار | مُشغّل Apktool يوفّره المُستدعي؛ [دليل الموارد](../apktool-resource-analysis.md) |
+| تطبيقات Flutter | بصمة لقطة Dart وهشوش المكتبات لكل ABI وأصل بناء المحرك | لا شيء (تحليل APK ساكن)؛ [دليل Flutter](../flutter-analysis.md) |
+| البرامج الثابتة | المناطق، ونتائج الاستخراج، وما يُمرّر إلى تحليل الشيفرة الأصلية | Binwalk / Unblob على Linux؛ [دليل البرامج الثابتة](../firmware-analysis.md) |
+| الحزم المسجلة | حقول مختارة وقيم متكررة ومرشحات وقواعد فك الترميز | Linux: TShark 4.x + prlimit; [دليل التقاط الحزم](../packet-captures.md) |
+| الإشارات المنطقية المسجلة | تعليقات مفكك الترميز ونطاقات العينات ومعلومات التشخيص | Linux: sigrok-cli 0.7.2 + prlimit; [دليل الإشارات](../signal-recordings.md) |
+| الحزم والموارد | قوائم الملفات، والبصمات، وملفات plist، وبنية حزم Apple، والموارد المستخرجة | [دليل تحليل الملفات وJavaScript](../javascript-artifact-reconstruction.md)، [تطبيقات Apple](../apple-application-analysis.md) |
+| سلوك العمليات | مخرجات الطرفية، والتفاعلات، وملاحظات الخروج ونظام الملفات، ومقارنات التشغيل | Linux/macOS مع PTY أصلي؛ [تسجيل سلوك العمليات](../process-capture.md) |
يقرأ الفحص الثابت لـ JavaScript و.NET الملفات المقدّمة دون تشغيل التطبيق. يشغّل التسجيل أثناء التنفيذ الهدف المحدد أو يتفاعل معه بصلاحيات مستخدمك؛ ويشرح كل دليل للتسجيل أثناء التنفيذ آثاره.
-تختلف صيغ ملفات الشيفرة الأصلية والأنظمة المضيفة المدعومة باختلاف الموفّر. راجع [إعداد Hopper وGhidra](docs/installation.md#hopper)، و[دليل IDA](docs/ida-provider.md)، و[دعم Ghidra التجريبي على Windows](docs/windows-ghidra-p0.md). يدعم Ghidra أيضًا [تحليل DOS ذي 16 بت](docs/ghidra-dos.md). لاختيار الموفّر، راجع [دليل CLI](docs/cli.md#choose-a-provider). تحقّق من [توفر الميزات في الإصدارات](docs/installation.md#released-package-and-main) للميزات المضافة بعد أحدث إصدار على npm.
+تختلف صيغ ملفات الشيفرة الأصلية والأنظمة المضيفة المدعومة باختلاف الموفّر. راجع [إعداد Hopper وGhidra](../installation.md#hopper)، و[دليل IDA](../ida-provider.md)، و[دعم Ghidra التجريبي على Windows](../windows-ghidra-p0.md). يدعم Ghidra أيضًا [تحليل DOS ذي 16 بت](../ghidra-dos.md). لاختيار الموفّر، راجع [دليل CLI](../cli.md#choose-a-provider). تحقّق من [توفر الميزات في الإصدارات](../installation.md#released-package-and-main) للميزات المضافة بعد أحدث إصدار على npm.
## دراسات الحالة
-[](https://rea.tools/ar/showcase/)
+[](https://rea.tools/ar/showcase/)
### DX-Ball: إعادة بناء حساب توزيع الصوت بين القناتين
@@ -189,28 +189,28 @@ npx rea-agents@latest setup
أي وكلاء يمكنهم استخدام REA؟
-أي وكيل يدعم خوادم MCP المحلية. يهيّئ الإعداد [الوكلاء المدعومين](docs/installation.md#supported-agents)؛ ويمكن للعملاء الآخرين استخدام [تسجيل MCP يدويًا](docs/installation.md#mcp-registry).
+أي وكيل يدعم خوادم MCP المحلية. يهيّئ الإعداد [الوكلاء المدعومين](../installation.md#supported-agents)؛ ويمكن للعملاء الآخرين استخدام [تسجيل MCP يدويًا](../installation.md#mcp-registry).
هل أحتاج إلى Hopper أو Ghidra أو IDA؟
-يستخدم تحليل الشيفرة الأصلية المتعمق أحدها. يعمل الفحص الثابت لـ JavaScript و.NET دون محرك لتحليل الشيفرة الأصلية. يستطيع الإعداد تثبيت Hopper بعد الموافقة؛ أما Ghidra وIDA فيستخدمان تثبيتاتك الموجودة. راجع [إعداد الموفّرين](docs/installation.md#hopper).
+يستخدم تحليل الشيفرة الأصلية المتعمق أحدها. يعمل الفحص الثابت لـ JavaScript و.NET دون محرك لتحليل الشيفرة الأصلية. يستطيع الإعداد تثبيت Hopper بعد الموافقة؛ أما Ghidra وIDA فيستخدمان تثبيتاتك الموجودة. راجع [إعداد الموفّرين](../installation.md#hopper).
هل أحتاج إلى تشغيل Hopper أولًا؟
-يشغّل REA برنامج Hopper عندما تحتاج العملية إليه. على macOS، قد تظهر عند التشغيل الأول نافذة تطلب اختيار وضع العرض التجريبي أو تفعيل الترخيص. راجع [تشغيل Hopper واستكشاف الأخطاء](docs/installation.md#launcher-paths-and-troubleshooting).
+يشغّل REA برنامج Hopper عندما تحتاج العملية إليه. على macOS، قد تظهر عند التشغيل الأول نافذة تطلب اختيار وضع العرض التجريبي أو تفعيل الترخيص. راجع [تشغيل Hopper واستكشاف الأخطاء](../installation.md#launcher-paths-and-troubleshooting).
ماذا يفعل تثبيت المهارة من skills.sh؟
-توفّر المهارة تعليمات الاستقصاء لوكيلك. استخدم `npx rea-agents setup` لتسجيل خادم MCP الخاص بـ REA وتثبيت التعليمات المطابقة، ثم أعد تشغيل وكيلك. راجع [تثبيت المهارة وحدها](docs/installation.md#skill-only-installation).
+توفّر المهارة تعليمات الاستقصاء لوكيلك. استخدم `npx rea-agents setup` لتسجيل خادم MCP الخاص بـ REA وتثبيت التعليمات المطابقة، ثم أعد تشغيل وكيلك. راجع [تثبيت المهارة وحدها](../installation.md#skill-only-installation).
@@ -253,14 +253,14 @@ npx rea-agents@latest setup
ابدأ بـ [الأدلة العملية](https://rea.tools/ar/guides/) على الموقع. للخيارات الدقيقة والمتطلبات وعقود النتائج:
-- [التثبيت والإعداد](docs/installation.md): تسجيل الوكلاء، وتهيئة الموفّرين، والتحديثات، وإلغاء التثبيت.
-- [الجاهزية واستكشاف الأخطاء](docs/installation.md#check-readiness-for-your-task): تشخيص وكيل أو محرك تحليل محدد.
-- [CLI وEvidence](docs/cli.md): الأوامر، واختيار الموفّر، واللقطات، والاستيراد/التصدير، وحالات الخروج.
-- [عقود MCP](docs/mcp-contracts.md) و[تعليمات الوكلاء](docs/mcp-prompts.md): نتائج الأدوات، والجلسات، والاستقصاءات الموجّهة.
-- [كتالوج الأدوات](docs/mcp-contracts.md#generated-catalog): قائمة الأدوات والموفّرين وأوامر CLI المولّدة أثناء البناء.
-- [خطة العمل](docs/roadmap.md): الأعمال المخطط لها ومتابعة القدرات.
+- [التثبيت والإعداد](../installation.md): تسجيل الوكلاء، وتهيئة الموفّرين، والتحديثات، وإلغاء التثبيت.
+- [الجاهزية واستكشاف الأخطاء](../installation.md#check-readiness-for-your-task): تشخيص وكيل أو محرك تحليل محدد.
+- [CLI وEvidence](../cli.md): الأوامر، واختيار الموفّر، واللقطات، والاستيراد/التصدير، وحالات الخروج.
+- [عقود MCP](../mcp-contracts.md) و[تعليمات الوكلاء](../mcp-prompts.md): نتائج الأدوات، والجلسات، والاستقصاءات الموجّهة.
+- [كتالوج الأدوات](../mcp-contracts.md#generated-catalog): قائمة الأدوات والموفّرين وأوامر CLI المولّدة أثناء البناء.
+- [خطة العمل](../roadmap.md): الأعمال المخطط لها ومتابعة القدرات.
-أبلغ عن الثغرات وفق [SECURITY.md](SECURITY.md).
+أبلغ عن الثغرات وفق [SECURITY.md](../../SECURITY.md).
## سجل النجوم
@@ -278,7 +278,7 @@ npx rea-agents@latest setup
## إخلاء المسؤولية
-راجع [DISCLAIMER.md](DISCLAIMER.md) للحصول على إرشادات الاستخدام المسؤول.
+راجع [DISCLAIMER.md](../../DISCLAIMER.md) للحصول على إرشادات الاستخدام المسؤول.
REA مخصص للبحث والتعليم والتشغيل البيني وحفظ البرمجيات بشكل قانوني، ولتحليل البرمجيات التي تملكها أو لديك تفويض لفحصها.
@@ -290,14 +290,14 @@ applicable licenses.
نرحّب بمساعدتك في تطوير REA! [افتح issue](https://github.com/morluto/rea/issues) للإبلاغ عن خطأ أو اقتراح ميزة، أو [أرسل pull request](https://github.com/morluto/rea/pulls) لتحسين الشيفرة أو التوثيق.
-راجع [CONTRIBUTING.md](CONTRIBUTING.md) لإعداد بيئة التطوير والفحوص، و[دليل الاختبار](docs/testing.md) لمسارات التحقق، و[خريطة البنية](docs/architecture.mermaid) لهيكل المشروع.
+راجع [CONTRIBUTING.md](../../CONTRIBUTING.md) لإعداد بيئة التطوير والفحوص، و[دليل الاختبار](../testing.md) لمسارات التحقق، و[خريطة البنية](../architecture.mermaid) لهيكل المشروع.
## الترخيص
-[MIT](LICENSE)
+[MIT](../../LICENSE)
REA-authored code is licensed under MIT. Third-party material retains its own
-licenses and copyright notices; see [Third-party notices](THIRD_PARTY_NOTICES.md)
+licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
for the dependency inventory, preserved license texts, and external-tool terms.
**You must legally obtain and be authorized to use commercial tools**, including
diff --git a/README_de.md b/docs/readme/README_de.md
similarity index 76%
rename from README_de.md
rename to docs/readme/README_de.md
index 2c6d20290..2dbb1d41a 100644
--- a/README_de.md
+++ b/docs/readme/README_de.md
@@ -1,6 +1,6 @@
-[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · **Deutsch** · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · **Deutsch** · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Alles reverse-engineeren
@@ -9,10 +9,10 @@
**Eine Funktion gefällt dir? Verstehe, wie sie funktioniert – bis hinunter auf die Binärebene.**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#current-status)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
@@ -25,7 +25,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
Der Reverse-Engineering-Community beitreten
Discord · Fragen und Antworten · Ergebnisse teilen
@@ -67,7 +67,7 @@ npx rea-agents setup
Wähle deine Agenten aus, prüfe die geplanten Änderungen und bestätige sie. Die Einrichtung fügt den MCP-Server von REA und passende Workflow-Anweisungen hinzu und sichert vorhandene Konfigurationen. Starte deinen Agenten anschließend neu.
-Die Einrichtung unterstützt Claude Code, Codex, Cursor, Gemini CLI, Grok Build und [weitere Agenten](docs/installation.md#supported-agents). Informationen zur Provider-Konfiguration und manuellen MCP-Registrierung findest du unter [Installation und Einrichtung](docs/installation.md).
+Die Einrichtung unterstützt Claude Code, Codex, Cursor, Gemini CLI, Grok Build und [weitere Agenten](../installation.md#supported-agents). Informationen zur Provider-Konfiguration und manuellen MCP-Registrierung findest du unter [Installation und Einrichtung](../installation.md).
### Deinen Agenten fragen
@@ -94,7 +94,7 @@ npm install --global rea-agents
rea --help
```
-Konfiguriere für native Analysen zuerst einen Provider. Native Befehle, Provider-Auswahl, Snapshots und Skriptnutzung beschreibt der [CLI- und Evidence-Leitfaden](docs/cli.md).
+Konfiguriere für native Analysen zuerst einen Provider. Native Befehle, Provider-Auswahl, Snapshots und Skriptnutzung beschreibt der [CLI- und Evidence-Leitfaden](../cli.md).
### REA aktualisieren
@@ -120,9 +120,9 @@ Prüfe die Änderungen und starte deinen Agenten neu. Für einzelne CLI-Aufrufe
Dein Agent ruft REA über MCP auf, um das Ziel zu untersuchen und relevanten Code zu verfolgen. REA liefert Ergebnisse mit ihren Belegen zurück. Der Agent nutzt sie für weitere Fragen, Erklärungen oder zum Schreiben und Testen einer Implementierung. CLI-Befehle verwenden dieselben Workflows.
-
+
-[Abbildung in voller Größe öffnen](website/public/assets/figures/rea-investigation-flow.svg).
+[Abbildung in voller Größe öffnen](../../website/public/assets/figures/rea-investigation-flow.svg).
@@ -130,37 +130,37 @@ Dein Agent ruft REA über MCP auf, um das Ziel zu untersuchen und relevanten Cod
REA benötigt Node.js 22.x (>=22.19), 24.x (>=24.11) oder 26+ sowie npm. Zusätzliche Werkzeuge und unterstützte Hostsysteme hängen vom Ziel ab:
-| Ziel | Was REA zurückgibt | Voraussetzungen und Anleitung |
-| ----------------------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
-| Native Binärdateien | Pseudocode, Assemblercode, Zeichenketten, Symbole, Aufrufe und Verweise | Hopper, Ghidra oder IDA; [native Analyse](https://rea.tools/de/guides/native/) |
-| Offline-ELF-Struktur | Sektionen, Segmente, ursprüngliche Symbole/Relokationen und mögliche Schutzmaßnahmen aus statischer Analyse | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; [Binärdiagnostik](docs/binary-diagnostics.md) |
-| EVM-Bytecode | Dispatch-Selektoren, Byte-Offsets, abgeleitete Argumente und Veränderbarkeit des Zustands | Lokale Eingabe mit Rohbytes oder Hexdarstellung unter Linux x64; [Offline-EVM-Leitfaden](docs/evm-bytecode.md) |
-| Aufgezeichnete Linux-Abstürze | Unverarbeitete Note-Einträge, Register/Signale aller aufgezeichneten Threads und optionale Zuordnungskandidaten | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; optional GDB/pwndbg; [aufgezeichnete Abstürze](docs/recorded-crashes.md) |
-| JavaScript / Electron | Module, Imports, Source Maps, Routen, IPC und Beziehungen zu nativen Add-ons | Node.js und npm; [Anwendungsanalyse](https://rea.tools/de/guides/javascript/) |
-| Websites | Seitenstruktur, Skripte, Netzwerkbeobachtungen und angeforderte Screenshots | Ein Browser aus der Chrome-Familie; [Browseranalyse](https://rea.tools/de/guides/browser/) |
-| Gespeicherte Netzwerkaufzeichnungen | Anfragen, Antworten, zugängliche Nutzdaten und Quellpositionen | HAR; mitmdump unter Linux für Aufzeichnungen im nativen mitmproxy-Format; [Leitfaden zu Netzwerkaufzeichnungen](docs/web-network-captures.md) |
-| Paketerfassungen | Endpunkte, DNS, TLS SNI, Protokollverteilung und HTTP-Anfragen | Vom Aufrufer bereitgestellte tshark- und capinfos-Binaries; [pcap-Leitfaden](docs/pcap-analysis.md) |
-| .NET-Assemblies | Metadaten, CIL-Anweisungen, deklarierte native Abhängigkeiten und Build-Vergleiche | Statische Untersuchung; [Leitfaden für verwalteten Code](docs/managed-code-analysis.md) |
-| Android-APKs | Manifest-Deklarationen, Klassen, dekompilierte Methoden und Verweise | JADX ohne grafische Oberfläche und ein vollständiges JDK unter Linux/macOS/Windows x64; [Android-Leitfaden](docs/android-analysis.md) |
-| Android-Geräte | Geräte, Pakete, Prozesse, Logs, Dumps, Bildschirmaufnahmen sowie APK- und Dateiübertragungen | Vom Aufrufer bereitgestelltes adb (Emulator oder Gerät); [ADB-Leitfaden](docs/adb-device-analysis.md) |
-| JEB-Projekte | Engine-gestützte Projekteinheiten, Artefakt-Prüfsummen und Pseudokode von Typen/Methoden | Ein vom Aufrufer gestarteter JEB-Client mit MCP, ausgewählt über REA_JEB_MCP_URL; [JEB-Leitfaden](docs/jeb-analysis.md) |
-| Android-Ressourcen | Dekodiertes Manifest, String-Tabellen, Sprachen und Versionsfakten | Vom Aufrufer bereitgestellter Apktool-Starter; [Ressourcen-Leitfaden](docs/apktool-resource-analysis.md) |
-| Flutter-Apps | Dart-Snapshot-Hash, Per-ABI-Bibliotheksprüfsummen und Engine-Build-Provenienz | Keine (statische APK-Analyse); [Flutter-Leitfaden](docs/flutter-analysis.md) |
-| Firmware | Bereiche, Extraktionsergebnisse und Übergaben an die native Analyse | Binwalk / Unblob unter Linux; [Firmware-Leitfaden](docs/firmware-analysis.md) |
-| Aufgezeichnete Pakete | Ausgewählte Felder, wiederholte Werte, Filter und Decode-as-Regeln | Linux: TShark 4.x + prlimit; [Mitschnitt-Anleitung](docs/packet-captures.md) |
-| Aufgezeichnete Logiksignale | Decoder-Anmerkungen, Sample-Bereiche und Diagnosen | Linux: sigrok-cli 0.7.2 + prlimit; [Signalaufzeichnungen](docs/signal-recordings.md) |
-| Pakete und Ressourcen | Dateiinventare, Hashwerte, plists, Aufbau von Apple-Bundles und extrahierte Ressourcen | [Artefakt- und JavaScript-Leitfaden](docs/javascript-artifact-reconstruction.md), [Apple-Anwendungen](docs/apple-application-analysis.md) |
-| Prozessverhalten | Terminalausgabe, Interaktionen, Beobachtungen zu Prozessende und Dateisystem sowie Laufvergleiche | Linux/macOS mit nativer PTY; [Prozessaufzeichnung](docs/process-capture.md) |
+| Ziel | Was REA zurückgibt | Voraussetzungen und Anleitung |
+| ----------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
+| Native Binärdateien | Pseudocode, Assemblercode, Zeichenketten, Symbole, Aufrufe und Verweise | Hopper, Ghidra oder IDA; [native Analyse](https://rea.tools/de/guides/native/) |
+| Offline-ELF-Struktur | Sektionen, Segmente, ursprüngliche Symbole/Relokationen und mögliche Schutzmaßnahmen aus statischer Analyse | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; [Binärdiagnostik](../binary-diagnostics.md) |
+| EVM-Bytecode | Dispatch-Selektoren, Byte-Offsets, abgeleitete Argumente und Veränderbarkeit des Zustands | Lokale Eingabe mit Rohbytes oder Hexdarstellung unter Linux x64; [Offline-EVM-Leitfaden](../evm-bytecode.md) |
+| Aufgezeichnete Linux-Abstürze | Unverarbeitete Note-Einträge, Register/Signale aller aufgezeichneten Threads und optionale Zuordnungskandidaten | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; optional GDB/pwndbg; [aufgezeichnete Abstürze](../recorded-crashes.md) |
+| JavaScript / Electron | Module, Imports, Source Maps, Routen, IPC und Beziehungen zu nativen Add-ons | Node.js und npm; [Anwendungsanalyse](https://rea.tools/de/guides/javascript/) |
+| Websites | Seitenstruktur, Skripte, Netzwerkbeobachtungen und angeforderte Screenshots | Ein Browser aus der Chrome-Familie; [Browseranalyse](https://rea.tools/de/guides/browser/) |
+| Gespeicherte Netzwerkaufzeichnungen | Anfragen, Antworten, zugängliche Nutzdaten und Quellpositionen | HAR; mitmdump unter Linux für Aufzeichnungen im nativen mitmproxy-Format; [Leitfaden zu Netzwerkaufzeichnungen](../web-network-captures.md) |
+| Paketerfassungen | Endpunkte, DNS, TLS SNI, Protokollverteilung und HTTP-Anfragen | Vom Aufrufer bereitgestellte tshark- und capinfos-Binaries; [pcap-Leitfaden](../pcap-analysis.md) |
+| .NET-Assemblies | Metadaten, CIL-Anweisungen, deklarierte native Abhängigkeiten und Build-Vergleiche | Statische Untersuchung; [Leitfaden für verwalteten Code](../managed-code-analysis.md) |
+| Android-APKs | Manifest-Deklarationen, Klassen, dekompilierte Methoden und Verweise | JADX ohne grafische Oberfläche und ein vollständiges JDK unter Linux/macOS/Windows x64; [Android-Leitfaden](../android-analysis.md) |
+| Android-Geräte | Geräte, Pakete, Prozesse, Logs, Dumps, Bildschirmaufnahmen sowie APK- und Dateiübertragungen | Vom Aufrufer bereitgestelltes adb (Emulator oder Gerät); [ADB-Leitfaden](../adb-device-analysis.md) |
+| JEB-Projekte | Engine-gestützte Projekteinheiten, Artefakt-Prüfsummen und Pseudokode von Typen/Methoden | Ein vom Aufrufer gestarteter JEB-Client mit MCP, ausgewählt über REA_JEB_MCP_URL; [JEB-Leitfaden](../jeb-analysis.md) |
+| Android-Ressourcen | Dekodiertes Manifest, String-Tabellen, Sprachen und Versionsfakten | Vom Aufrufer bereitgestellter Apktool-Starter; [Ressourcen-Leitfaden](../apktool-resource-analysis.md) |
+| Flutter-Apps | Dart-Snapshot-Hash, Per-ABI-Bibliotheksprüfsummen und Engine-Build-Provenienz | Keine (statische APK-Analyse); [Flutter-Leitfaden](../flutter-analysis.md) |
+| Firmware | Bereiche, Extraktionsergebnisse und Übergaben an die native Analyse | Binwalk / Unblob unter Linux; [Firmware-Leitfaden](../firmware-analysis.md) |
+| Aufgezeichnete Pakete | Ausgewählte Felder, wiederholte Werte, Filter und Decode-as-Regeln | Linux: TShark 4.x + prlimit; [Mitschnitt-Anleitung](../packet-captures.md) |
+| Aufgezeichnete Logiksignale | Decoder-Anmerkungen, Sample-Bereiche und Diagnosen | Linux: sigrok-cli 0.7.2 + prlimit; [Signalaufzeichnungen](../signal-recordings.md) |
+| Pakete und Ressourcen | Dateiinventare, Hashwerte, plists, Aufbau von Apple-Bundles und extrahierte Ressourcen | [Artefakt- und JavaScript-Leitfaden](../javascript-artifact-reconstruction.md), [Apple-Anwendungen](../apple-application-analysis.md) |
+| Prozessverhalten | Terminalausgabe, Interaktionen, Beobachtungen zu Prozessende und Dateisystem sowie Laufvergleiche | Linux/macOS mit nativer PTY; [Prozessaufzeichnung](../process-capture.md) |
Statische JavaScript- und .NET-Untersuchungen lesen die bereitgestellten Dateien, ohne die Anwendung auszuführen. Laufzeitaufzeichnungen führen das gewählte Ziel mit deinen Benutzerrechten aus oder interagieren damit; die jeweiligen Anleitungen beschreiben die Auswirkungen.
-Native Formate und unterstützte Hostsysteme unterscheiden sich je nach Provider. Siehe [Hopper- und Ghidra-Einrichtung](docs/installation.md#hopper), den [IDA-Leitfaden](docs/ida-provider.md) und die [experimentelle Ghidra-Unterstützung unter Windows](docs/windows-ghidra-p0.md). Ghidra unterstützt auch [16-Bit-DOS-Analyse](docs/ghidra-dos.md). Die Provider-Auswahl beschreibt der [CLI-Leitfaden](docs/cli.md#choose-a-provider). Prüfe die [Verfügbarkeit in Releases](docs/installation.md#released-package-and-main) für Funktionen, die seit der neuesten npm-Version hinzugekommen sind.
+Native Formate und unterstützte Hostsysteme unterscheiden sich je nach Provider. Siehe [Hopper- und Ghidra-Einrichtung](../installation.md#hopper), den [IDA-Leitfaden](../ida-provider.md) und die [experimentelle Ghidra-Unterstützung unter Windows](../windows-ghidra-p0.md). Ghidra unterstützt auch [16-Bit-DOS-Analyse](../ghidra-dos.md). Die Provider-Auswahl beschreibt der [CLI-Leitfaden](../cli.md#choose-a-provider). Prüfe die [Verfügbarkeit in Releases](../installation.md#released-package-and-main) für Funktionen, die seit der neuesten npm-Version hinzugekommen sind.
## Fallstudien
-[](https://rea.tools/de/showcase/)
+[](https://rea.tools/de/showcase/)
### DX-Ball: Eine Stereo-Panning-Berechnung rekonstruieren
@@ -187,28 +187,28 @@ Wenn du mit REA etwas Interessantes untersucht hast, würden wir es gern sehen.
Welche Agenten können REA verwenden?
-Jeder Agent, der lokale MCP-Server unterstützt. Die Einrichtung konfiguriert die [unterstützten Agenten](docs/installation.md#supported-agents); andere Clients können die [manuelle MCP-Registrierung](docs/installation.md#mcp-registry) nutzen.
+Jeder Agent, der lokale MCP-Server unterstützt. Die Einrichtung konfiguriert die [unterstützten Agenten](../installation.md#supported-agents); andere Clients können die [manuelle MCP-Registrierung](../installation.md#mcp-registry) nutzen.
Brauche ich Hopper, Ghidra oder IDA?
-Für tiefgehende native Analysen wird eines dieser Programme verwendet. Statische JavaScript- und .NET-Untersuchungen funktionieren ohne native Analyse-Engine. Die Einrichtung kann Hopper nach Zustimmung installieren; Ghidra und IDA nutzen deine vorhandenen Installationen. Siehe [Provider-Einrichtung](docs/installation.md#hopper).
+Für tiefgehende native Analysen wird eines dieser Programme verwendet. Statische JavaScript- und .NET-Untersuchungen funktionieren ohne native Analyse-Engine. Die Einrichtung kann Hopper nach Zustimmung installieren; Ghidra und IDA nutzen deine vorhandenen Installationen. Siehe [Provider-Einrichtung](../installation.md#hopper).
Muss ich Hopper vorher starten?
-REA startet Hopper, wenn eine Operation es benötigt. Unter macOS kann beim ersten Start ein Dialog erscheinen, in dem du den Demomodus auswählst oder deine Lizenz aktivierst. Siehe [Hopper-Start und Fehlerbehebung](docs/installation.md#launcher-paths-and-troubleshooting).
+REA startet Hopper, wenn eine Operation es benötigt. Unter macOS kann beim ersten Start ein Dialog erscheinen, in dem du den Demomodus auswählst oder deine Lizenz aktivierst. Siehe [Hopper-Start und Fehlerbehebung](../installation.md#launcher-paths-and-troubleshooting).
Was bewirkt die Installation des Skills über skills.sh?
-Der Skill stellt Untersuchungsanweisungen für deinen Agenten bereit. Verwende `npx rea-agents setup`, um den MCP-Server von REA zu registrieren und passende Anweisungen zu installieren. Starte deinen Agenten anschließend neu. Siehe [Installation nur des Skills](docs/installation.md#skill-only-installation).
+Der Skill stellt Untersuchungsanweisungen für deinen Agenten bereit. Verwende `npx rea-agents setup`, um den MCP-Server von REA zu registrieren und passende Anweisungen zu installieren. Starte deinen Agenten anschließend neu. Siehe [Installation nur des Skills](../installation.md#skill-only-installation).
@@ -251,14 +251,14 @@ Wenn du einen Agenten verwendest, schließe die [Aktualisierung der Einrichtung]
Beginne mit den [praxisnahen Anleitungen](https://rea.tools/de/guides/) auf der Website. Genaue Optionen, Voraussetzungen und Ergebniskontrakte findest du hier:
-- [Installation und Einrichtung](docs/installation.md): Agentenregistrierung, Provider-Konfiguration, Updates und Deinstallation.
-- [Bereitschaft und Fehlerbehebung](docs/installation.md#check-readiness-for-your-task): Diagnose eines bestimmten Agenten oder einer Analyse-Engine.
-- [CLI und Evidence](docs/cli.md): Befehle, Provider-Auswahl, Snapshots, Import/Export und Exit-Status.
-- [MCP-Kontrakte](docs/mcp-contracts.md) und [Agenten-Prompts](docs/mcp-prompts.md): Werkzeugergebnisse, Sitzungen und geführte Untersuchungen.
-- [Werkzeugkatalog](docs/mcp-contracts.md#generated-catalog): Beim Build erzeugtes Inventar von Werkzeugen, Providern und CLI-Befehlen.
-- [Roadmap](docs/roadmap.md): Geplante Arbeiten und Fortschrittsübersichten zu Fähigkeiten.
+- [Installation und Einrichtung](../installation.md): Agentenregistrierung, Provider-Konfiguration, Updates und Deinstallation.
+- [Bereitschaft und Fehlerbehebung](../installation.md#check-readiness-for-your-task): Diagnose eines bestimmten Agenten oder einer Analyse-Engine.
+- [CLI und Evidence](../cli.md): Befehle, Provider-Auswahl, Snapshots, Import/Export und Exit-Status.
+- [MCP-Kontrakte](../mcp-contracts.md) und [Agenten-Prompts](../mcp-prompts.md): Werkzeugergebnisse, Sitzungen und geführte Untersuchungen.
+- [Werkzeugkatalog](../mcp-contracts.md#generated-catalog): Beim Build erzeugtes Inventar von Werkzeugen, Providern und CLI-Befehlen.
+- [Roadmap](../roadmap.md): Geplante Arbeiten und Fortschrittsübersichten zu Fähigkeiten.
-Melde Sicherheitslücken gemäß [SECURITY.md](SECURITY.md).
+Melde Sicherheitslücken gemäß [SECURITY.md](../../SECURITY.md).
## Sternverlauf
@@ -276,7 +276,7 @@ Danke an alle, die REA nutzen, Fehler melden, Funktionswünsche äußern, Builds
## Haftungsausschluss
-Hinweise zur verantwortungsvollen Nutzung findest du in [DISCLAIMER.md](DISCLAIMER.md).
+Hinweise zur verantwortungsvollen Nutzung findest du in [DISCLAIMER.md](../../DISCLAIMER.md).
REA dient der rechtmäßigen Forschung, Bildung, Interoperabilität, Softwareerhaltung und Analyse von Software, die dir gehört oder die du mit entsprechender Genehmigung untersuchen darfst.
@@ -288,14 +288,14 @@ applicable licenses.
Wir freuen uns über deine Hilfe bei REA! [Öffne ein Issue](https://github.com/morluto/rea/issues), um einen Fehler zu melden oder eine Funktion vorzuschlagen, oder [sende einen Pull Request](https://github.com/morluto/rea/pulls), um Code oder Dokumentation zu verbessern.
-Die Entwicklungsumgebung und Prüfungen beschreibt [CONTRIBUTING.md](CONTRIBUTING.md), die Verifikationspfade der [Testleitfaden](docs/testing.md) und die Projektstruktur die [Architekturübersicht](docs/architecture.mermaid).
+Die Entwicklungsumgebung und Prüfungen beschreibt [CONTRIBUTING.md](../../CONTRIBUTING.md), die Verifikationspfade der [Testleitfaden](../testing.md) und die Projektstruktur die [Architekturübersicht](../architecture.mermaid).
## Lizenz
-[MIT](LICENSE)
+[MIT](../../LICENSE)
REA-authored code is licensed under MIT. Third-party material retains its own
-licenses and copyright notices; see [Third-party notices](THIRD_PARTY_NOTICES.md)
+licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
for the dependency inventory, preserved license texts, and external-tool terms.
**You must legally obtain and be authorized to use commercial tools**, including
diff --git a/README_es.md b/docs/readme/README_es.md
similarity index 75%
rename from README_es.md
rename to docs/readme/README_es.md
index f955b5891..d09be25e2 100644
--- a/README_es.md
+++ b/docs/readme/README_es.md
@@ -1,6 +1,6 @@
-[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · **Español** · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · **Español** · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Ingeniería inversa de cualquier cosa
@@ -9,10 +9,10 @@
**¿Ves una función que te gusta? Entiende cómo funciona, hasta el nivel binario.**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#current-status)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
@@ -25,7 +25,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
Únete a la comunidad de ingeniería inversa
Discord · Preguntas y respuestas · Comparte tus resultados
@@ -67,7 +67,7 @@ npx rea-agents setup
Elige tus agentes, revisa los cambios propuestos y apruébalos. La configuración añade el servidor MCP de REA y las instrucciones de trabajo correspondientes, con copias de seguridad de la configuración existente. Reinicia tu agente al terminar.
-La configuración admite Claude Code, Codex, Cursor, Gemini CLI, Grok Build y [otros agentes](docs/installation.md#supported-agents). Consulta [instalación y configuración](docs/installation.md) para configurar proveedores y registrar MCP manualmente.
+La configuración admite Claude Code, Codex, Cursor, Gemini CLI, Grok Build y [otros agentes](../installation.md#supported-agents). Consulta [instalación y configuración](../installation.md) para configurar proveedores y registrar MCP manualmente.
### Pregunta a tu agente
@@ -94,7 +94,7 @@ npm install --global rea-agents
rea --help
```
-Para el análisis nativo, configura primero un proveedor. Consulta la [guía de CLI y Evidence](docs/cli.md) para los comandos nativos, la selección de proveedores, las instantáneas y el uso en scripts.
+Para el análisis nativo, configura primero un proveedor. Consulta la [guía de CLI y Evidence](../cli.md) para los comandos nativos, la selección de proveedores, las instantáneas y el uso en scripts.
### Actualiza REA
@@ -120,9 +120,9 @@ Revisa los cambios de configuración y reinicia tu agente. Para ejecutar comando
Tu agente llama a REA mediante MCP para inspeccionar el objetivo y seguir el código relevante. REA devuelve los hallazgos junto con sus pruebas. El agente los utiliza para hacer preguntas de seguimiento, explicar el comportamiento o escribir y probar una implementación. Los comandos de CLI usan los mismos flujos de trabajo.
-
+
-[Abre la figura a tamaño completo](website/public/assets/figures/rea-investigation-flow.svg).
+[Abre la figura a tamaño completo](../../website/public/assets/figures/rea-investigation-flow.svg).
@@ -130,37 +130,37 @@ Tu agente llama a REA mediante MCP para inspeccionar el objetivo y seguir el có
REA requiere Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, además de npm. Las herramientas adicionales y los sistemas anfitriones compatibles dependen del objetivo:
-| Objetivo | Qué devuelve REA | Requisitos y guía |
-| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
-| Binarios nativos | Pseudocódigo, ensamblador, cadenas, símbolos, llamadas y referencias | Hopper, Ghidra o IDA; [análisis nativo](https://rea.tools/es/guides/native/) |
-| Estructura ELF sin ejecutar | Secciones, segmentos, símbolos/reubicaciones originales y posibles medidas de mitigación detectadas mediante análisis estático | pwntools proporcionado por quien llama, en Linux x64; [diagnóstico de binarios](docs/binary-diagnostics.md) |
-| Bytecode EVM | Selectores de despacho, desplazamientos en bytes, argumentos inferidos y mutabilidad | Entrada local con bytes sin procesar o representación hexadecimal, en Linux x64; [guía de EVM sin ejecutar](docs/evm-bytecode.md) |
-| Fallos de Linux registrados | Registros note sin procesar, registros/señales de cada hilo registrado y posibles correspondencias de memoria opcionales | pwntools proporcionado por quien llama, en Linux x64; GDB/pwndbg opcionales; [fallos registrados](docs/recorded-crashes.md) |
-| JavaScript / Electron | Módulos, importaciones, mapas de código fuente, rutas, IPC y relaciones con complementos nativos | Node.js y npm; [análisis de aplicaciones](https://rea.tools/es/guides/javascript/) |
-| Sitios web | Estructura de la página, scripts, observaciones de red y capturas de pantalla solicitadas | Un navegador de la familia Chrome; [análisis del navegador](https://rea.tools/es/guides/browser/) |
-| Capturas de red guardadas | Solicitudes, respuestas, contenido accesible de las cargas útiles y ubicaciones de origen | HAR; mitmdump en Linux para capturas en formato nativo de mitmproxy; [guía de capturas](docs/web-network-captures.md) |
-| Capturas de paquetes | Endpoints, DNS, TLS SNI, distribución de protocolos y solicitudes HTTP | tshark y capinfos suministrados por el autor de la llamada; [guía pcap](docs/pcap-analysis.md) |
-| Ensamblados .NET | Metadatos, instrucciones CIL, dependencias nativas declaradas y comparaciones de compilaciones | Inspección estática; [guía de código administrado](docs/managed-code-analysis.md) |
-| APK de Android | Declaraciones del manifiesto, clases, métodos descompilados y referencias | JADX sin interfaz gráfica y un JDK completo en Linux/macOS/Windows x64; [guía de Android](docs/android-analysis.md) |
-| Dispositivos Android | Dispositivos, paquetes, procesos, registros, volcados, capturas de pantalla y transferencias de APK/archivos | adb suministrado por el autor de la llamada (emulador o dispositivo); [guía de ADB](docs/adb-device-analysis.md) |
-| Proyectos JEB | Unidades de proyecto del motor, resúmenes de artefactos y pseudocódigo de tipos/métodos | Un cliente JEB iniciado por el autor de la llamada que sirve MCP, seleccionado con REA_JEB_MCP_URL; [guía de JEB](docs/jeb-analysis.md) |
-| Recursos de Android | Manifiesto decodificado, tablas de cadenas, idiomas y datos de versión | Lanzador de Apktool suministrado por el autor de la llamada; [guía de recursos](docs/apktool-resource-analysis.md) |
-| Aplicaciones Flutter | Hash de snapshot de Dart, resúmenes de bibliotecas por ABI y procedencia de compilación del motor | Ninguna (análisis APK estático); [guía de Flutter](docs/flutter-analysis.md) |
-| Firmware | Regiones, resultados de extracción y derivaciones al análisis nativo | Binwalk / Unblob en Linux; [guía de firmware](docs/firmware-analysis.md) |
-| Paquetes grabados | Campos seleccionados, valores repetidos, filtros y reglas de decodificación | Linux: TShark 4.x + prlimit; [guía de capturas](docs/packet-captures.md) |
-| Señales lógicas grabadas | Anotaciones del decodificador, rangos de muestras y diagnósticos | Linux: sigrok-cli 0.7.2 + prlimit; [guía de señales](docs/signal-recordings.md) |
-| Paquetes y recursos | Inventarios de archivos, resúmenes criptográficos, plists, estructura de paquetes de Apple y recursos extraídos | [Guía de artefactos y JavaScript](docs/javascript-artifact-reconstruction.md), [aplicaciones de Apple](docs/apple-application-analysis.md) |
-| Comportamiento de procesos | Salida de la terminal, interacciones, observaciones de salida y del sistema de archivos, y comparaciones entre ejecuciones | Linux/macOS con una PTY nativa; [captura de procesos](docs/process-capture.md) |
+| Objetivo | Qué devuelve REA | Requisitos y guía |
+| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
+| Binarios nativos | Pseudocódigo, ensamblador, cadenas, símbolos, llamadas y referencias | Hopper, Ghidra o IDA; [análisis nativo](https://rea.tools/es/guides/native/) |
+| Estructura ELF sin ejecutar | Secciones, segmentos, símbolos/reubicaciones originales y posibles medidas de mitigación detectadas mediante análisis estático | pwntools proporcionado por quien llama, en Linux x64; [diagnóstico de binarios](../binary-diagnostics.md) |
+| Bytecode EVM | Selectores de despacho, desplazamientos en bytes, argumentos inferidos y mutabilidad | Entrada local con bytes sin procesar o representación hexadecimal, en Linux x64; [guía de EVM sin ejecutar](../evm-bytecode.md) |
+| Fallos de Linux registrados | Registros note sin procesar, registros/señales de cada hilo registrado y posibles correspondencias de memoria opcionales | pwntools proporcionado por quien llama, en Linux x64; GDB/pwndbg opcionales; [fallos registrados](../recorded-crashes.md) |
+| JavaScript / Electron | Módulos, importaciones, mapas de código fuente, rutas, IPC y relaciones con complementos nativos | Node.js y npm; [análisis de aplicaciones](https://rea.tools/es/guides/javascript/) |
+| Sitios web | Estructura de la página, scripts, observaciones de red y capturas de pantalla solicitadas | Un navegador de la familia Chrome; [análisis del navegador](https://rea.tools/es/guides/browser/) |
+| Capturas de red guardadas | Solicitudes, respuestas, contenido accesible de las cargas útiles y ubicaciones de origen | HAR; mitmdump en Linux para capturas en formato nativo de mitmproxy; [guía de capturas](../web-network-captures.md) |
+| Capturas de paquetes | Endpoints, DNS, TLS SNI, distribución de protocolos y solicitudes HTTP | tshark y capinfos suministrados por el autor de la llamada; [guía pcap](../pcap-analysis.md) |
+| Ensamblados .NET | Metadatos, instrucciones CIL, dependencias nativas declaradas y comparaciones de compilaciones | Inspección estática; [guía de código administrado](../managed-code-analysis.md) |
+| APK de Android | Declaraciones del manifiesto, clases, métodos descompilados y referencias | JADX sin interfaz gráfica y un JDK completo en Linux/macOS/Windows x64; [guía de Android](../android-analysis.md) |
+| Dispositivos Android | Dispositivos, paquetes, procesos, registros, volcados, capturas de pantalla y transferencias de APK/archivos | adb suministrado por el autor de la llamada (emulador o dispositivo); [guía de ADB](../adb-device-analysis.md) |
+| Proyectos JEB | Unidades de proyecto del motor, resúmenes de artefactos y pseudocódigo de tipos/métodos | Un cliente JEB iniciado por el autor de la llamada que sirve MCP, seleccionado con REA_JEB_MCP_URL; [guía de JEB](../jeb-analysis.md) |
+| Recursos de Android | Manifiesto decodificado, tablas de cadenas, idiomas y datos de versión | Lanzador de Apktool suministrado por el autor de la llamada; [guía de recursos](../apktool-resource-analysis.md) |
+| Aplicaciones Flutter | Hash de snapshot de Dart, resúmenes de bibliotecas por ABI y procedencia de compilación del motor | Ninguna (análisis APK estático); [guía de Flutter](../flutter-analysis.md) |
+| Firmware | Regiones, resultados de extracción y derivaciones al análisis nativo | Binwalk / Unblob en Linux; [guía de firmware](../firmware-analysis.md) |
+| Paquetes grabados | Campos seleccionados, valores repetidos, filtros y reglas de decodificación | Linux: TShark 4.x + prlimit; [guía de capturas](../packet-captures.md) |
+| Señales lógicas grabadas | Anotaciones del decodificador, rangos de muestras y diagnósticos | Linux: sigrok-cli 0.7.2 + prlimit; [guía de señales](../signal-recordings.md) |
+| Paquetes y recursos | Inventarios de archivos, resúmenes criptográficos, plists, estructura de paquetes de Apple y recursos extraídos | [Guía de artefactos y JavaScript](../javascript-artifact-reconstruction.md), [aplicaciones de Apple](../apple-application-analysis.md) |
+| Comportamiento de procesos | Salida de la terminal, interacciones, observaciones de salida y del sistema de archivos, y comparaciones entre ejecuciones | Linux/macOS con una PTY nativa; [captura de procesos](../process-capture.md) |
La inspección estática de JavaScript y .NET lee los archivos suministrados sin ejecutar la aplicación. La captura en ejecución inicia el objetivo seleccionado o interactúa con él con los permisos de tu usuario; cada guía describe sus efectos.
-Los formatos nativos y los sistemas anfitriones compatibles varían según el proveedor. Consulta [configuración de Hopper y Ghidra](docs/installation.md#hopper), la [guía de IDA](docs/ida-provider.md) y la [compatibilidad experimental de Ghidra en Windows](docs/windows-ghidra-p0.md). Ghidra también admite [análisis de DOS de 16 bits](docs/ghidra-dos.md). Para elegir un proveedor, consulta la [guía de CLI](docs/cli.md#choose-a-provider). Comprueba la [disponibilidad por versión](docs/installation.md#released-package-and-main) de las funciones añadidas después de la última versión publicada en npm.
+Los formatos nativos y los sistemas anfitriones compatibles varían según el proveedor. Consulta [configuración de Hopper y Ghidra](../installation.md#hopper), la [guía de IDA](../ida-provider.md) y la [compatibilidad experimental de Ghidra en Windows](../windows-ghidra-p0.md). Ghidra también admite [análisis de DOS de 16 bits](../ghidra-dos.md). Para elegir un proveedor, consulta la [guía de CLI](../cli.md#choose-a-provider). Comprueba la [disponibilidad por versión](../installation.md#released-package-and-main) de las funciones añadidas después de la última versión publicada en npm.
## Casos prácticos
-[](https://rea.tools/es/showcase/)
+[](https://rea.tools/es/showcase/)
### DX-Ball: reconstruir un cálculo de panoramización de sonido
@@ -187,28 +187,28 @@ Si has usado REA para investigar algo interesante, nos gustaría verlo. Comparte
¿Qué agentes pueden usar REA?
-Cualquier agente que admita servidores MCP locales. La configuración prepara los [agentes compatibles](docs/installation.md#supported-agents); otros clientes pueden usar el [registro manual de MCP](docs/installation.md#mcp-registry).
+Cualquier agente que admita servidores MCP locales. La configuración prepara los [agentes compatibles](../installation.md#supported-agents); otros clientes pueden usar el [registro manual de MCP](../installation.md#mcp-registry).
¿Necesito Hopper, Ghidra o IDA?
-El análisis nativo en profundidad utiliza uno de ellos. La inspección estática de JavaScript y .NET funciona sin un motor de análisis nativo. La configuración puede instalar Hopper después de tu aprobación; Ghidra e IDA usan las instalaciones que ya tienes. Consulta [configuración de proveedores](docs/installation.md#hopper).
+El análisis nativo en profundidad utiliza uno de ellos. La inspección estática de JavaScript y .NET funciona sin un motor de análisis nativo. La configuración puede instalar Hopper después de tu aprobación; Ghidra e IDA usan las instalaciones que ya tienes. Consulta [configuración de proveedores](../installation.md#hopper).
¿Tengo que iniciar Hopper primero?
-REA inicia Hopper cuando una operación lo necesita. En macOS, puede aparecer un diálogo al iniciarlo por primera vez para elegir el modo de demostración o activar tu licencia. Consulta [inicio de Hopper y resolución de problemas](docs/installation.md#launcher-paths-and-troubleshooting).
+REA inicia Hopper cuando una operación lo necesita. En macOS, puede aparecer un diálogo al iniciarlo por primera vez para elegir el modo de demostración o activar tu licencia. Consulta [inicio de Hopper y resolución de problemas](../installation.md#launcher-paths-and-troubleshooting).
¿Qué hace la instalación de la skill desde skills.sh?
-La skill proporciona instrucciones de investigación a tu agente. Usa `npx rea-agents setup` para registrar el servidor MCP de REA e instalar las instrucciones correspondientes; después reinicia tu agente. Consulta [instalación solo de la skill](docs/installation.md#skill-only-installation).
+La skill proporciona instrucciones de investigación a tu agente. Usa `npx rea-agents setup` para registrar el servidor MCP de REA e instalar las instrucciones correspondientes; después reinicia tu agente. Consulta [instalación solo de la skill](../installation.md#skill-only-installation).
@@ -251,14 +251,14 @@ Si usas un agente, completa la [actualización de la configuración](#actualiza-
Empieza con las [guías prácticas](https://rea.tools/es/guides/) del sitio web. Para opciones exactas, requisitos y contratos de resultados:
-- [Instalación y configuración](docs/installation.md): registro de agentes, configuración de proveedores, actualizaciones y desinstalación.
-- [Preparación y resolución de problemas](docs/installation.md#check-readiness-for-your-task): diagnóstico de un agente o motor de análisis concreto.
-- [CLI y Evidence](docs/cli.md): comandos, selección de proveedores, instantáneas, importación/exportación y estados de salida.
-- [Contratos de MCP](docs/mcp-contracts.md) y [prompts para agentes](docs/mcp-prompts.md): resultados de herramientas, sesiones e investigaciones guiadas.
-- [Catálogo de herramientas](docs/mcp-contracts.md#generated-catalog): inventario de herramientas, proveedores y comandos de CLI generado durante la compilación.
-- [Hoja de ruta](docs/roadmap.md): trabajo previsto y seguimiento de capacidades.
+- [Instalación y configuración](../installation.md): registro de agentes, configuración de proveedores, actualizaciones y desinstalación.
+- [Preparación y resolución de problemas](../installation.md#check-readiness-for-your-task): diagnóstico de un agente o motor de análisis concreto.
+- [CLI y Evidence](../cli.md): comandos, selección de proveedores, instantáneas, importación/exportación y estados de salida.
+- [Contratos de MCP](../mcp-contracts.md) y [prompts para agentes](../mcp-prompts.md): resultados de herramientas, sesiones e investigaciones guiadas.
+- [Catálogo de herramientas](../mcp-contracts.md#generated-catalog): inventario de herramientas, proveedores y comandos de CLI generado durante la compilación.
+- [Hoja de ruta](../roadmap.md): trabajo previsto y seguimiento de capacidades.
-Informa de vulnerabilidades siguiendo [SECURITY.md](SECURITY.md).
+Informa de vulnerabilidades siguiendo [SECURITY.md](../../SECURITY.md).
## Historial de estrellas
@@ -276,7 +276,7 @@ Gracias a quienes usan REA, informan de errores, solicitan funciones, prueban co
## Aviso legal
-Consulta [DISCLAIMER.md](DISCLAIMER.md) para obtener orientación sobre el uso responsable.
+Consulta [DISCLAIMER.md](../../DISCLAIMER.md) para obtener orientación sobre el uso responsable.
REA está destinado a la investigación, la educación, la interoperabilidad, la preservación de software y el análisis de software propio o autorizado, dentro de la ley.
@@ -288,14 +288,14 @@ applicable licenses.
¡Nos encantaría que ayudaras con REA! [Abre una issue](https://github.com/morluto/rea/issues) para comunicar un error o proponer una función, o [envía una pull request](https://github.com/morluto/rea/pulls) para mejorar el código o la documentación.
-Consulta [CONTRIBUTING.md](CONTRIBUTING.md) para preparar el entorno de desarrollo y las comprobaciones, la [guía de pruebas](docs/testing.md) para los procesos de verificación y el [mapa de arquitectura](docs/architecture.mermaid) para la estructura del proyecto.
+Consulta [CONTRIBUTING.md](../../CONTRIBUTING.md) para preparar el entorno de desarrollo y las comprobaciones, la [guía de pruebas](../testing.md) para los procesos de verificación y el [mapa de arquitectura](../architecture.mermaid) para la estructura del proyecto.
## Licencia
-[MIT](LICENSE)
+[MIT](../../LICENSE)
REA-authored code is licensed under MIT. Third-party material retains its own
-licenses and copyright notices; see [Third-party notices](THIRD_PARTY_NOTICES.md)
+licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
for the dependency inventory, preserved license texts, and external-tool terms.
**You must legally obtain and be authorized to use commercial tools**, including
diff --git a/README_fa.md b/docs/readme/README_fa.md
similarity index 74%
rename from README_fa.md
rename to docs/readme/README_fa.md
index 54af87c57..b24e4380c 100644
--- a/README_fa.md
+++ b/docs/readme/README_fa.md
@@ -1,6 +1,6 @@
-[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · **فارسی** · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · **فارسی** · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: مهندسی معکوس هر چیزی
@@ -9,27 +9,27 @@
**قابلیتی دیدهاید که دوستش دارید؟ سازوکارش را تا سطح باینری درک کنید.**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#current-status)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
**[وبسایت](https://rea.tools/fa/) · [راهنماها](https://rea.tools/fa/guides/) · [نمونههای عملی](https://rea.tools/fa/showcase/)**
-[شروع سریع](#شروع-سریع) · [REA چگونه کار میکند؟](#rea-چگونه-کار-میکند) · [چه چیزهایی را میتوان تحلیل کرد؟](#چه-چیزهایی-را-میتوان-تحلیل-کرد) · [نمونههای عملی](#نمونههای-عملی) · [پرسشهای متداول](#پرسشهای-متداول) · [مستندات](#مستندات)
+[شروع سریع](#شروع-سریع) · [REA چگونه کار میکند؟](#rea-چگونه-کار-میکند) · [چه چیزهایی را میتوان تحلیل کرد؟](#چه-چیزهایی-را-میتوان-تحلیل-کرد) · [نمونههای عملی](#نمونههای-عملی) · [پرسشهای متداول](#پرسشهای-متداول) · [مستندات](#مستندات)
npx rea-agents setup
-
+
-
+
@@ -59,7 +59,7 @@ npx rea-agents setup
ایجنتهای موردنظر را انتخاب کنید، تغییرات پیشنهادی را بررسی و تأیید کنید. فرایند راهاندازی، سرور MCP مربوط به REA و دستورالعملهای گردشکار متناظر را اضافه میکند و از پیکربندی موجود نسخهٔ پشتیبان میگیرد. پس از آن ایجنت خود را مجدداً راهاندازی کنید.
-راهاندازی از Claude Code، Codex، Cursor، Gemini CLI، Grok Build و [ایجنتهای دیگر](docs/installation.md#supported-agents) پشتیبانی میکند. برای پیکربندی ارائهدهنده و ثبت دستی MCP، [نصب و راهاندازی](docs/installation.md) را ببینید.
+راهاندازی از Claude Code، Codex، Cursor، Gemini CLI، Grok Build و [ایجنتهای دیگر](../installation.md#supported-agents) پشتیبانی میکند. برای پیکربندی ارائهدهنده و ثبت دستی MCP، [نصب و راهاندازی](../installation.md) را ببینید.
### از ایجنت خود بپرسید
@@ -86,7 +86,7 @@ npm install --global rea-agents
rea --help
```
-برای تحلیل بومی ابتدا یک ارائهدهنده پیکربندی کنید. برای دستورات بومی، انتخاب ارائهدهنده، snapshotها و اسکریپتنویسی، [راهنمای CLI و شواهد](docs/cli.md) را ببینید.
+برای تحلیل بومی ابتدا یک ارائهدهنده پیکربندی کنید. برای دستورات بومی، انتخاب ارائهدهنده، snapshotها و اسکریپتنویسی، [راهنمای CLI و شواهد](../cli.md) را ببینید.
### بهروزرسانی REA
@@ -112,9 +112,9 @@ npx rea-agents@latest setup
ایجنت شما از طریق MCP، REA را فراخوانی میکند تا هدف را بررسی و کد مرتبط را ردیابی کند. REA یافتهها را همراه با شواهد برمیگرداند. ایجنت از این نتایج برای طرح پرسشهای تکمیلی، توضیح رفتار یا نوشتن و آزمایش یک پیادهسازی استفاده میکند. دستورات CLI نیز از همین گردشکارها بهره میبرند.
-
+
-[نمایش تصویر در اندازهٔ کامل](website/public/assets/figures/rea-investigation-flow.svg).
+[نمایش تصویر در اندازهٔ کامل](../../website/public/assets/figures/rea-investigation-flow.svg).
@@ -122,37 +122,37 @@ npx rea-agents@latest setup
REA به Node.js 22.x (>=22.19)، 24.x (>=24.11) یا 26+، بههمراه npm نیاز دارد. ابزارهای اضافی و پشتیبانی سیستم میزبان به نوع هدف بستگی دارند:
-| هدف | خروجی REA | پیشنیازها و راهنما |
-| ---------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
-| باینریهای بومی | شبهکد، اسمبلی، رشتهها، نمادها، فراخوانیها و ارجاعات | Hopper، Ghidra یا IDA؛ [تحلیل بومی](https://rea.tools/fa/guides/native/) |
-| ساختار ELF آفلاین | بخشها، سگمنتها، نمادها و relocationهای اصلی و گزینههای احتمالی کاهش آسیبپذیری ایستا | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ [تشخیص باینری](docs/binary-diagnostics.md) |
-| بایتکد EVM | selectorهای dispatch، offsetهای بایتی، آرگومانهای استنباطشده و قابلیت تغییر وضعیت | دادهٔ خام/hex محلی روی Linux x64؛ [راهنمای EVM آفلاین](docs/evm-bytecode.md) |
-| کرشهای ثبتشدهٔ لینوکس | یادداشتهای خام، رجیسترها/سیگنالهای تمام threadهای ثبتشده و گزینههای احتمالی نگاشت | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ GDB/pwndbg اختیاری؛ [کرشهای ثبتشده](docs/recorded-crashes.md) |
-| JavaScript / Electron | ماژولها، importها، source mapها، مسیرها، IPC و ارتباط افزونههای بومی | Node.js و npm؛ [تحلیل برنامه](https://rea.tools/fa/guides/javascript/) |
-| وبسایتها | ساختار صفحه، اسکریپتها، مشاهدات شبکه و اسکرینشاتهای درخواستی | مرورگر مبتنی بر Chrome؛ [تحلیل مرورگر](https://rea.tools/fa/guides/browser/) |
-| دادههای ضبطشدهٔ شبکه | درخواستها، پاسخها، payloadهای آشکارشده و مکان آنها در منبع | HAR؛ ابزار mitmdump در لینوکس برای ضبطهای بومی mitmproxy؛ [راهنمای ضبط شبکه](docs/web-network-captures.md) |
-| اسمبلیهای .NET | فراداده، دستورالعملهای CIL، وابستگیهای بومی اعلامشده و مقایسهٔ buildها | بررسی ایستا؛ [راهنمای کد مدیریتشده](docs/managed-code-analysis.md) |
-| فایلهای APK اندروید | تعاریف Manifest، کلاسها، متدهای دیکامپایلشده و ارجاعات | JADX بدون رابط گرافیکی و JDK کامل در Linux/macOS/Windows x64؛ [راهنمای اندروید](docs/android-analysis.md) |
-| دستگاههای اندروید | دستگاهها، بستهها، فرایندها، لاگها، دامپها، اسکرینشاتها و انتقال APK/فایل | adb تأمینشده توسط فراخوانکننده (شبیهساز یا دستگاه)؛ [راهنمای ADB](docs/adb-device-analysis.md) |
-| پروژههای JEB | واحدهای پروژه از موتور، هشهای مصنوع و شبهکدِ نوع/متد | یک کلاینت JEB که توسط فراخوانکننده اجرا شده و MCP ارائه میدهد و با REA_JEB_MCP_URL انتخاب میشود؛ [راهنمای JEB](docs/jeb-analysis.md) |
-| منابع اندروید | مانیفست رمزگشاییشده، جداول رشتهها، زبانها و اطلاعات نسخه | راهانداز Apktool تأمینشده توسط فراخوانکننده؛ [راهنمای منابع](docs/apktool-resource-analysis.md) |
-| برنامههای فلاتر | هش اسنپشات دارت، هش کتابخانهها به تفکیک ABI و اصالت بیلد موتور | هیچ (تحلیل ایستا APK)؛ [راهنمای فلاتر](docs/flutter-analysis.md) |
-| Firmware | ناحیهها، نتایج استخراج و تحویل به ابزارهای تحلیل بومی | Binwalk / Unblob در لینوکس؛ [راهنمای Firmware](docs/firmware-analysis.md) |
-| بستههای ضبطشده | فیلدهای منتخب، مقادیر تکراری، فیلترها و قواعد رمزگشایی | Linux: TShark 4.x + prlimit; [راهنمای ضبط بسته](docs/packet-captures.md) |
-| ضبط بستهها | نقاط پایانی، DNS، TLS SNI، توزیع پروتکل و درخواستهای HTTP | tshark و capinfos تأمینشده توسط فراخوانکننده؛ [راهنمای pcap](docs/pcap-analysis.md) |
-| سیگنالهای منطقی ضبطشده | توضیحات رمزگشا، بازههای نمونه و اطلاعات عیبیابی | Linux: sigrok-cli 0.7.2 + prlimit; [راهنمای سیگنالها](docs/signal-recordings.md) |
-| بستهها و منابع | فهرست فایلها، digestها، فایلهای plist، ساختار bundle اپل و منابع استخراجشده | [راهنمای آرتیفکت و JavaScript](docs/javascript-artifact-reconstruction.md)، [برنامههای اپل](docs/apple-application-analysis.md) |
-| رفتار فرایند | خروجی ترمینال، تعاملات، وضعیت خروج، مشاهدات سیستم فایل و مقایسهٔ اجراها | Linux/macOS با PTY بومی؛ [ضبط فرایند](docs/process-capture.md) |
+| هدف | خروجی REA | پیشنیازها و راهنما |
+| ---------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
+| باینریهای بومی | شبهکد، اسمبلی، رشتهها، نمادها، فراخوانیها و ارجاعات | Hopper، Ghidra یا IDA؛ [تحلیل بومی](https://rea.tools/fa/guides/native/) |
+| ساختار ELF آفلاین | بخشها، سگمنتها، نمادها و relocationهای اصلی و گزینههای احتمالی کاهش آسیبپذیری ایستا | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ [تشخیص باینری](../binary-diagnostics.md) |
+| بایتکد EVM | selectorهای dispatch، offsetهای بایتی، آرگومانهای استنباطشده و قابلیت تغییر وضعیت | دادهٔ خام/hex محلی روی Linux x64؛ [راهنمای EVM آفلاین](../evm-bytecode.md) |
+| کرشهای ثبتشدهٔ لینوکس | یادداشتهای خام، رجیسترها/سیگنالهای تمام threadهای ثبتشده و گزینههای احتمالی نگاشت | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ GDB/pwndbg اختیاری؛ [کرشهای ثبتشده](../recorded-crashes.md) |
+| JavaScript / Electron | ماژولها، importها، source mapها، مسیرها، IPC و ارتباط افزونههای بومی | Node.js و npm؛ [تحلیل برنامه](https://rea.tools/fa/guides/javascript/) |
+| وبسایتها | ساختار صفحه، اسکریپتها، مشاهدات شبکه و اسکرینشاتهای درخواستی | مرورگر مبتنی بر Chrome؛ [تحلیل مرورگر](https://rea.tools/fa/guides/browser/) |
+| دادههای ضبطشدهٔ شبکه | درخواستها، پاسخها، payloadهای آشکارشده و مکان آنها در منبع | HAR؛ ابزار mitmdump در لینوکس برای ضبطهای بومی mitmproxy؛ [راهنمای ضبط شبکه](../web-network-captures.md) |
+| اسمبلیهای .NET | فراداده، دستورالعملهای CIL، وابستگیهای بومی اعلامشده و مقایسهٔ buildها | بررسی ایستا؛ [راهنمای کد مدیریتشده](../managed-code-analysis.md) |
+| فایلهای APK اندروید | تعاریف Manifest، کلاسها، متدهای دیکامپایلشده و ارجاعات | JADX بدون رابط گرافیکی و JDK کامل در Linux/macOS/Windows x64؛ [راهنمای اندروید](../android-analysis.md) |
+| دستگاههای اندروید | دستگاهها، بستهها، فرایندها، لاگها، دامپها، اسکرینشاتها و انتقال APK/فایل | adb تأمینشده توسط فراخوانکننده (شبیهساز یا دستگاه)؛ [راهنمای ADB](../adb-device-analysis.md) |
+| پروژههای JEB | واحدهای پروژه از موتور، هشهای مصنوع و شبهکدِ نوع/متد | یک کلاینت JEB که توسط فراخوانکننده اجرا شده و MCP ارائه میدهد و با REA_JEB_MCP_URL انتخاب میشود؛ [راهنمای JEB](../jeb-analysis.md) |
+| منابع اندروید | مانیفست رمزگشاییشده، جداول رشتهها، زبانها و اطلاعات نسخه | راهانداز Apktool تأمینشده توسط فراخوانکننده؛ [راهنمای منابع](../apktool-resource-analysis.md) |
+| برنامههای فلاتر | هش اسنپشات دارت، هش کتابخانهها به تفکیک ABI و اصالت بیلد موتور | هیچ (تحلیل ایستا APK)؛ [راهنمای فلاتر](../flutter-analysis.md) |
+| Firmware | ناحیهها، نتایج استخراج و تحویل به ابزارهای تحلیل بومی | Binwalk / Unblob در لینوکس؛ [راهنمای Firmware](../firmware-analysis.md) |
+| بستههای ضبطشده | فیلدهای منتخب، مقادیر تکراری، فیلترها و قواعد رمزگشایی | Linux: TShark 4.x + prlimit; [راهنمای ضبط بسته](../packet-captures.md) |
+| ضبط بستهها | نقاط پایانی، DNS، TLS SNI، توزیع پروتکل و درخواستهای HTTP | tshark و capinfos تأمینشده توسط فراخوانکننده؛ [راهنمای pcap](../pcap-analysis.md) |
+| سیگنالهای منطقی ضبطشده | توضیحات رمزگشا، بازههای نمونه و اطلاعات عیبیابی | Linux: sigrok-cli 0.7.2 + prlimit; [راهنمای سیگنالها](../signal-recordings.md) |
+| بستهها و منابع | فهرست فایلها، digestها، فایلهای plist، ساختار bundle اپل و منابع استخراجشده | [راهنمای آرتیفکت و JavaScript](../javascript-artifact-reconstruction.md)، [برنامههای اپل](../apple-application-analysis.md) |
+| رفتار فرایند | خروجی ترمینال، تعاملات، وضعیت خروج، مشاهدات سیستم فایل و مقایسهٔ اجراها | Linux/macOS با PTY بومی؛ [ضبط فرایند](../process-capture.md) |
بررسی ایستای JavaScript و .NET فایلهای ارائهشده را بدون اجرای برنامه میخواند. ضبط زمان اجرا، هدف انتخابشده را با مجوزهای کاربری شما اجرا میکند یا با آن تعامل دارد؛ آثار این عملیات در راهنمای مربوط به هر نوع ضبط توضیح داده شدهاند.
-فرمتهای بومی و پشتیبانی سیستم میزبان بر اساس ارائهدهنده متفاوتاند. [راهاندازی Hopper و Ghidra](docs/installation.md#hopper)، [راهنمای IDA](docs/ida-provider.md) و [پشتیبانی آزمایشی Ghidra در ویندوز](docs/windows-ghidra-p0.md) را ببینید. Ghidra همچنین از [تحلیل DOS شانزدهبیتی](docs/ghidra-dos.md) پشتیبانی میکند. برای انتخاب ارائهدهنده، به [راهنمای CLI](docs/cli.md#choose-a-provider) مراجعه کنید. برای قابلیتهایی که پس از آخرین انتشار npm اضافه شدهاند، [وضعیت انتشار](docs/installation.md#released-package-and-main) را بررسی کنید.
+فرمتهای بومی و پشتیبانی سیستم میزبان بر اساس ارائهدهنده متفاوتاند. [راهاندازی Hopper و Ghidra](../installation.md#hopper)، [راهنمای IDA](../ida-provider.md) و [پشتیبانی آزمایشی Ghidra در ویندوز](../windows-ghidra-p0.md) را ببینید. Ghidra همچنین از [تحلیل DOS شانزدهبیتی](../ghidra-dos.md) پشتیبانی میکند. برای انتخاب ارائهدهنده، به [راهنمای CLI](../cli.md#choose-a-provider) مراجعه کنید. برای قابلیتهایی که پس از آخرین انتشار npm اضافه شدهاند، [وضعیت انتشار](../installation.md#released-package-and-main) را بررسی کنید.
## نمونههای عملی
-[](https://rea.tools/fa/showcase/)
+[](https://rea.tools/fa/showcase/)
### DX-Ball: بازسازی محاسبهٔ موقعیت استریوی صدا
@@ -178,31 +178,31 @@ API کلیپبورد در renderer را پیدا کنید، مسیر آن ر
کدام ایجنتها میتوانند از REA استفاده کنند؟
-هر ایجنتی که از سرورهای MCP محلی پشتیبانی کند. فرایند setup، [ایجنتهای پشتیبانیشده](docs/installation.md#supported-agents) را پیکربندی میکند؛ سایر کلاینتها میتوانند از [ثبت دستی MCP](docs/installation.md#mcp-registry) استفاده کنند.
+هر ایجنتی که از سرورهای MCP محلی پشتیبانی کند. فرایند setup، [ایجنتهای پشتیبانیشده](../installation.md#supported-agents) را پیکربندی میکند؛ سایر کلاینتها میتوانند از [ثبت دستی MCP](../installation.md#mcp-registry) استفاده کنند.
آیا به Hopper، Ghidra یا IDA نیاز دارم؟
-تحلیل عمیق بومی به یکی از آنها نیاز دارد. بررسی ایستای JavaScript و .NET بدون موتور تحلیل بومی کار میکند. setup میتواند با تأیید شما Hopper را نصب کند؛ Ghidra و IDA از نصبهای موجود شما استفاده میکنند. [راهاندازی ارائهدهنده](docs/installation.md#hopper) را ببینید.
+تحلیل عمیق بومی به یکی از آنها نیاز دارد. بررسی ایستای JavaScript و .NET بدون موتور تحلیل بومی کار میکند. setup میتواند با تأیید شما Hopper را نصب کند؛ Ghidra و IDA از نصبهای موجود شما استفاده میکنند. [راهاندازی ارائهدهنده](../installation.md#hopper) را ببینید.
آیا باید ابتدا Hopper را اجرا کنم؟
-REA زمانی که عملیاتی به Hopper نیاز داشته باشد آن را اجرا میکند. در macOS، ممکن است هنگام نخستین اجرا پنجرهای برای انتخاب حالت آزمایشی یا فعالسازی مجوز نمایش داده شود. [راهاندازی Hopper و رفع اشکال](docs/installation.md#launcher-paths-and-troubleshooting) را ببینید.
+REA زمانی که عملیاتی به Hopper نیاز داشته باشد آن را اجرا میکند. در macOS، ممکن است هنگام نخستین اجرا پنجرهای برای انتخاب حالت آزمایشی یا فعالسازی مجوز نمایش داده شود. [راهاندازی Hopper و رفع اشکال](../installation.md#launcher-paths-and-troubleshooting) را ببینید.
نصب skill از skills.sh چه کاری انجام میدهد؟
-این skill دستورالعملهای بررسی را برای ایجنت فراهم میکند. برای ثبت سرور MCP مربوط به REA و نصب دستورالعملهای متناظر، `npx rea-agents setup` را اجرا کنید و سپس ایجنت را مجدداً راهاندازی کنید. [نصب مستقل skill](docs/installation.md#skill-only-installation) را ببینید.
+این skill دستورالعملهای بررسی را برای ایجنت فراهم میکند. برای ثبت سرور MCP مربوط به REA و نصب دستورالعملهای متناظر، `npx rea-agents setup` را اجرا کنید و سپس ایجنت را مجدداً راهاندازی کنید. [نصب مستقل skill](../installation.md#skill-only-installation) را ببینید.
REA چه نوع کدی برمیگرداند؟
-تحلیل بومی، شبهکد و اسمبلی برمیگرداند. تحلیل JavaScript/Electron ماژولها و روابط میان آنها را بازیابی میکند. ایجنت شما از این یافتهها برای نوشتن و آزمایش یک پیادهسازی استفاده میکند؛ [نمونههای عملی](#نمونههای-عملی) مثالهای گامبهگام ارائه میدهند.
+تحلیل بومی، شبهکد و اسمبلی برمیگرداند. تحلیل JavaScript/Electron ماژولها و روابط میان آنها را بازیابی میکند. ایجنت شما از این یافتهها برای نوشتن و آزمایش یک پیادهسازی استفاده میکند؛ [نمونههای عملی](#نمونههای-عملی) مثالهای گامبهگام ارائه میدهند.
@@ -228,7 +228,7 @@ rea update
npx rea-agents@latest setup
```
-اگر از ایجنت استفاده میکنید، [بهروزرسانی راهاندازی](#بهروزرسانی-rea) را تکمیل و ایجنت را مجدداً اجرا کنید. همان کار را دوباره امتحان کنید. اگر مشکل ادامه داشت، یک [issue ثبت کنید](https://github.com/morluto/rea/issues) و نسخهٔ REA، نوع هدف، مراحل بازتولید و خروجی خطا را ذکر کنید.
+اگر از ایجنت استفاده میکنید، [بهروزرسانی راهاندازی](#بهروزرسانی-rea) را تکمیل و ایجنت را مجدداً اجرا کنید. همان کار را دوباره امتحان کنید. اگر مشکل ادامه داشت، یک [issue ثبت کنید](https://github.com/morluto/rea/issues) و نسخهٔ REA، نوع هدف، مراحل بازتولید و خروجی خطا را ذکر کنید.
@@ -236,14 +236,14 @@ npx rea-agents@latest setup
از [راهنماهای عملی وبسایت](https://rea.tools/fa/guides/) شروع کنید. برای گزینههای دقیق، پیشنیازها و قراردادهای خروجی:
-- [نصب و راهاندازی](docs/installation.md): ثبت ایجنت، پیکربندی ارائهدهنده، بهروزرسانی و حذف نصب.
-- [آمادگی و رفع اشکال](docs/installation.md#check-readiness-for-your-task): عیبیابی یک ایجنت یا موتور تحلیل.
-- [CLI و شواهد](docs/cli.md): دستورات، انتخاب ارائهدهنده، snapshotها، ورود/خروج داده و وضعیتهای خروج.
-- [قراردادهای MCP](docs/mcp-contracts.md) و [پرامپتهای ایجنت](docs/mcp-prompts.md): نتایج ابزارها، نشستها و بررسیهای هدایتشده.
-- [فهرست ابزارها](docs/mcp-contracts.md#generated-catalog): فهرست تولیدشده هنگام build از ابزارها، ارائهدهندگان و دستورات CLI.
-- [نقشهٔ راه](docs/roadmap.md): برنامههای آینده و پیگیری قابلیتها.
+- [نصب و راهاندازی](../installation.md): ثبت ایجنت، پیکربندی ارائهدهنده، بهروزرسانی و حذف نصب.
+- [آمادگی و رفع اشکال](../installation.md#check-readiness-for-your-task): عیبیابی یک ایجنت یا موتور تحلیل.
+- [CLI و شواهد](../cli.md): دستورات، انتخاب ارائهدهنده، snapshotها، ورود/خروج داده و وضعیتهای خروج.
+- [قراردادهای MCP](../mcp-contracts.md) و [پرامپتهای ایجنت](../mcp-prompts.md): نتایج ابزارها، نشستها و بررسیهای هدایتشده.
+- [فهرست ابزارها](../mcp-contracts.md#generated-catalog): فهرست تولیدشده هنگام build از ابزارها، ارائهدهندگان و دستورات CLI.
+- [نقشهٔ راه](../roadmap.md): برنامههای آینده و پیگیری قابلیتها.
-آسیبپذیریها را از طریق [SECURITY.md](SECURITY.md) گزارش کنید.
+آسیبپذیریها را از طریق [SECURITY.md](../../SECURITY.md) گزارش کنید.
## تاریخچهٔ ستارهها
@@ -255,7 +255,7 @@ npx rea-agents@latest setup
## سلب مسئولیت
-برای راهنمای استفادهٔ مسئولانه، [DISCLAIMER.md](DISCLAIMER.md) را ببینید.
+برای راهنمای استفادهٔ مسئولانه، [DISCLAIMER.md](../../DISCLAIMER.md) را ببینید.
REA برای پژوهش، آموزش، تعاملپذیری، حفظ نرمافزار و تحلیل نرمافزارهایی است که مالک آنها هستید یا اجازهٔ بررسی آنها را دارید، در چارچوب قانون.
@@ -267,14 +267,14 @@ applicable licenses.
از کمک شما به REA استقبال میکنیم! برای گزارش خطا یا پیشنهاد قابلیت، یک [issue باز کنید](https://github.com/morluto/rea/issues) یا برای بهبود کد و مستندات [pull request بفرستید](https://github.com/morluto/rea/pulls).
-برای راهاندازی محیط توسعه و بررسیها، [CONTRIBUTING.md](CONTRIBUTING.md)، برای مراحل اعتبارسنجی [آزمایشها](docs/testing.md) و برای ساختار پروژه [نقشهٔ معماری](docs/architecture.mermaid) را ببینید.
+برای راهاندازی محیط توسعه و بررسیها، [CONTRIBUTING.md](../../CONTRIBUTING.md)، برای مراحل اعتبارسنجی [آزمایشها](../testing.md) و برای ساختار پروژه [نقشهٔ معماری](../architecture.mermaid) را ببینید.
## مجوز
-[MIT](LICENSE)
+[MIT](../../LICENSE)
REA-authored code is licensed under MIT. Third-party material retains its own
-licenses and copyright notices; see [Third-party notices](THIRD_PARTY_NOTICES.md)
+licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
for the dependency inventory, preserved license texts, and external-tool terms.
**You must legally obtain and be authorized to use commercial tools**, including
diff --git a/README_fr.md b/docs/readme/README_fr.md
similarity index 76%
rename from README_fr.md
rename to docs/readme/README_fr.md
index 346210525..e6278ac93 100644
--- a/README_fr.md
+++ b/docs/readme/README_fr.md
@@ -1,6 +1,6 @@
-[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · **Français** · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · **Français** · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA : rétro-ingénierie de tout
@@ -9,10 +9,10 @@
**Repérez une fonctionnalité qui vous plaît. Comprenez son fonctionnement, jusqu'au niveau du binaire.**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#current-status)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
@@ -25,7 +25,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
Rejoignez la communauté de rétro-ingénierie
Discord · Questions et réponses · Partage de projets
@@ -67,7 +67,7 @@ npx rea-agents setup
Choisissez vos agents, vérifiez les modifications proposées et approuvez-les. Setup ajoute le serveur MCP de REA et les instructions de workflow correspondantes, et sauvegarde la configuration existante. Redémarrez ensuite votre agent.
-Setup prend en charge Claude Code, Codex, Cursor, Gemini CLI, Grok Build et [d'autres agents](docs/installation.md#supported-agents). Consultez [Installation et configuration](docs/installation.md) pour la configuration des fournisseurs et l'enregistrement MCP manuel.
+Setup prend en charge Claude Code, Codex, Cursor, Gemini CLI, Grok Build et [d'autres agents](../installation.md#supported-agents). Consultez [Installation et configuration](../installation.md) pour la configuration des fournisseurs et l'enregistrement MCP manuel.
### Interroger votre agent
@@ -95,7 +95,7 @@ npm install --global rea-agents
rea --help
```
-Pour l'analyse native, configurez d'abord un fournisseur. Consultez le [guide CLI et Evidence](docs/cli.md) pour les commandes natives, le choix du fournisseur, les snapshots et l'utilisation dans des scripts.
+Pour l'analyse native, configurez d'abord un fournisseur. Consultez le [guide CLI et Evidence](../cli.md) pour les commandes natives, le choix du fournisseur, les snapshots et l'utilisation dans des scripts.
### Mettre à jour REA
@@ -123,9 +123,9 @@ Vérifiez les modifications proposées par Setup et redémarrez votre agent. Pou
Votre agent appelle REA via MCP pour inspecter la cible et suivre le code pertinent. REA renvoie ses constatations avec les preuves correspondantes. L'agent s'en sert pour poser des questions complémentaires, expliquer le comportement, ou écrire et tester une implémentation. Les commandes CLI utilisent les mêmes workflows.
-
+
-[Ouvrir la figure en taille réelle](website/public/assets/figures/rea-investigation-flow.svg).
+[Ouvrir la figure en taille réelle](../../website/public/assets/figures/rea-investigation-flow.svg).
@@ -133,38 +133,38 @@ Votre agent appelle REA via MCP pour inspecter la cible et suivre le code pertin
REA nécessite Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, ainsi que npm. Les outils supplémentaires et les hôtes pris en charge dépendent de la cible :
-| Cible | Ce que REA renvoie | Prérequis et guide |
-| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
-| Binaires natifs | Pseudo-code, assembleur, chaînes, symboles, appels et références | Hopper, Ghidra ou IDA ; [analyse native](https://rea.tools/fr/guides/native/) |
-| Structure ELF hors ligne | Sections, segments, symboles et relocations d'origine, et protections statiques candidates | pwntools fourni par l'utilisateur sous Linux x64 ; [diagnostics binaires](docs/binary-diagnostics.md) |
-| Artefact WASM | Validation, sections, formes WAT exactes d'import/export et WAT décodé | WABT 1.0.42 fourni par l'utilisateur ; `inspect_wasm_artifact` |
-| Bytecode EVM | Sélecteurs de dispatch, offsets en octets, arguments inférés et mutabilité | Fichier local brut ou hexadécimal sous Linux x64 ; [guide EVM hors ligne](docs/evm-bytecode.md) |
-| Crashs Linux enregistrés | Notes brutes, registres et signaux de chaque thread enregistré, et correspondances de mappings facultatives | pwntools fourni par l'utilisateur sous Linux x64 ; GDB/pwndbg en option ; [crashs enregistrés](docs/recorded-crashes.md) |
-| JavaScript / Electron | Modules, imports, source maps, routes, IPC et relations avec les modules natifs | Node.js et npm ; [analyse d'applications](https://rea.tools/fr/guides/javascript/) |
-| Sites web | Structure des pages, scripts, observations réseau et captures d'écran demandées | Un navigateur de la famille Chrome ; [analyse du navigateur](https://rea.tools/fr/guides/browser/) |
-| Captures réseau enregistrées | Requêtes, réponses, payloads exposés et emplacements dans le code source | HAR ; mitmdump sous Linux pour les captures mitmproxy natives ; [guide des captures](docs/web-network-captures.md) |
-| Captures de paquets | Points de terminaison, DNS, TLS SNI, distribution des protocoles et requêtes HTTP | tshark et capinfos fournis par l'appelant ; [guide pcap](docs/pcap-analysis.md) |
-| Assemblys .NET | Métadonnées, instructions CIL, dépendances natives déclarées et comparaisons de builds | Inspection statique ; [guide du code managé](docs/managed-code-analysis.md) |
-| APK Android | Déclarations du manifeste, classes, méthodes décompilées et références | JADX headless et un JDK complet sous Linux/macOS/Windows x64 ; [guide Android](docs/android-analysis.md) |
-| Appareils Android | Appareils, paquets, processus, journaux, dumps, captures d'écran et transferts d'APK/fichiers | adb fourni par l'appelant (émulateur ou appareil) ; [guide ADB](docs/adb-device-analysis.md) |
-| Projets JEB | Unités de projet fournies par le moteur, condensés d'artefacts et pseudo-code de types/méthodes | Un client JEB démarré par l'appelant servant MCP, sélectionné via REA_JEB_MCP_URL ; [guide JEB](docs/jeb-analysis.md) |
-| Ressources Android | Manifeste décodé, tables de chaînes, langues et faits de version | Lanceur Apktool fourni par l'appelant ; [guide des ressources](docs/apktool-resource-analysis.md) |
-| Applications Flutter | Empreinte de snapshot Dart, condensés de bibliothèques par ABI et provenance de build du moteur | Aucune (analyse APK statique) ; [guide Flutter](docs/flutter-analysis.md) |
-| Firmware | Régions, résultats d'extraction et passage de relais vers l'analyse native | Binwalk / Unblob sous Linux ; [guide firmware](docs/firmware-analysis.md) |
-| Paquets enregistrés | Champs sélectionnés, valeurs répétées, filtres et règles de décodage | Linux: TShark 4.x + prlimit; [guide des captures](docs/packet-captures.md) |
-| Signaux logiques enregistrés | Annotations du décodeur, plages d’échantillons et diagnostics | Linux: sigrok-cli 0.7.2 + prlimit; [guide des signaux](docs/signal-recordings.md) |
-| Paquets et ressources | Inventaires de fichiers, empreintes, plists, anatomie des bundles Apple et ressources extraites | [Guide artefacts et JavaScript](docs/javascript-artifact-reconstruction.md), [applications Apple](docs/apple-application-analysis.md) |
-| Comportement des processus | Sortie du terminal, interactions, code de sortie, observations du système de fichiers et comparaisons | Linux/macOS avec un PTY natif ; [capture de processus](docs/process-capture.md) |
+| Cible | Ce que REA renvoie | Prérequis et guide |
+| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
+| Binaires natifs | Pseudo-code, assembleur, chaînes, symboles, appels et références | Hopper, Ghidra ou IDA ; [analyse native](https://rea.tools/fr/guides/native/) |
+| Structure ELF hors ligne | Sections, segments, symboles et relocations d'origine, et protections statiques candidates | pwntools fourni par l'utilisateur sous Linux x64 ; [diagnostics binaires](../binary-diagnostics.md) |
+| Artefact WASM | Validation, sections, formes WAT exactes d'import/export et WAT décodé | WABT 1.0.42 fourni par l'utilisateur ; `inspect_wasm_artifact` |
+| Bytecode EVM | Sélecteurs de dispatch, offsets en octets, arguments inférés et mutabilité | Fichier local brut ou hexadécimal sous Linux x64 ; [guide EVM hors ligne](../evm-bytecode.md) |
+| Crashs Linux enregistrés | Notes brutes, registres et signaux de chaque thread enregistré, et correspondances de mappings facultatives | pwntools fourni par l'utilisateur sous Linux x64 ; GDB/pwndbg en option ; [crashs enregistrés](../recorded-crashes.md) |
+| JavaScript / Electron | Modules, imports, source maps, routes, IPC et relations avec les modules natifs | Node.js et npm ; [analyse d'applications](https://rea.tools/fr/guides/javascript/) |
+| Sites web | Structure des pages, scripts, observations réseau et captures d'écran demandées | Un navigateur de la famille Chrome ; [analyse du navigateur](https://rea.tools/fr/guides/browser/) |
+| Captures réseau enregistrées | Requêtes, réponses, payloads exposés et emplacements dans le code source | HAR ; mitmdump sous Linux pour les captures mitmproxy natives ; [guide des captures](../web-network-captures.md) |
+| Captures de paquets | Points de terminaison, DNS, TLS SNI, distribution des protocoles et requêtes HTTP | tshark et capinfos fournis par l'appelant ; [guide pcap](../pcap-analysis.md) |
+| Assemblys .NET | Métadonnées, instructions CIL, dépendances natives déclarées et comparaisons de builds | Inspection statique ; [guide du code managé](../managed-code-analysis.md) |
+| APK Android | Déclarations du manifeste, classes, méthodes décompilées et références | JADX headless et un JDK complet sous Linux/macOS/Windows x64 ; [guide Android](../android-analysis.md) |
+| Appareils Android | Appareils, paquets, processus, journaux, dumps, captures d'écran et transferts d'APK/fichiers | adb fourni par l'appelant (émulateur ou appareil) ; [guide ADB](../adb-device-analysis.md) |
+| Projets JEB | Unités de projet fournies par le moteur, condensés d'artefacts et pseudo-code de types/méthodes | Un client JEB démarré par l'appelant servant MCP, sélectionné via REA_JEB_MCP_URL ; [guide JEB](../jeb-analysis.md) |
+| Ressources Android | Manifeste décodé, tables de chaînes, langues et faits de version | Lanceur Apktool fourni par l'appelant ; [guide des ressources](../apktool-resource-analysis.md) |
+| Applications Flutter | Empreinte de snapshot Dart, condensés de bibliothèques par ABI et provenance de build du moteur | Aucune (analyse APK statique) ; [guide Flutter](../flutter-analysis.md) |
+| Firmware | Régions, résultats d'extraction et passage de relais vers l'analyse native | Binwalk / Unblob sous Linux ; [guide firmware](../firmware-analysis.md) |
+| Paquets enregistrés | Champs sélectionnés, valeurs répétées, filtres et règles de décodage | Linux: TShark 4.x + prlimit; [guide des captures](../packet-captures.md) |
+| Signaux logiques enregistrés | Annotations du décodeur, plages d’échantillons et diagnostics | Linux: sigrok-cli 0.7.2 + prlimit; [guide des signaux](../signal-recordings.md) |
+| Paquets et ressources | Inventaires de fichiers, empreintes, plists, anatomie des bundles Apple et ressources extraites | [Guide artefacts et JavaScript](../javascript-artifact-reconstruction.md), [applications Apple](../apple-application-analysis.md) |
+| Comportement des processus | Sortie du terminal, interactions, code de sortie, observations du système de fichiers et comparaisons | Linux/macOS avec un PTY natif ; [capture de processus](../process-capture.md) |
L'inspection statique JavaScript et .NET lit les fichiers fournis sans exécuter l'application. La capture à l'exécution lance la cible choisie ou interagit avec elle avec vos permissions utilisateur ; chaque guide d'exécution décrit ses effets.
-Les formats natifs et les hôtes pris en charge varient selon le fournisseur. Consultez [la configuration de Hopper et Ghidra](docs/installation.md#hopper), le [guide IDA](docs/ida-provider.md) et [la prise en charge expérimentale de Ghidra sous Windows](docs/windows-ghidra-p0.md). Ghidra prend aussi en charge [l'analyse DOS 16 bits](docs/ghidra-dos.md). Pour les gros binaires, augmentez son délai de démarrage avec `REA_GHIDRA_STARTUP_TIMEOUT_MS`. Pour le choix du fournisseur, consultez le [guide CLI](docs/cli.md#choose-a-provider). Vérifiez la [disponibilité dans les versions publiées](docs/installation.md#released-package-and-main) pour les fonctionnalités ajoutées depuis la dernière version npm.
+Les formats natifs et les hôtes pris en charge varient selon le fournisseur. Consultez [la configuration de Hopper et Ghidra](../installation.md#hopper), le [guide IDA](../ida-provider.md) et [la prise en charge expérimentale de Ghidra sous Windows](../windows-ghidra-p0.md). Ghidra prend aussi en charge [l'analyse DOS 16 bits](../ghidra-dos.md). Pour les gros binaires, augmentez son délai de démarrage avec `REA_GHIDRA_STARTUP_TIMEOUT_MS`. Pour le choix du fournisseur, consultez le [guide CLI](../cli.md#choose-a-provider). Vérifiez la [disponibilité dans les versions publiées](../installation.md#released-package-and-main) pour les fonctionnalités ajoutées depuis la dernière version npm.
## Études de cas
-[](https://rea.tools/fr/showcase/)
+[](https://rea.tools/fr/showcase/)
### DX-Ball : reconstruire un calcul de panoramique sonore
@@ -191,28 +191,28 @@ Si vous avez utilisé REA sur un projet intéressant, nous serions ravis de le d
Quels agents peuvent utiliser REA ?
-Tout agent compatible avec les serveurs MCP locaux. Setup configure les [agents pris en charge](docs/installation.md#supported-agents) ; les autres clients peuvent utiliser [l'enregistrement MCP manuel](docs/installation.md#mcp-registry).
+Tout agent compatible avec les serveurs MCP locaux. Setup configure les [agents pris en charge](../installation.md#supported-agents) ; les autres clients peuvent utiliser [l'enregistrement MCP manuel](../installation.md#mcp-registry).
Ai-je besoin de Hopper, Ghidra ou IDA ?
-L'analyse native approfondie utilise l'un d'eux. L'inspection statique JavaScript et .NET fonctionne sans moteur d'analyse native. Setup peut installer Hopper après votre accord ; Ghidra et IDA utilisent vos installations existantes. Consultez [la configuration des fournisseurs](docs/installation.md#hopper).
+L'analyse native approfondie utilise l'un d'eux. L'inspection statique JavaScript et .NET fonctionne sans moteur d'analyse native. Setup peut installer Hopper après votre accord ; Ghidra et IDA utilisent vos installations existantes. Consultez [la configuration des fournisseurs](../installation.md#hopper).
Dois-je lancer Hopper au préalable ?
-REA lance Hopper lorsqu'une opération en a besoin. Sous macOS, une boîte de dialogue de premier lancement peut vous demander de choisir le mode démo ou d'activer votre licence. Consultez [Démarrage et dépannage de Hopper](docs/installation.md#launcher-paths-and-troubleshooting).
+REA lance Hopper lorsqu'une opération en a besoin. Sous macOS, une boîte de dialogue de premier lancement peut vous demander de choisir le mode démo ou d'activer votre licence. Consultez [Démarrage et dépannage de Hopper](../installation.md#launcher-paths-and-troubleshooting).
Que fait l'installation du skill depuis skills.sh ?
-Le skill fournit des instructions d'investigation à votre agent. Utilisez `npx rea-agents setup` pour enregistrer le serveur MCP de REA et installer les instructions correspondantes, puis redémarrez votre agent. Consultez [l'installation du skill seul](docs/installation.md#skill-only-installation).
+Le skill fournit des instructions d'investigation à votre agent. Utilisez `npx rea-agents setup` pour enregistrer le serveur MCP de REA et installer les instructions correspondantes, puis redémarrez votre agent. Consultez [l'installation du skill seul](../installation.md#skill-only-installation).
@@ -255,14 +255,14 @@ Si vous utilisez un agent, terminez l'[actualisation de la configuration](#mettr
Commencez par les [guides pas à pas](https://rea.tools/fr/guides/) du site web. Pour les options exactes, les prérequis et les contrats de résultats :
-- [Installation et configuration](docs/installation.md) : enregistrement des agents, configuration des fournisseurs, mises à jour et désinstallation.
-- [Vérification et dépannage](docs/installation.md#check-readiness-for-your-task) : diagnostiquer un agent ou un moteur d'analyse donné.
-- [CLI et Evidence](docs/cli.md) : commandes, choix du fournisseur, snapshots, import/export et codes de sortie.
-- [Contrats MCP](docs/mcp-contracts.md) et [prompts pour agents](docs/mcp-prompts.md) : résultats des outils, sessions et investigations guidées.
-- [Catalogue des outils](docs/mcp-contracts.md#generated-catalog) : inventaire des outils, fournisseurs et commandes CLI, généré lors du build.
-- [Feuille de route](docs/roadmap.md) : travaux prévus et suivi des capacités.
+- [Installation et configuration](../installation.md) : enregistrement des agents, configuration des fournisseurs, mises à jour et désinstallation.
+- [Vérification et dépannage](../installation.md#check-readiness-for-your-task) : diagnostiquer un agent ou un moteur d'analyse donné.
+- [CLI et Evidence](../cli.md) : commandes, choix du fournisseur, snapshots, import/export et codes de sortie.
+- [Contrats MCP](../mcp-contracts.md) et [prompts pour agents](../mcp-prompts.md) : résultats des outils, sessions et investigations guidées.
+- [Catalogue des outils](../mcp-contracts.md#generated-catalog) : inventaire des outils, fournisseurs et commandes CLI, généré lors du build.
+- [Feuille de route](../roadmap.md) : travaux prévus et suivi des capacités.
-Signalez les vulnérabilités via [SECURITY.md](SECURITY.md).
+Signalez les vulnérabilités via [SECURITY.md](../../SECURITY.md).
## Historique des étoiles
@@ -280,7 +280,7 @@ Merci à toutes les personnes qui utilisent REA, signalent des bugs, proposent d
## Avertissement
-Consultez [DISCLAIMER.md](DISCLAIMER.md) pour les consignes d'utilisation responsable.
+Consultez [DISCLAIMER.md](../../DISCLAIMER.md) pour les consignes d'utilisation responsable.
REA est destiné à la recherche, à l’éducation, à l’interopérabilité, à la préservation logicielle et à l’analyse de logiciels que vous possédez ou êtes autorisé à examiner, dans le respect de la loi.
@@ -292,14 +292,14 @@ applicable licenses.
Votre aide sur REA est la bienvenue ! [Ouvrez une issue](https://github.com/morluto/rea/issues) pour signaler un bug ou proposer une fonctionnalité, ou [envoyez une pull request](https://github.com/morluto/rea/pulls) pour améliorer le code ou la documentation.
-Consultez [CONTRIBUTING.md](CONTRIBUTING.md) pour l'environnement de développement et les vérifications, la page [tests](docs/testing.md) pour les procédures de vérification, et la [carte de l'architecture](docs/architecture.mermaid) pour la structure du projet.
+Consultez [CONTRIBUTING.md](../../CONTRIBUTING.md) pour l'environnement de développement et les vérifications, la page [tests](../testing.md) pour les procédures de vérification, et la [carte de l'architecture](../architecture.mermaid) pour la structure du projet.
## Licence
-[MIT](LICENSE)
+[MIT](../../LICENSE)
REA-authored code is licensed under MIT. Third-party material retains its own
-licenses and copyright notices; see [Third-party notices](THIRD_PARTY_NOTICES.md)
+licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
for the dependency inventory, preserved license texts, and external-tool terms.
**You must legally obtain and be authorized to use commercial tools**, including
diff --git a/README_id.md b/docs/readme/README_id.md
similarity index 75%
rename from README_id.md
rename to docs/readme/README_id.md
index 7a564c434..fba947fb2 100644
--- a/README_id.md
+++ b/docs/readme/README_id.md
@@ -1,6 +1,6 @@
-[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · **Bahasa Indonesia** · [Italiano](README_it.md)
+[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · **Bahasa Indonesia** · [Italiano](README_it.md)
# REA: Rekayasa Balik Apa Pun
@@ -9,10 +9,10 @@
**Melihat fitur yang Anda sukai. Pahami cara kerjanya hingga ke tingkat biner.**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#current-status)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
@@ -25,7 +25,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
Bergabunglah dengan Komunitas Rekayasa Balik
Discord · Tanya Jawab · Berbagi Hasil
@@ -67,7 +67,7 @@ npx rea-agents setup
Pilih agen yang ingin digunakan, tinjau perubahan yang diusulkan, lalu setujui perubahan tersebut. Proses penyiapan menambahkan server MCP REA dan instruksi alur kerja yang sesuai, serta membuat cadangan konfigurasi yang sudah ada. Setelah itu, mulai ulang agen Anda.
-Penyiapan mendukung Claude Code, Codex, Cursor, Gemini CLI, Grok Build, dan [agen lainnya](docs/installation.md#supported-agents). Lihat [instalasi dan penyiapan](docs/installation.md) untuk konfigurasi penyedia dan pendaftaran MCP secara manual.
+Penyiapan mendukung Claude Code, Codex, Cursor, Gemini CLI, Grok Build, dan [agen lainnya](../installation.md#supported-agents). Lihat [instalasi dan penyiapan](../installation.md) untuk konfigurasi penyedia dan pendaftaran MCP secara manual.
### Minta agen Anda
@@ -94,7 +94,7 @@ npm install --global rea-agents
rea --help
```
-Untuk analisis native, konfigurasikan penyedia terlebih dahulu. Lihat [Panduan CLI dan Bukti](docs/cli.md) untuk perintah native, pemilihan penyedia, snapshot, dan pembuatan skrip.
+Untuk analisis native, konfigurasikan penyedia terlebih dahulu. Lihat [Panduan CLI dan Bukti](../cli.md) untuk perintah native, pemilihan penyedia, snapshot, dan pembuatan skrip.
### Perbarui REA
@@ -124,9 +124,9 @@ Agen Anda memanggil REA melalui MCP untuk memeriksa target dan menelusuri kode y
Perintah CLI menggunakan alur kerja yang sama.
-
+
-[Buka ilustrasi dalam ukuran penuh](website/public/assets/figures/rea-investigation-flow.svg).
+[Buka ilustrasi dalam ukuran penuh](../../website/public/assets/figures/rea-investigation-flow.svg).
@@ -136,46 +136,46 @@ REA memerlukan Node.js 22.x (>=22.19), 24.x (>=24.11), atau 26+, beserta npm.
Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis:
-| Target | Hasil yang diberikan REA | Persyaratan dan panduan |
-| -------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
-| Biner native | Pseudocode, assembly, string, simbol, pemanggilan, dan referensi | Hopper, Ghidra, atau IDA; [analisis native](https://rea.tools/id/guides/native/) |
-| Tata letak ELF offline | Bagian, segmen, simbol/relokasi asli, dan kandidat mitigasi statis | pwntools yang disediakan pemanggil pada Linux x64; [diagnostik biner](docs/binary-diagnostics.md) |
-| Artefak WASM | Validasi, bagian, bentuk WAT import/ekspor yang tepat, dan WAT yang didekode | WABT 1.0.42 yang disediakan pemanggil; `inspect_wasm_artifact` |
-| Bytecode EVM | Selector dispatch, offset byte, argumen yang disimpulkan, dan sifat mutabilitas | Pembawa data raw/hex lokal; [panduan EVM offline](docs/evm-bytecode.md) |
-| Crash Linux yang direkam | Catatan mentah, register/sinyal setiap thread yang direkam, dan kandidat pemetaan opsional | pwntools yang disediakan pemanggil; GDB/pwndbg opsional; [crash yang direkam](docs/recorded-crashes.md) |
-| JavaScript / Electron | Modul, impor, source map, rute, IPC, dan hubungan add-on native | Node.js dan npm; [analisis aplikasi](https://rea.tools/id/guides/javascript/) |
-| Situs web | Struktur halaman, skrip, pengamatan jaringan, dan tangkapan layar yang diminta | Peramban berbasis Chrome; [analisis peramban](https://rea.tools/id/guides/browser/) |
-| Rekaman jaringan tersimpan | Permintaan, respons, payload yang terekspos, dan lokasi sumber | HAR; mitmdump di Linux untuk rekaman native mitmproxy; [panduan rekaman](docs/web-network-captures.md) |
-| Capturan paket | Endpoint, DNS, TLS SNI, distribusi protokol, dan permintaan HTTP | tshark dan capinfos buatan pemanggil; [panduan pcap](docs/pcap-analysis.md) |
-| Assembly .NET | Metadata, instruksi CIL, dependensi native yang dideklarasikan, dan perbandingan build | Tidak ada (hanya analisis statis); [panduan kode terkelola](docs/managed-code-analysis.md) |
-| APK Android | Deklarasi manifest, kelas, metode hasil dekompilasi, dan referensi | JADX mode headless dan JDK lengkap di Linux/macOS/Windows x64; [panduan Android](docs/android-analysis.md) |
-| Perangkat Android | Perangkat, paket, proses, log, dump, tangkapan layar, serta transfer APK/berkas | adb buatan pemanggil (emulator atau perangkat); [panduan ADB](docs/adb-device-analysis.md) |
-| Proyek JEB | Unit proyek bertenaga mesin, hash artefak, dan pseudokode tipe/metode | Klien JEB yang dijalankan pemanggil dan menyajikan MCP, dipilih dengan REA_JEB_MCP_URL; [panduan JEB](docs/jeb-analysis.md) |
-| Resource Android | Manifest terdekode, tabel string, lokal, dan fakta versi | Peluncur Apktool buatan pemanggil; [panduan resource](docs/apktool-resource-analysis.md) |
-| Aplikasi Flutter | Hash snapshot Dart, hash pustaka per ABI, dan asal build engine | Tidak ada (parsing APK statis); [panduan Flutter](docs/flutter-analysis.md) |
-| Firmware | Wilayah, hasil ekstraksi, dan pengalihan ke analisis native | Binwalk / Unblob di Linux; [panduan firmware](docs/firmware-analysis.md) |
-| Paket rekaman | Kolom terpilih, nilai berulang, filter, dan aturan decode-as | Linux: TShark 4.x + prlimit; [panduan tangkapan](docs/packet-captures.md) |
-| Sinyal logika terekam | Anotasi dekoder, rentang sampel, dan diagnostik | Linux: sigrok-cli 0.7.2 + prlimit; [panduan sinyal](docs/signal-recordings.md) |
-| Paket dan sumber daya | Inventaris berkas, digest, plist, struktur paket Apple, dan sumber daya yang diekstrak | [Panduan artefak dan JavaScript](docs/javascript-artifact-reconstruction.md), [aplikasi Apple](docs/apple-application-analysis.md) |
-| Perilaku proses | Keluaran terminal, interaksi, pengamatan proses keluar dan sistem berkas, serta perbandingan eksekusi | Linux/macOS dengan PTY native; [perekaman proses](docs/process-capture.md) |
+| Target | Hasil yang diberikan REA | Persyaratan dan panduan |
+| -------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
+| Biner native | Pseudocode, assembly, string, simbol, pemanggilan, dan referensi | Hopper, Ghidra, atau IDA; [analisis native](https://rea.tools/id/guides/native/) |
+| Tata letak ELF offline | Bagian, segmen, simbol/relokasi asli, dan kandidat mitigasi statis | pwntools yang disediakan pemanggil pada Linux x64; [diagnostik biner](../binary-diagnostics.md) |
+| Artefak WASM | Validasi, bagian, bentuk WAT import/ekspor yang tepat, dan WAT yang didekode | WABT 1.0.42 yang disediakan pemanggil; `inspect_wasm_artifact` |
+| Bytecode EVM | Selector dispatch, offset byte, argumen yang disimpulkan, dan sifat mutabilitas | Pembawa data raw/hex lokal; [panduan EVM offline](../evm-bytecode.md) |
+| Crash Linux yang direkam | Catatan mentah, register/sinyal setiap thread yang direkam, dan kandidat pemetaan opsional | pwntools yang disediakan pemanggil; GDB/pwndbg opsional; [crash yang direkam](../recorded-crashes.md) |
+| JavaScript / Electron | Modul, impor, source map, rute, IPC, dan hubungan add-on native | Node.js dan npm; [analisis aplikasi](https://rea.tools/id/guides/javascript/) |
+| Situs web | Struktur halaman, skrip, pengamatan jaringan, dan tangkapan layar yang diminta | Peramban berbasis Chrome; [analisis peramban](https://rea.tools/id/guides/browser/) |
+| Rekaman jaringan tersimpan | Permintaan, respons, payload yang terekspos, dan lokasi sumber | HAR; mitmdump di Linux untuk rekaman native mitmproxy; [panduan rekaman](../web-network-captures.md) |
+| Capturan paket | Endpoint, DNS, TLS SNI, distribusi protokol, dan permintaan HTTP | tshark dan capinfos buatan pemanggil; [panduan pcap](../pcap-analysis.md) |
+| Assembly .NET | Metadata, instruksi CIL, dependensi native yang dideklarasikan, dan perbandingan build | Tidak ada (hanya analisis statis); [panduan kode terkelola](../managed-code-analysis.md) |
+| APK Android | Deklarasi manifest, kelas, metode hasil dekompilasi, dan referensi | JADX mode headless dan JDK lengkap di Linux/macOS/Windows x64; [panduan Android](../android-analysis.md) |
+| Perangkat Android | Perangkat, paket, proses, log, dump, tangkapan layar, serta transfer APK/berkas | adb buatan pemanggil (emulator atau perangkat); [panduan ADB](../adb-device-analysis.md) |
+| Proyek JEB | Unit proyek bertenaga mesin, hash artefak, dan pseudokode tipe/metode | Klien JEB yang dijalankan pemanggil dan menyajikan MCP, dipilih dengan REA_JEB_MCP_URL; [panduan JEB](../jeb-analysis.md) |
+| Resource Android | Manifest terdekode, tabel string, lokal, dan fakta versi | Peluncur Apktool buatan pemanggil; [panduan resource](../apktool-resource-analysis.md) |
+| Aplikasi Flutter | Hash snapshot Dart, hash pustaka per ABI, dan asal build engine | Tidak ada (parsing APK statis); [panduan Flutter](../flutter-analysis.md) |
+| Firmware | Wilayah, hasil ekstraksi, dan pengalihan ke analisis native | Binwalk / Unblob di Linux; [panduan firmware](../firmware-analysis.md) |
+| Paket rekaman | Kolom terpilih, nilai berulang, filter, dan aturan decode-as | Linux: TShark 4.x + prlimit; [panduan tangkapan](../packet-captures.md) |
+| Sinyal logika terekam | Anotasi dekoder, rentang sampel, dan diagnostik | Linux: sigrok-cli 0.7.2 + prlimit; [panduan sinyal](../signal-recordings.md) |
+| Paket dan sumber daya | Inventaris berkas, digest, plist, struktur paket Apple, dan sumber daya yang diekstrak | [Panduan artefak dan JavaScript](../javascript-artifact-reconstruction.md), [aplikasi Apple](../apple-application-analysis.md) |
+| Perilaku proses | Keluaran terminal, interaksi, pengamatan proses keluar dan sistem berkas, serta perbandingan eksekusi | Linux/macOS dengan PTY native; [perekaman proses](../process-capture.md) |
Pemeriksaan JavaScript dan .NET statis membaca berkas yang disediakan tanpa menjalankan aplikasinya. Perekaman runtime menjalankan atau berinteraksi dengan target yang dipilih menggunakan izin pengguna Anda. Setiap panduan runtime menjelaskan dampaknya.
-Format native dan dukungan sistem host berbeda-beda menurut penyedia. Lihat [penyiapan Hopper dan Ghidra](docs/installation.md#hopper), [panduan IDA](docs/ida-provider.md), dan [dukungan eksperimental Ghidra di Windows](docs/windows-ghidra-p0.md).
+Format native dan dukungan sistem host berbeda-beda menurut penyedia. Lihat [penyiapan Hopper dan Ghidra](../installation.md#hopper), [panduan IDA](../ida-provider.md), dan [dukungan eksperimental Ghidra di Windows](../windows-ghidra-p0.md).
-Ghidra juga mendukung [analisis DOS 16-bit](docs/ghidra-dos.md).
+Ghidra juga mendukung [analisis DOS 16-bit](../ghidra-dos.md).
Untuk biner berukuran besar, tingkatkan batas waktu startup menggunakan `REA_GHIDRA_STARTUP_TIMEOUT_MS`.
-Untuk memilih penyedia, lihat [panduan CLI](docs/cli.md#choose-a-provider).
+Untuk memilih penyedia, lihat [panduan CLI](../cli.md#choose-a-provider).
-Periksa [ketersediaan rilis](docs/installation.md#released-package-and-main) untuk mengetahui fitur yang ditambahkan setelah rilis npm terbaru.
+Periksa [ketersediaan rilis](../installation.md#released-package-and-main) untuk mengetahui fitur yang ditambahkan setelah rilis npm terbaru.
## Contoh Penggunaan
-[](https://rea.tools/id/showcase/)
+[](https://rea.tools/id/showcase/)
### DX-Ball: merekonstruksi perhitungan sound-pan
@@ -202,7 +202,7 @@ Jika Anda telah menggunakan REA untuk menganalisis sesuatu yang menarik, kami in
Agen apa saja yang dapat menggunakan REA?
-Agen apa pun yang mendukung server MCP lokal dapat menggunakan REA. Proses penyiapan mengonfigurasi [agen yang didukung](docs/installation.md#supported-agents). Klien lain dapat menggunakan [pendaftaran MCP manual](docs/installation.md#mcp-registry).
+Agen apa pun yang mendukung server MCP lokal dapat menggunakan REA. Proses penyiapan mengonfigurasi [agen yang didukung](../installation.md#supported-agents). Klien lain dapat menggunakan [pendaftaran MCP manual](../installation.md#mcp-registry).
@@ -211,7 +211,7 @@ Agen apa pun yang mendukung server MCP lokal dapat menggunakan REA. Proses penyi
Analisis native mendalam menggunakan salah satu dari ketiganya. Pemeriksaan JavaScript dan .NET statis dapat dilakukan tanpa mesin analisis native.
-Proses penyiapan dapat memasang Hopper setelah memperoleh persetujuan. Ghidra dan IDA menggunakan instalasi yang sudah tersedia. Lihat [penyiapan penyedia](docs/installation.md#hopper).
+Proses penyiapan dapat memasang Hopper setelah memperoleh persetujuan. Ghidra dan IDA menggunakan instalasi yang sudah tersedia. Lihat [penyiapan penyedia](../installation.md#hopper).
@@ -220,7 +220,7 @@ Proses penyiapan dapat memasang Hopper setelah memperoleh persetujuan. Ghidra da
REA akan menjalankan Hopper saat suatu operasi membutuhkannya. Di macOS, dialog saat pertama kali dijalankan mungkin meminta Anda memilih mode demo atau mengaktifkan lisensi.
-Lihat [startup dan pemecahan masalah Hopper](docs/installation.md#launcher-paths-and-troubleshooting).
+Lihat [startup dan pemecahan masalah Hopper](../installation.md#launcher-paths-and-troubleshooting).
@@ -229,7 +229,7 @@ Lihat [startup dan pemecahan masalah Hopper](docs/installation.md#launcher-paths
Skill tersebut menyediakan instruksi investigasi untuk agen Anda. Gunakan `npx rea-agents setup` untuk mendaftarkan server MCP REA dan memasang instruksi yang sesuai, lalu mulai ulang agen Anda.
-Lihat [instalasi khusus skill](docs/installation.md#skill-only-installation).
+Lihat [instalasi khusus skill](../installation.md#skill-only-installation).
@@ -276,14 +276,14 @@ Mulailah dengan [panduan praktik di situs web](https://rea.tools/id/guides/).
Untuk mengetahui opsi, prasyarat, dan kontrak hasil secara tepat:
-- [Instalasi dan penyiapan](docs/installation.md): pendaftaran agen, konfigurasi penyedia, pembaruan, dan penghapusan instalasi.
-- [Kesiapan dan pemecahan masalah](docs/installation.md#check-readiness-for-your-task): mendiagnosis satu agen atau mesin analisis.
-- [CLI dan Bukti](docs/cli.md): perintah, pemilihan penyedia, snapshot, impor/ekspor, dan status keluar.
-- [Kontrak MCP](docs/mcp-contracts.md) dan [prompt agen](docs/mcp-prompts.md): hasil alat, sesi, dan investigasi terpandu.
-- [Katalog alat](docs/mcp-contracts.md#generated-catalog): inventaris alat, penyedia, dan perintah CLI yang dibuat secara otomatis.
-- [Peta jalan](docs/roadmap.md): pekerjaan yang direncanakan dan pelacak kemampuan.
+- [Instalasi dan penyiapan](../installation.md): pendaftaran agen, konfigurasi penyedia, pembaruan, dan penghapusan instalasi.
+- [Kesiapan dan pemecahan masalah](../installation.md#check-readiness-for-your-task): mendiagnosis satu agen atau mesin analisis.
+- [CLI dan Bukti](../cli.md): perintah, pemilihan penyedia, snapshot, impor/ekspor, dan status keluar.
+- [Kontrak MCP](../mcp-contracts.md) dan [prompt agen](../mcp-prompts.md): hasil alat, sesi, dan investigasi terpandu.
+- [Katalog alat](../mcp-contracts.md#generated-catalog): inventaris alat, penyedia, dan perintah CLI yang dibuat secara otomatis.
+- [Peta jalan](../roadmap.md): pekerjaan yang direncanakan dan pelacak kemampuan.
-Laporkan kerentanan melalui [SECURITY.md](SECURITY.md).
+Laporkan kerentanan melalui [SECURITY.md](../../SECURITY.md).
## Riwayat Bintang
@@ -301,7 +301,7 @@ Terima kasih kepada semua orang yang menggunakan REA, melaporkan bug, meminta fi
## Penafian
-Lihat [DISCLAIMER.md](DISCLAIMER.md) untuk panduan penggunaan yang bertanggung jawab.
+Lihat [DISCLAIMER.md](../../DISCLAIMER.md) untuk panduan penggunaan yang bertanggung jawab.
REA ditujukan untuk penelitian, pendidikan, interoperabilitas, pelestarian perangkat lunak, dan analisis perangkat lunak yang Anda miliki atau diizinkan untuk Anda periksa, sesuai hukum.
@@ -313,14 +313,14 @@ applicable licenses.
Kami sangat menghargai bantuan Anda untuk REA! [Buat issue](https://github.com/morluto/rea/issues) untuk melaporkan bug atau menyarankan fitur, atau [kirim pull request](https://github.com/morluto/rea/pulls) untuk meningkatkan kode atau dokumentasi.
-Lihat [CONTRIBUTING.md](CONTRIBUTING.md) untuk penyiapan pengembangan dan pemeriksaan, [pengujian](docs/testing.md) untuk tahapan verifikasi, serta [peta arsitektur](docs/architecture.mermaid) untuk struktur proyek.
+Lihat [CONTRIBUTING.md](../../CONTRIBUTING.md) untuk penyiapan pengembangan dan pemeriksaan, [pengujian](../testing.md) untuk tahapan verifikasi, serta [peta arsitektur](../architecture.mermaid) untuk struktur proyek.
## Lisensi
-[MIT](LICENSE)
+[MIT](../../LICENSE)
REA-authored code is licensed under MIT. Third-party material retains its own
-licenses and copyright notices; see [Third-party notices](THIRD_PARTY_NOTICES.md)
+licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
for the dependency inventory, preserved license texts, and external-tool terms.
**You must legally obtain and be authorized to use commercial tools**, including
diff --git a/README_it.md b/docs/readme/README_it.md
similarity index 74%
rename from README_it.md
rename to docs/readme/README_it.md
index 8b1c55afe..b7e7c6100 100644
--- a/README_it.md
+++ b/docs/readme/README_it.md
@@ -1,6 +1,6 @@
-[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · **Italiano**
+[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · **Italiano**
# REA: Reverse Engineer Anything
@@ -9,10 +9,10 @@
**Vedi una funzionalità che ti piace. Capisci come funziona, fino al livello binario.**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#cosa-puoi-analizzare)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
@@ -25,7 +25,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
Unisciti alla community di ingegneria inversa
Discord · Domande e risposte · Show and Tell
@@ -67,7 +67,7 @@ npx rea-agents setup
Scegli i tuoi agenti, rivedi le modifiche proposte e approvale. La configurazione aggiunge il server MCP di REA e le istruzioni di workflow corrispondenti, con backup della configurazione esistente. Riavvia quindi il tuo agente.
-La configurazione supporta Claude Code, Codex, Cursor, Gemini CLI, Grok Build e [altri agenti](docs/installation.md#supported-agents). Vedi [installazione e configurazione](docs/installation.md) per la configurazione dei provider e la registrazione manuale MCP.
+La configurazione supporta Claude Code, Codex, Cursor, Gemini CLI, Grok Build e [altri agenti](../installation.md#supported-agents). Vedi [installazione e configurazione](../installation.md) per la configurazione dei provider e la registrazione manuale MCP.
### Chiedi al tuo agente
@@ -83,7 +83,7 @@ Sostituisci Note con la tua app di destinazione e la funzionalità che vuoi capi
Ispeziona una directory di app JavaScript/Electron estratta o un ASAR:
```bash
-npx -y rea-agents@latest analyze-javascript-application /percorso/assoluto/verso/app --json
+npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
Il risultato include moduli, import, confini Electron e le relative prove. Sostituisci il percorso con il tuo bersaglio, ad esempio `"D:/apps/example"` su Windows.
@@ -95,7 +95,7 @@ npm install --global rea-agents
rea --help
```
-Per l'analisi nativa, configura prima un provider. Vedi la [guida CLI ed Evidence](docs/cli.md) per i comandi nativi, la selezione del provider, gli snapshot e lo scripting.
+Per l'analisi nativa, configura prima un provider. Vedi la [guida CLI ed Evidence](../cli.md) per i comandi nativi, la selezione del provider, gli snapshot e lo scripting.
### Aggiorna REA
@@ -123,9 +123,9 @@ Rivedi le modifiche di configurazione e riavvia il tuo agente. Per comandi CLI s
Il tuo agente chiama REA tramite MCP per ispezionare il bersaglio e tracciare il codice rilevante. REA restituisce i risultati con le relative prove. L'agente li usa per fare domande di approfondimento, spiegare il comportamento o scrivere e testare un'implementazione. Anche i comandi CLI usano gli stessi workflow.
-
+
-[Apri la figura a dimensione intera](website/public/assets/figures/rea-investigation-flow.svg).
+[Apri la figura a dimensione intera](../../website/public/assets/figures/rea-investigation-flow.svg).
@@ -133,38 +133,38 @@ Il tuo agente chiama REA tramite MCP per ispezionare il bersaglio e tracciare il
REA richiede Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, oltre a npm. Strumenti aggiuntivi e supporto host dipendono dal bersaglio:
-| Obiettivo | Cosa restituisce REA | Requisiti e guida |
-| ---------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
-| Binari nativi | Pseudocodice, assembly, stringhe, simboli, chiamate e riferimenti | Hopper, Ghidra o IDA; [analisi nativa](https://rea.tools/it/guides/native/) |
-| Layout ELF offline | Sezioni, segmenti, simboli/relocazioni originali e candidati mitigazioni statiche | pwntools fornito dal chiamante su Linux x64; [diagnostica binaria](docs/binary-diagnostics.md) |
-| Artefatto WASM | Convalida, sezioni, forme WAT di import/export esatte e WAT decodificato | WABT 1.0.42 fornito dal chiamante; `inspect_wasm_artifact` |
-| Bytecode EVM | Selettori di dispatch, offset dei byte, argomenti inferiti e mutabilità | Carrier raw/hex locale; [guida EVM offline](docs/evm-bytecode.md) |
-| Crash Linux registrati | Note grezze, registri/segnai di ogni thread registrato e candidati mapping opzionali | pwntools fornito dal chiamante; GDB/pwndbg opzionale; [crash registrati](docs/recorded-crashes.md) |
-| JavaScript / Electron | Moduli, import, source map, route, IPC e relazioni con add-on nativi | Node.js e npm; [analisi delle applicazioni](https://rea.tools/it/guides/javascript/) |
-| Siti web | Struttura della pagina, script, osservazioni di rete e screenshot richiesti | Un browser della famiglia Chrome; [analisi browser](https://rea.tools/it/guides/browser/) |
-| Acquisizioni di rete salvate | Richieste, risposte, payload esposti e posizioni sorgente | HAR; mitmdump su Linux per acquisizioni mitmproxy native; [guida alle acquisizioni](docs/web-network-captures.md) |
-| Captured di pacchetti | Endpoint, DNS, TLS SNI, distribuzione dei protocolli e richieste HTTP | tshark e capinfos forniti dal chiamante; [guida pcap](docs/pcap-analysis.md) |
-| Assembly .NET | Metadati, istruzioni CIL, dipendenze native dichiarate e confronti di build | Nessuno (solo statico); [guida managed-code](docs/managed-code-analysis.md) |
-| APK Android | Dichiarazioni manifest, classi, metodi decompilati e riferimenti | JADX headless e un JDK completo su Linux/macOS/Windows x64; [guida Android](docs/android-analysis.md) |
-| Dispositivi Android | Dispositivi, pacchetti, processi, log, dump, screenshot e trasferimenti APK/file | adb fornito dal chiamante (emulatore o hardware); [guida ADB](docs/adb-device-analysis.md) |
-| Progetti JEB | Unità di progetto dal motore, digest degli artifact e pseudocodice di tipi/metodi | Client JEB avviato dal chiamante via MCP, selezionato con REA_JEB_MCP_URL; [guida JEB](docs/jeb-analysis.md) |
-| Risorse Android | Manifest decodificato, tabelle stringhe, lingue e dati di versione | Launcher Apktool fornito dal chiamante; [guida alle risorse](docs/apktool-resource-analysis.md) |
-| App Flutter | Hash dello snapshot Dart, digest delle librerie per ABI e provenienza della build del motore | Nessuno (analisi APK statica); [guida Flutter](docs/flutter-analysis.md) |
-| Firmware | Regioni, risultati di estrazione e passaggi all'analisi nativa | Binwalk / Unblob su Linux; [guida firmware](docs/firmware-analysis.md) |
-| Pacchetti registrati | Campi selezionati, valori ripetuti, filtri e regole di decodifica | Linux: TShark 4.x + prlimit; [guida alle catture](docs/packet-captures.md) |
-| Segnali logici registrati | Annotazioni del decoder, intervalli di campioni e diagnostica | Linux: sigrok-cli 0.7.2 + prlimit; [guida ai segnali](docs/signal-recordings.md) |
-| Pacchetti e risorse | Inventari file, digest, plist, anatomia dei bundle Apple e risorse estratte | [Guida artifact e JavaScript](docs/javascript-artifact-reconstruction.md), [Applicazioni Apple](docs/apple-application-analysis.md) |
-| Comportamento dei processi | Output del terminale, interazioni, osservazioni su uscita e filesystem ed confronti di esecuzione | Linux/macOS con un PTY nativo; [cattura dei processi](docs/process-capture.md) |
+| Obiettivo | Cosa restituisce REA | Requisiti e guida |
+| ---------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
+| Binari nativi | Pseudocodice, assembly, stringhe, simboli, chiamate e riferimenti | Hopper, Ghidra o IDA; [analisi nativa](https://rea.tools/it/guides/native/) |
+| Layout ELF offline | Sezioni, segmenti, simboli/relocazioni originali e candidati mitigazioni statiche | pwntools fornito dal chiamante su Linux x64; [diagnostica binaria](../binary-diagnostics.md) |
+| Artefatto WASM | Convalida, sezioni, forme WAT di import/export esatte e WAT decodificato | WABT 1.0.42 fornito dal chiamante; `inspect_wasm_artifact` |
+| Bytecode EVM | Selettori di dispatch, offset dei byte, argomenti inferiti e mutabilità | Carrier raw/hex locale; [guida EVM offline](../evm-bytecode.md) |
+| Crash Linux registrati | Note grezze, registri/segnai di ogni thread registrato e candidati mapping opzionali | pwntools fornito dal chiamante; GDB/pwndbg opzionale; [crash registrati](../recorded-crashes.md) |
+| JavaScript / Electron | Moduli, import, source map, route, IPC e relazioni con add-on nativi | Node.js e npm; [analisi delle applicazioni](https://rea.tools/it/guides/javascript/) |
+| Siti web | Struttura della pagina, script, osservazioni di rete e screenshot richiesti | Un browser della famiglia Chrome; [analisi browser](https://rea.tools/it/guides/browser/) |
+| Acquisizioni di rete salvate | Richieste, risposte, payload esposti e posizioni sorgente | HAR; mitmdump su Linux per acquisizioni mitmproxy native; [guida alle acquisizioni](../web-network-captures.md) |
+| Captured di pacchetti | Endpoint, DNS, TLS SNI, distribuzione dei protocolli e richieste HTTP | tshark e capinfos forniti dal chiamante; [guida pcap](../pcap-analysis.md) |
+| Assembly .NET | Metadati, istruzioni CIL, dipendenze native dichiarate e confronti di build | Nessuno (solo statico); [guida managed-code](../managed-code-analysis.md) |
+| APK Android | Dichiarazioni manifest, classi, metodi decompilati e riferimenti | JADX headless e un JDK completo su Linux/macOS/Windows x64; [guida Android](../android-analysis.md) |
+| Dispositivi Android | Dispositivi, pacchetti, processi, log, dump, screenshot e trasferimenti APK/file | adb fornito dal chiamante (emulatore o hardware); [guida ADB](../adb-device-analysis.md) |
+| Progetti JEB | Unità di progetto dal motore, digest degli artifact e pseudocodice di tipi/metodi | Client JEB avviato dal chiamante via MCP, selezionato con REA_JEB_MCP_URL; [guida JEB](../jeb-analysis.md) |
+| Risorse Android | Manifest decodificato, tabelle stringhe, lingue e dati di versione | Launcher Apktool fornito dal chiamante; [guida alle risorse](../apktool-resource-analysis.md) |
+| App Flutter | Hash dello snapshot Dart, digest delle librerie per ABI e provenienza della build del motore | Nessuno (analisi APK statica); [guida Flutter](../flutter-analysis.md) |
+| Firmware | Regioni, risultati di estrazione e passaggi all'analisi nativa | Binwalk / Unblob su Linux; [guida firmware](../firmware-analysis.md) |
+| Pacchetti registrati | Campi selezionati, valori ripetuti, filtri e regole di decodifica | Linux: TShark 4.x + prlimit; [guida alle catture](../packet-captures.md) |
+| Segnali logici registrati | Annotazioni del decoder, intervalli di campioni e diagnostica | Linux: sigrok-cli 0.7.2 + prlimit; [guida ai segnali](../signal-recordings.md) |
+| Pacchetti e risorse | Inventari file, digest, plist, anatomia dei bundle Apple e risorse estratte | [Guida artifact e JavaScript](../javascript-artifact-reconstruction.md), [Applicazioni Apple](../apple-application-analysis.md) |
+| Comportamento dei processi | Output del terminale, interazioni, osservazioni su uscita e filesystem ed confronti di esecuzione | Linux/macOS con un PTY nativo; [cattura dei processi](../process-capture.md) |
L'ispezione statica di JavaScript e .NET legge i file forniti senza eseguire l'applicazione. La cattura a runtime esegue o interagisce con il bersaglio selezionato usando i tuoi permessi utente; ogni guida runtime descrive i suoi effetti.
-I formati nativi e il supporto host variano in base al provider. Vedi la [configurazione di Hopper e Ghidra](docs/installation.md#hopper), la [guida IDA](docs/ida-provider.md) e il [supporto sperimentale Windows Ghidra](docs/windows-ghidra-p0.md). Ghidra supporta anche l'[analisi DOS a 16 bit](docs/ghidra-dos.md). Per binari di grandi dimensioni, aumenta il timeout di avvio con `REA_GHIDRA_STARTUP_TIMEOUT_MS`. Per la selezione del provider, vedi la [guida CLI](docs/cli.md#choose-a-provider). Controlla la [disponibilità delle release](docs/installation.md#released-package-and-main) per le funzionalità aggiunte dall'ultima release npm.
+I formati nativi e il supporto host variano in base al provider. Vedi la [configurazione di Hopper e Ghidra](../installation.md#hopper), la [guida IDA](../ida-provider.md) e il [supporto sperimentale Windows Ghidra](../windows-ghidra-p0.md). Ghidra supporta anche l'[analisi DOS a 16 bit](../ghidra-dos.md). Per binari di grandi dimensioni, aumenta il timeout di avvio con `REA_GHIDRA_STARTUP_TIMEOUT_MS`. Per la selezione del provider, vedi la [guida CLI](../cli.md#choose-a-provider). Controlla la [disponibilità delle release](../installation.md#released-package-and-main) per le funzionalità aggiunte dall'ultima release npm.
## Showcase
-[](https://rea.tools/it/showcase/)
+[](https://rea.tools/it/showcase/)
### DX-Ball: ricostruisci un calcolo sound-pan
@@ -191,28 +191,28 @@ Se hai usato REA su qualcosa di interessante, ci piacerebbe vederlo. Condividi i
Quali agenti possono usare REA?
-Qualsiasi agente che supporti server MCP locali. La configurazione configura gli [agenti supportati](docs/installation.md#supported-agents); altri client possono usare la [registrazione MCP manuale](docs/installation.md#mcp-registry).
+Qualsiasi agente che supporti server MCP locali. La configurazione configura gli [agenti supportati](../installation.md#supported-agents); altri client possono usare la [registrazione MCP manuale](../installation.md#mcp-registry).
Ho bisogno di Hopper, Ghidra o IDA?
-L'analisi nativa profonda ne usa uno. L'ispezione statica di JavaScript e .NET funziona senza un motore di analisi nativo. La configurazione può installare Hopper previa approvazione; Ghidra e IDA usano le tue installazioni esistenti. Vedi [configurazione provider](docs/installation.md#hopper).
+L'analisi nativa profonda ne usa uno. L'ispezione statica di JavaScript e .NET funziona senza un motore di analisi nativo. La configurazione può installare Hopper previa approvazione; Ghidra e IDA usano le tue installazioni esistenti. Vedi [configurazione provider](../installation.md#hopper).
Devo avviare Hopper per primo?
-REA avvia Hopper quando un'operazione ne ha bisogno. Su macOS, una finestra di dialogo al primo avvio potrebbe chiederti di scegliere la modalità demo o di attivare la licenza. Vedi [avvio di Hopper e risoluzione dei problemi](docs/installation.md#launcher-paths-and-troubleshooting).
+REA avvia Hopper quando un'operazione ne ha bisogno. Su macOS, una finestra di dialogo al primo avvio potrebbe chiederti di scegliere la modalità demo o di attivare la licenza. Vedi [avvio di Hopper e risoluzione dei problemi](../installation.md#launcher-paths-and-troubleshooting).
Cosa fa l'installazione dello skill da skills.sh?
-Lo skill fornisce istruzioni di indagine per il tuo agente. Usa `npx rea-agents setup` per registrare il server MCP di REA e installare le istruzioni corrispondenti, quindi riavvia il tuo agente. Vedi [installazione solo skill](docs/installation.md#skill-only-installation).
+Lo skill fornisce istruzioni di indagine per il tuo agente. Usa `npx rea-agents setup` per registrare il server MCP di REA e installare le istruzioni corrispondenti, quindi riavvia il tuo agente. Vedi [installazione solo skill](../installation.md#skill-only-installation).
@@ -255,14 +255,14 @@ Se stai usando un agente, completa l'[aggiornamento della configurazione](#aggio
Inizia dalle [guide pratiche](https://rea.tools/it/guides/) del sito web. Per opzioni esatte, prerequisiti e contratti dei risultati:
-- [Installazione e configurazione](docs/installation.md): registrazione agente, configurazione provider, aggiornamenti e disinstallazione.
-- [Pronto all'uso e risoluzione dei problemi](docs/installation.md#check-readiness-for-your-task): diagnostica un agente o un motore di analisi.
-- [CLI ed Evidence](docs/cli.md): comandi, selezione provider, snapshot, import/export e stati di uscita.
-- [Contratti MCP](docs/mcp-contracts.md) e [prompt agente](docs/mcp-prompts.md): risultati degli strumenti, sessioni e indagini guidate.
-- [Catalogo strumenti](docs/mcp-contracts.md#generated-catalog): inventario generato di strumenti, provider e comandi CLI.
-- [Roadmap](docs/roadmap.md): lavoro pianificato e tracker delle capacità.
+- [Installazione e configurazione](../installation.md): registrazione agente, configurazione provider, aggiornamenti e disinstallazione.
+- [Pronto all'uso e risoluzione dei problemi](../installation.md#check-readiness-for-your-task): diagnostica un agente o un motore di analisi.
+- [CLI ed Evidence](../cli.md): comandi, selezione provider, snapshot, import/export e stati di uscita.
+- [Contratti MCP](../mcp-contracts.md) e [prompt agente](../mcp-prompts.md): risultati degli strumenti, sessioni e indagini guidate.
+- [Catalogo strumenti](../mcp-contracts.md#generated-catalog): inventario generato di strumenti, provider e comandi CLI.
+- [Roadmap](../roadmap.md): lavoro pianificato e tracker delle capacità.
-Segnala vulnerabilità tramite [SECURITY.md](SECURITY.md).
+Segnala vulnerabilità tramite [SECURITY.md](../../SECURITY.md).
## Cronologia delle stelle
@@ -280,7 +280,7 @@ Grazie a tutti coloro che usano REA, segnalano bug, propongono funzionalità, te
## Disclaimer
-Consulta [DISCLAIMER.md](DISCLAIMER.md) per le indicazioni sull'uso responsabile.
+Consulta [DISCLAIMER.md](../../DISCLAIMER.md) per le indicazioni sull'uso responsabile.
REA è destinato alla ricerca, alla formazione, all’interoperabilità, alla conservazione del software e all’analisi di software di tua proprietà o che sei autorizzato a esaminare, nel rispetto della legge.
@@ -292,14 +292,14 @@ applicable licenses.
Apprezzeremmo il tuo aiuto con REA! [Apri un issue](https://github.com/morluto/rea/issues) per segnalare un bug o suggerire una funzionalità, oppure [invia una pull request](https://github.com/morluto/rea/pulls) per migliorare il codice o la documentazione.
-Vedi [CONTRIBUTING.md](CONTRIBUTING.md) per la configurazione di sviluppo e i controlli, [testing](docs/testing.md) per le lane di verifica e la [mappa dell'architettura](docs/architecture.mermaid) per la struttura del progetto.
+Vedi [CONTRIBUTING.md](../../CONTRIBUTING.md) per la configurazione di sviluppo e i controlli, [testing](../testing.md) per le lane di verifica e la [mappa dell'architettura](../architecture.mermaid) per la struttura del progetto.
## License
-[MIT](LICENSE)
+[MIT](../../LICENSE)
REA-authored code is licensed under MIT. Third-party material retains its own
-licenses and copyright notices; see [Third-party notices](THIRD_PARTY_NOTICES.md)
+licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
for the dependency inventory, preserved license texts, and external-tool terms.
**You must legally obtain and be authorized to use commercial tools**, including
diff --git a/README_ja.md b/docs/readme/README_ja.md
similarity index 76%
rename from README_ja.md
rename to docs/readme/README_ja.md
index 5fb42d096..c9d7981ba 100644
--- a/README_ja.md
+++ b/docs/readme/README_ja.md
@@ -1,6 +1,6 @@
-[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · **日本語** · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · **日本語** · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA:あらゆるものをリバースエンジニアリング
@@ -9,10 +9,10 @@
**気に入った機能を見つけたら、その仕組みをバイナリのレベルまで理解する。**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#current-status)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
@@ -25,7 +25,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
リバースエンジニアリングのコミュニティに参加
Discord · 質問と回答 · 成果の共有
@@ -67,7 +67,7 @@ npx rea-agents setup
使うエージェントを選び、予定されている変更を確認して承認してください。セットアップは REA の MCP サーバーと対応するワークフローの手順を追加し、既存の設定をバックアップします。完了後にエージェントを再起動してください。
-セットアップは Claude Code、Codex、Cursor、Gemini CLI、Grok Build、および[その他のエージェント](docs/installation.md#supported-agents)に対応しています。プロバイダーの設定と MCP の手動登録については、[インストールとセットアップ](docs/installation.md)を参照してください。
+セットアップは Claude Code、Codex、Cursor、Gemini CLI、Grok Build、および[その他のエージェント](../installation.md#supported-agents)に対応しています。プロバイダーの設定と MCP の手動登録については、[インストールとセットアップ](../installation.md)を参照してください。
### エージェントに依頼する
@@ -94,7 +94,7 @@ npm install --global rea-agents
rea --help
```
-ネイティブ分析には、先にプロバイダーの設定が必要です。ネイティブ分析のコマンド、プロバイダーの選択、スナップショット、スクリプトでの利用は、[CLI と Evidence のガイド](docs/cli.md)を参照してください。
+ネイティブ分析には、先にプロバイダーの設定が必要です。ネイティブ分析のコマンド、プロバイダーの選択、スナップショット、スクリプトでの利用は、[CLI と Evidence のガイド](../cli.md)を参照してください。
### REA を更新する
@@ -120,9 +120,9 @@ npx rea-agents@latest setup
エージェントは MCP 経由で REA を呼び出し、対象を調べて関連するコードを追跡します。REA は調査結果と根拠を返します。エージェントはそれを使って追加の質問をしたり、動作を説明したり、実装を書いてテストしたりします。CLI コマンドも同じワークフローを使います。
-
+
-[原寸大の図を開く](website/public/assets/figures/rea-investigation-flow.svg)。
+[原寸大の図を開く](../../website/public/assets/figures/rea-investigation-flow.svg)。
@@ -130,37 +130,37 @@ npx rea-agents@latest setup
REA には Node.js 22.x(>=22.19)、24.x(>=24.11)、または 26+ と npm が必要です。追加のツールや対応ホストは、対象によって異なります。
-| 対象 | REA が返すもの | 要件とガイド |
-| -------------------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
-| ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](https://rea.tools/ja/guides/native/) |
-| オフライン ELF レイアウト | セクション、セグメント、元のシンボル/再配置情報、静的解析によるセキュリティ対策の候補 | Linux x64 で利用者が用意する pwntools;[バイナリ診断](docs/binary-diagnostics.md) |
-| EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | Linux x64 上のローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](docs/evm-bytecode.md) |
-| 記録済みの Linux クラッシュ | 生の note レコード、記録された全スレッドのレジスター/シグナル、任意のマッピング候補 | Linux x64 で利用者が用意する pwntools;GDB/pwndbg は任意;[記録済みクラッシュ](docs/recorded-crashes.md) |
-| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、IPC、ネイティブアドオンの関係 | Node.js と npm;[アプリケーション分析](https://rea.tools/ja/guides/javascript/) |
-| ウェブサイト | ページ構造、スクリプト、ネットワークの観測結果、依頼されたスクリーンショット | Chrome 系ブラウザー;[ブラウザー分析](https://rea.tools/ja/guides/browser/) |
-| 保存済みのネットワークキャプチャ | リクエスト、レスポンス、取得可能なペイロード、ソース内の位置 | HAR;mitmproxy ネイティブ形式のキャプチャには Linux 上の mitmdump;[キャプチャガイド](docs/web-network-captures.md) |
-| パケットキャプチャ | エンドポイント、DNS、TLS SNI、プロトコル分布、HTTPリクエスト | 呼び出し側が用意した tshark と capinfos;[pcapガイド](docs/pcap-analysis.md) |
-| .NET アセンブリ | メタデータ、CIL 命令、宣言されたネイティブ依存関係、ビルドの比較 | 静的な検査;[マネージドコードのガイド](docs/managed-code-analysis.md) |
-| Android APK | マニフェストの宣言、クラス、逆コンパイルされたメソッド、参照 | Linux/macOS/Windows x64 上のヘッドレス JADX と完全な JDK;[Android ガイド](docs/android-analysis.md) |
-| Android デバイス | デバイス、パッケージ、プロセス、ログ、ダンプ、スクリーンキャプチャ、APK/ファイル転送 | 呼び出し側が用意した adb(エミュレーターまたは実機);[ADB ガイド](docs/adb-device-analysis.md) |
-| JEBプロジェクト | エンジンによるプロジェクトユニット、アーティファクトのダイジェスト、型/メソッドの擬似コード | 呼び出し側が起動し MCP を提供する JEB クライアント(REA_JEB_MCP_URL で選択);[JEB ガイド](docs/jeb-analysis.md) |
-| Android リソース | デコードされたマニフェスト、文字列テーブル、ロケール、バージョン情報 | 呼び出し側が用意した Apktool ランチャー;[リソースガイド](docs/apktool-resource-analysis.md) |
-| Flutter アプリ | Dart スナップショットハッシュ、ABI ごとのライブラリダイジェスト、エンジンビルドの来歴 | 不要(静的 APK 解析);[Flutter ガイド](docs/flutter-analysis.md) |
-| ファームウェア | 領域、抽出結果、ネイティブ分析への引き継ぎ | Linux 上の Binwalk / Unblob;[ファームウェアガイド](docs/firmware-analysis.md) |
-| 記録済みパケット | 選択フィールド、繰り返し値、フィルター、デコード指定 | Linux: TShark 4.x + prlimit; [キャプチャガイド](docs/packet-captures.md) |
-| 記録済みロジック信号 | デコーダ注釈、サンプル範囲、診断情報 | Linux: sigrok-cli 0.7.2 + prlimit; [信号記録ガイド](docs/signal-recordings.md) |
-| パッケージとリソース | ファイル一覧、ダイジェスト、plist、Apple バンドルの構造、抽出されたリソース | [アーティファクトと JavaScript のガイド](docs/javascript-artifact-reconstruction.md)、[Apple アプリケーション](docs/apple-application-analysis.md) |
-| プロセスの動作 | ターミナル出力、対話、終了とファイルシステムの観測結果、実行の比較 | ネイティブ PTY を備えた Linux/macOS;[プロセスキャプチャ](docs/process-capture.md) |
+| 対象 | REA が返すもの | 要件とガイド |
+| -------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
+| ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](https://rea.tools/ja/guides/native/) |
+| オフライン ELF レイアウト | セクション、セグメント、元のシンボル/再配置情報、静的解析によるセキュリティ対策の候補 | Linux x64 で利用者が用意する pwntools;[バイナリ診断](../binary-diagnostics.md) |
+| EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | Linux x64 上のローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](../evm-bytecode.md) |
+| 記録済みの Linux クラッシュ | 生の note レコード、記録された全スレッドのレジスター/シグナル、任意のマッピング候補 | Linux x64 で利用者が用意する pwntools;GDB/pwndbg は任意;[記録済みクラッシュ](../recorded-crashes.md) |
+| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、IPC、ネイティブアドオンの関係 | Node.js と npm;[アプリケーション分析](https://rea.tools/ja/guides/javascript/) |
+| ウェブサイト | ページ構造、スクリプト、ネットワークの観測結果、依頼されたスクリーンショット | Chrome 系ブラウザー;[ブラウザー分析](https://rea.tools/ja/guides/browser/) |
+| 保存済みのネットワークキャプチャ | リクエスト、レスポンス、取得可能なペイロード、ソース内の位置 | HAR;mitmproxy ネイティブ形式のキャプチャには Linux 上の mitmdump;[キャプチャガイド](../web-network-captures.md) |
+| パケットキャプチャ | エンドポイント、DNS、TLS SNI、プロトコル分布、HTTPリクエスト | 呼び出し側が用意した tshark と capinfos;[pcapガイド](../pcap-analysis.md) |
+| .NET アセンブリ | メタデータ、CIL 命令、宣言されたネイティブ依存関係、ビルドの比較 | 静的な検査;[マネージドコードのガイド](../managed-code-analysis.md) |
+| Android APK | マニフェストの宣言、クラス、逆コンパイルされたメソッド、参照 | Linux/macOS/Windows x64 上のヘッドレス JADX と完全な JDK;[Android ガイド](../android-analysis.md) |
+| Android デバイス | デバイス、パッケージ、プロセス、ログ、ダンプ、スクリーンキャプチャ、APK/ファイル転送 | 呼び出し側が用意した adb(エミュレーターまたは実機);[ADB ガイド](../adb-device-analysis.md) |
+| JEBプロジェクト | エンジンによるプロジェクトユニット、アーティファクトのダイジェスト、型/メソッドの擬似コード | 呼び出し側が起動し MCP を提供する JEB クライアント(REA_JEB_MCP_URL で選択);[JEB ガイド](../jeb-analysis.md) |
+| Android リソース | デコードされたマニフェスト、文字列テーブル、ロケール、バージョン情報 | 呼び出し側が用意した Apktool ランチャー;[リソースガイド](../apktool-resource-analysis.md) |
+| Flutter アプリ | Dart スナップショットハッシュ、ABI ごとのライブラリダイジェスト、エンジンビルドの来歴 | 不要(静的 APK 解析);[Flutter ガイド](../flutter-analysis.md) |
+| ファームウェア | 領域、抽出結果、ネイティブ分析への引き継ぎ | Linux 上の Binwalk / Unblob;[ファームウェアガイド](../firmware-analysis.md) |
+| 記録済みパケット | 選択フィールド、繰り返し値、フィルター、デコード指定 | Linux: TShark 4.x + prlimit; [キャプチャガイド](../packet-captures.md) |
+| 記録済みロジック信号 | デコーダ注釈、サンプル範囲、診断情報 | Linux: sigrok-cli 0.7.2 + prlimit; [信号記録ガイド](../signal-recordings.md) |
+| パッケージとリソース | ファイル一覧、ダイジェスト、plist、Apple バンドルの構造、抽出されたリソース | [アーティファクトと JavaScript のガイド](../javascript-artifact-reconstruction.md)、[Apple アプリケーション](../apple-application-analysis.md) |
+| プロセスの動作 | ターミナル出力、対話、終了とファイルシステムの観測結果、実行の比較 | ネイティブ PTY を備えた Linux/macOS;[プロセスキャプチャ](../process-capture.md) |
静的な JavaScript と .NET の検査は、指定されたファイルを読み取り、アプリケーションを実行しません。実行時キャプチャは、あなたのユーザー権限で対象を実行したり、対象とやり取りしたりします。それぞれの実行時ガイドに、どのような操作が行われるかを記載しています。
-対応するネイティブ形式とホストは、プロバイダーによって異なります。[Hopper と Ghidra のセットアップ](docs/installation.md#hopper)、[IDA ガイド](docs/ida-provider.md)、[Windows 向け Ghidra の実験的対応](docs/windows-ghidra-p0.md)を参照してください。Ghidra は [16 ビット DOS 分析](docs/ghidra-dos.md)にも対応しています。プロバイダーの選択は [CLI ガイド](docs/cli.md#choose-a-provider)を参照してください。最新の npm リリース以降に追加された機能については、[リリースでの利用可否](docs/installation.md#released-package-and-main)を確認してください。
+対応するネイティブ形式とホストは、プロバイダーによって異なります。[Hopper と Ghidra のセットアップ](../installation.md#hopper)、[IDA ガイド](../ida-provider.md)、[Windows 向け Ghidra の実験的対応](../windows-ghidra-p0.md)を参照してください。Ghidra は [16 ビット DOS 分析](../ghidra-dos.md)にも対応しています。プロバイダーの選択は [CLI ガイド](../cli.md#choose-a-provider)を参照してください。最新の npm リリース以降に追加された機能については、[リリースでの利用可否](../installation.md#released-package-and-main)を確認してください。
## 事例
-[](https://rea.tools/ja/showcase/)
+[](https://rea.tools/ja/showcase/)
### DX-Ball:音声のパン計算を再構築する
@@ -189,28 +189,28 @@ REA で興味深い対象を調べたら、ぜひ共有してください。[iss
どのエージェントで REA を使えますか?
-ローカル MCP サーバーに対応するエージェントなら使えます。セットアップは[対応エージェント](docs/installation.md#supported-agents)を設定します。その他のクライアントでは、[MCP の手動登録](docs/installation.md#mcp-registry)を利用できます。
+ローカル MCP サーバーに対応するエージェントなら使えます。セットアップは[対応エージェント](../installation.md#supported-agents)を設定します。その他のクライアントでは、[MCP の手動登録](../installation.md#mcp-registry)を利用できます。
Hopper、Ghidra、IDA は必要ですか?
-深いネイティブ分析には、そのいずれかを使います。静的な JavaScript と .NET の検査には、ネイティブ分析エンジンは必要ありません。セットアップは承認後に Hopper をインストールできます。Ghidra と IDA は既存のインストールを使います。[プロバイダーのセットアップ](docs/installation.md#hopper)を参照してください。
+深いネイティブ分析には、そのいずれかを使います。静的な JavaScript と .NET の検査には、ネイティブ分析エンジンは必要ありません。セットアップは承認後に Hopper をインストールできます。Ghidra と IDA は既存のインストールを使います。[プロバイダーのセットアップ](../installation.md#hopper)を参照してください。
先に Hopper を起動する必要はありますか?
-REA は必要な操作の際に Hopper を起動します。macOS では、初回起動時にデモモードを選ぶか、ライセンスを有効化するよう求められることがあります。[Hopper の起動とトラブルシューティング](docs/installation.md#launcher-paths-and-troubleshooting)を参照してください。
+REA は必要な操作の際に Hopper を起動します。macOS では、初回起動時にデモモードを選ぶか、ライセンスを有効化するよう求められることがあります。[Hopper の起動とトラブルシューティング](../installation.md#launcher-paths-and-troubleshooting)を参照してください。
skills.sh からスキルをインストールすると何ができますか?
-スキルはエージェントに調査の手順を提供します。`npx rea-agents setup` で REA の MCP サーバーを登録し、対応する手順をインストールしてから、エージェントを再起動してください。[スキルのみのインストール](docs/installation.md#skill-only-installation)を参照してください。
+スキルはエージェントに調査の手順を提供します。`npx rea-agents setup` で REA の MCP サーバーを登録し、対応する手順をインストールしてから、エージェントを再起動してください。[スキルのみのインストール](../installation.md#skill-only-installation)を参照してください。
@@ -253,14 +253,14 @@ npx rea-agents@latest setup
まずはウェブサイトの[実践ガイド](https://rea.tools/ja/guides/)を参照してください。正確なオプション、前提条件、結果の契約については、以下を参照してください。
-- [インストールとセットアップ](docs/installation.md):エージェントの登録、プロバイダーの設定、更新、アンインストール。
-- [準備状況の確認とトラブルシューティング](docs/installation.md#check-readiness-for-your-task):特定のエージェントや分析エンジンを診断。
-- [CLI と Evidence](docs/cli.md):コマンド、プロバイダーの選択、スナップショット、インポート/エクスポート、終了ステータス。
-- [MCP 契約](docs/mcp-contracts.md)と[エージェント向けプロンプト](docs/mcp-prompts.md):ツールの結果、セッション、ガイド付き調査。
-- [ツールカタログ](docs/mcp-contracts.md#generated-catalog):ビルド時に生成されるツール、プロバイダー、CLI コマンドの一覧。
-- [ロードマップ](docs/roadmap.md):計画中の作業と機能の進捗。
+- [インストールとセットアップ](../installation.md):エージェントの登録、プロバイダーの設定、更新、アンインストール。
+- [準備状況の確認とトラブルシューティング](../installation.md#check-readiness-for-your-task):特定のエージェントや分析エンジンを診断。
+- [CLI と Evidence](../cli.md):コマンド、プロバイダーの選択、スナップショット、インポート/エクスポート、終了ステータス。
+- [MCP 契約](../mcp-contracts.md)と[エージェント向けプロンプト](../mcp-prompts.md):ツールの結果、セッション、ガイド付き調査。
+- [ツールカタログ](../mcp-contracts.md#generated-catalog):ビルド時に生成されるツール、プロバイダー、CLI コマンドの一覧。
+- [ロードマップ](../roadmap.md):計画中の作業と機能の進捗。
-脆弱性の報告は [SECURITY.md](SECURITY.md) に従ってください。
+脆弱性の報告は [SECURITY.md](../../SECURITY.md) に従ってください。
## スター履歴
@@ -278,7 +278,7 @@ REA の利用、バグ報告、機能の要望、ビルドのテスト、修正
## 免責事項
-責任ある利用については [DISCLAIMER.md](DISCLAIMER.md) を参照してください。
+責任ある利用については [DISCLAIMER.md](../../DISCLAIMER.md) を参照してください。
REA は、法令に従った研究、教育、相互運用性の確保、ソフトウェアの保存、および所有するソフトウェアや調査を許可されたソフトウェアの解析を目的としています。
@@ -290,14 +290,14 @@ applicable licenses.
REA への貢献を歓迎しますしてバグや機能の提案を報告したり、[pull request を送信](https://github.com/morluto/rea/pulls)してコードやドキュメントを改善したりできます。
-開発環境とチェックは [CONTRIBUTING.md](CONTRIBUTING.md)、検証レーンは[テストガイド](docs/testing.md)、プロジェクト構造は[アーキテクチャ図](docs/architecture.mermaid)を参照してください。
+開発環境とチェックは [CONTRIBUTING.md](../../CONTRIBUTING.md)、検証レーンは[テストガイド](../testing.md)、プロジェクト構造は[アーキテクチャ図](../architecture.mermaid)を参照してください。
## ライセンス
-[MIT](LICENSE)
+[MIT](../../LICENSE)
REA-authored code is licensed under MIT. Third-party material retains its own
-licenses and copyright notices; see [Third-party notices](THIRD_PARTY_NOTICES.md)
+licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
for the dependency inventory, preserved license texts, and external-tool terms.
**You must legally obtain and be authorized to use commercial tools**, including
diff --git a/README_ko.md b/docs/readme/README_ko.md
similarity index 74%
rename from README_ko.md
rename to docs/readme/README_ko.md
index 5a9e7cd5d..26db0d824 100644
--- a/README_ko.md
+++ b/docs/readme/README_ko.md
@@ -1,6 +1,6 @@
-[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · **한국어** · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · **한국어** · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: 무엇이든 리버스 엔지니어링
@@ -9,10 +9,10 @@
**마음에 드는 기능을 발견했다면, 바이너리 수준까지 작동 원리를 이해하세요.**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#current-status)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
@@ -25,7 +25,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
리버스 엔지니어링 커뮤니티에 참여하세요
Discord · 질문과 답변 · 결과 공유
@@ -67,7 +67,7 @@ npx rea-agents setup
에이전트를 선택하고, 예정된 변경 사항을 검토한 뒤 승인하세요. 설정 과정은 기존 설정을 백업하고 REA의 MCP 서버와 버전에 맞는 워크플로 지침을 추가합니다. 완료 후 에이전트를 다시 시작하세요.
-설정은 Claude Code, Codex, Cursor, Gemini CLI, Grok Build 및 [다른 에이전트](docs/installation.md#supported-agents)를 지원합니다. 제공자 설정과 수동 MCP 등록은 [설치 및 설정](docs/installation.md)을 참고하세요.
+설정은 Claude Code, Codex, Cursor, Gemini CLI, Grok Build 및 [다른 에이전트](../installation.md#supported-agents)를 지원합니다. 제공자 설정과 수동 MCP 등록은 [설치 및 설정](../installation.md)을 참고하세요.
### 에이전트에게 요청
@@ -94,7 +94,7 @@ npm install --global rea-agents
rea --help
```
-네이티브 분석에는 먼저 제공자를 설정해야 합니다. 네이티브 명령, 제공자 선택, 스냅샷, 스크립트 사용은 [CLI 및 Evidence 가이드](docs/cli.md)를 참고하세요.
+네이티브 분석에는 먼저 제공자를 설정해야 합니다. 네이티브 명령, 제공자 선택, 스냅샷, 스크립트 사용은 [CLI 및 Evidence 가이드](../cli.md)를 참고하세요.
### REA 업데이트
@@ -120,9 +120,9 @@ npx rea-agents@latest setup
에이전트는 MCP를 통해 REA를 호출하여 대상을 검사하고 관련 코드를 추적합니다. REA는 조사 결과와 근거를 반환합니다. 에이전트는 이를 바탕으로 추가 질문을 하고, 동작을 설명하거나, 구현을 작성하고 테스트합니다. CLI 명령도 같은 워크플로를 사용합니다.
-
+
-[원본 크기의 그림 열기](website/public/assets/figures/rea-investigation-flow.svg).
+[원본 크기의 그림 열기](../../website/public/assets/figures/rea-investigation-flow.svg).
@@ -130,37 +130,37 @@ npx rea-agents@latest setup
REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니다. 추가 도구와 호스트 지원은 대상에 따라 다릅니다.
-| 대상 | REA가 반환하는 내용 | 요구 사항 및 가이드 |
-| --------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
-| 네이티브 바이너리 | 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조 | Hopper, Ghidra 또는 IDA; [네이티브 분석](https://rea.tools/ko/guides/native/) |
-| 오프라인 ELF 레이아웃 | 섹션, 세그먼트, 원본 심볼/재배치 정보, 정적 분석으로 얻은 보안 완화 기법 후보 | Linux x64에서 호출자가 제공하는 pwntools; [바이너리 진단](docs/binary-diagnostics.md) |
-| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](docs/evm-bytecode.md) |
-| 기록된 Linux 크래시 | 원시 note 레코드, 기록된 모든 스레드의 레지스터/시그널, 선택적 매핑 후보 | Linux x64에서 호출자가 제공하는 pwntools; GDB/pwndbg는 선택 사항; [기록된 크래시](docs/recorded-crashes.md) |
-| JavaScript / Electron | 모듈, 임포트, 소스 맵, 라우트, IPC, 네이티브 애드온 관계 | Node.js와 npm; [애플리케이션 분석](https://rea.tools/ko/guides/javascript/) |
-| 웹사이트 | 페이지 구조, 스크립트, 네트워크 관찰 결과, 요청한 스크린샷 | Chrome 계열 브라우저; [브라우저 분석](https://rea.tools/ko/guides/browser/) |
-| 저장된 네트워크 캡처 | 요청, 응답, 접근 가능한 페이로드, 소스 위치 | HAR; mitmproxy 네이티브 캡처에는 Linux의 mitmdump; [캡처 가이드](docs/web-network-captures.md) |
-| 패킷 캡처 | 엔드포인트, DNS, TLS SNI, 프로토콜 분포, HTTP 요청 | 호출자 제공 tshark 및 capinfos; [pcap 가이드](docs/pcap-analysis.md) |
-| .NET 어셈블리 | 메타데이터, CIL 명령, 선언된 네이티브 의존성, 빌드 비교 | 정적 검사; [관리 코드 가이드](docs/managed-code-analysis.md) |
-| Android APK | 매니페스트 선언, 클래스, 디컴파일된 메서드, 참조 | Linux/macOS/Windows x64의 헤드리스 JADX와 전체 JDK; [Android 가이드](docs/android-analysis.md) |
-| Android 기기 | 기기, 패키지, 프로세스, 로그, 덤프, 화면 캡처, APK/파일 전송 | 호출자 제공 adb(에뮬레이터 또는 실제 기기); [ADB 가이드](docs/adb-device-analysis.md) |
-| JEB 프로젝트 | 엔진 기반 프로젝트 유닛, 아티팩트 해시, 타입/메서드 의사코드 | 호출자가 시작하고 MCP를 제공하는 JEB 클라이언트(REA_JEB_MCP_URL로 선택); [JEB 가이드](docs/jeb-analysis.md) |
-| Android 리소스 | 디코딩된 매니페스트, 문자열 테이블, 로캘, 버전 정보 | 호출자 제공 Apktool 런처; [리소스 가이드](docs/apktool-resource-analysis.md) |
-| Flutter 앱 | Dart 스냅샷 해시, ABI별 라이브러리 해시, 엔진 빌드 출처 | 없음 (정적 APK 파싱); [Flutter 가이드](docs/flutter-analysis.md) |
-| 펌웨어 | 영역, 추출 결과, 네이티브 분석으로 전달할 정보 | Linux의 Binwalk / Unblob; [펌웨어 가이드](docs/firmware-analysis.md) |
-| 기록된 패킷 | 선택 필드, 반복 값, 필터 및 디코딩 규칙 | Linux: TShark 4.x + prlimit; [캡처 가이드](docs/packet-captures.md) |
-| 기록된 논리 신호 | 디코더 주석, 샘플 범위, 진단 정보 | Linux: sigrok-cli 0.7.2 + prlimit; [신호 기록 가이드](docs/signal-recordings.md) |
-| 패키지 및 리소스 | 파일 목록, 다이제스트, plist, Apple 번들 구조, 추출한 리소스 | [아티팩트 및 JavaScript 가이드](docs/javascript-artifact-reconstruction.md), [Apple 애플리케이션](docs/apple-application-analysis.md) |
-| 프로세스 동작 | 터미널 출력, 상호작용, 종료 및 파일 시스템 관찰 결과, 실행 비교 | 네이티브 PTY를 지원하는 Linux/macOS; [프로세스 캡처](docs/process-capture.md) |
+| 대상 | REA가 반환하는 내용 | 요구 사항 및 가이드 |
+| --------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
+| 네이티브 바이너리 | 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조 | Hopper, Ghidra 또는 IDA; [네이티브 분석](https://rea.tools/ko/guides/native/) |
+| 오프라인 ELF 레이아웃 | 섹션, 세그먼트, 원본 심볼/재배치 정보, 정적 분석으로 얻은 보안 완화 기법 후보 | Linux x64에서 호출자가 제공하는 pwntools; [바이너리 진단](../binary-diagnostics.md) |
+| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](../evm-bytecode.md) |
+| 기록된 Linux 크래시 | 원시 note 레코드, 기록된 모든 스레드의 레지스터/시그널, 선택적 매핑 후보 | Linux x64에서 호출자가 제공하는 pwntools; GDB/pwndbg는 선택 사항; [기록된 크래시](../recorded-crashes.md) |
+| JavaScript / Electron | 모듈, 임포트, 소스 맵, 라우트, IPC, 네이티브 애드온 관계 | Node.js와 npm; [애플리케이션 분석](https://rea.tools/ko/guides/javascript/) |
+| 웹사이트 | 페이지 구조, 스크립트, 네트워크 관찰 결과, 요청한 스크린샷 | Chrome 계열 브라우저; [브라우저 분석](https://rea.tools/ko/guides/browser/) |
+| 저장된 네트워크 캡처 | 요청, 응답, 접근 가능한 페이로드, 소스 위치 | HAR; mitmproxy 네이티브 캡처에는 Linux의 mitmdump; [캡처 가이드](../web-network-captures.md) |
+| 패킷 캡처 | 엔드포인트, DNS, TLS SNI, 프로토콜 분포, HTTP 요청 | 호출자 제공 tshark 및 capinfos; [pcap 가이드](../pcap-analysis.md) |
+| .NET 어셈블리 | 메타데이터, CIL 명령, 선언된 네이티브 의존성, 빌드 비교 | 정적 검사; [관리 코드 가이드](../managed-code-analysis.md) |
+| Android APK | 매니페스트 선언, 클래스, 디컴파일된 메서드, 참조 | Linux/macOS/Windows x64의 헤드리스 JADX와 전체 JDK; [Android 가이드](../android-analysis.md) |
+| Android 기기 | 기기, 패키지, 프로세스, 로그, 덤프, 화면 캡처, APK/파일 전송 | 호출자 제공 adb(에뮬레이터 또는 실제 기기); [ADB 가이드](../adb-device-analysis.md) |
+| JEB 프로젝트 | 엔진 기반 프로젝트 유닛, 아티팩트 해시, 타입/메서드 의사코드 | 호출자가 시작하고 MCP를 제공하는 JEB 클라이언트(REA_JEB_MCP_URL로 선택); [JEB 가이드](../jeb-analysis.md) |
+| Android 리소스 | 디코딩된 매니페스트, 문자열 테이블, 로캘, 버전 정보 | 호출자 제공 Apktool 런처; [리소스 가이드](../apktool-resource-analysis.md) |
+| Flutter 앱 | Dart 스냅샷 해시, ABI별 라이브러리 해시, 엔진 빌드 출처 | 없음 (정적 APK 파싱); [Flutter 가이드](../flutter-analysis.md) |
+| 펌웨어 | 영역, 추출 결과, 네이티브 분석으로 전달할 정보 | Linux의 Binwalk / Unblob; [펌웨어 가이드](../firmware-analysis.md) |
+| 기록된 패킷 | 선택 필드, 반복 값, 필터 및 디코딩 규칙 | Linux: TShark 4.x + prlimit; [캡처 가이드](../packet-captures.md) |
+| 기록된 논리 신호 | 디코더 주석, 샘플 범위, 진단 정보 | Linux: sigrok-cli 0.7.2 + prlimit; [신호 기록 가이드](../signal-recordings.md) |
+| 패키지 및 리소스 | 파일 목록, 다이제스트, plist, Apple 번들 구조, 추출한 리소스 | [아티팩트 및 JavaScript 가이드](../javascript-artifact-reconstruction.md), [Apple 애플리케이션](../apple-application-analysis.md) |
+| 프로세스 동작 | 터미널 출력, 상호작용, 종료 및 파일 시스템 관찰 결과, 실행 비교 | 네이티브 PTY를 지원하는 Linux/macOS; [프로세스 캡처](../process-capture.md) |
정적 JavaScript 및 .NET 검사는 제공된 파일을 읽으며 애플리케이션을 실행하지 않습니다. 런타임 캡처는 사용자의 권한으로 선택한 대상을 실행하거나 대상과 상호작용합니다. 각 런타임 가이드에서 그 영향을 설명합니다.
-지원하는 네이티브 형식과 호스트는 제공자마다 다릅니다. [Hopper 및 Ghidra 설정](docs/installation.md#hopper), [IDA 가이드](docs/ida-provider.md), [실험적 Windows Ghidra 지원](docs/windows-ghidra-p0.md)을 참고하세요. Ghidra는 [16비트 DOS 분석](docs/ghidra-dos.md)도 지원합니다. 제공자 선택은 [CLI 가이드](docs/cli.md#choose-a-provider)를 참고하세요. 최신 npm 릴리스 이후 추가된 기능은 [릴리스에서의 사용 가능 여부](docs/installation.md#released-package-and-main)를 확인하세요.
+지원하는 네이티브 형식과 호스트는 제공자마다 다릅니다. [Hopper 및 Ghidra 설정](../installation.md#hopper), [IDA 가이드](../ida-provider.md), [실험적 Windows Ghidra 지원](../windows-ghidra-p0.md)을 참고하세요. Ghidra는 [16비트 DOS 분석](../ghidra-dos.md)도 지원합니다. 제공자 선택은 [CLI 가이드](../cli.md#choose-a-provider)를 참고하세요. 최신 npm 릴리스 이후 추가된 기능은 [릴리스에서의 사용 가능 여부](../installation.md#released-package-and-main)를 확인하세요.
## 사례
-[](https://rea.tools/ko/showcase/)
+[](https://rea.tools/ko/showcase/)
### DX-Ball: 사운드 패닝 계산 재구현
@@ -189,28 +189,28 @@ REA로 흥미로운 대상을 분석했다면 알려 주세요. [issue](https://
어떤 에이전트에서 REA를 사용할 수 있나요?
-로컬 MCP 서버를 지원하는 에이전트라면 사용할 수 있습니다. 설정 과정은 [지원하는 에이전트](docs/installation.md#supported-agents)를 구성합니다. 다른 클라이언트는 [수동 MCP 등록](docs/installation.md#mcp-registry)을 이용할 수 있습니다.
+로컬 MCP 서버를 지원하는 에이전트라면 사용할 수 있습니다. 설정 과정은 [지원하는 에이전트](../installation.md#supported-agents)를 구성합니다. 다른 클라이언트는 [수동 MCP 등록](../installation.md#mcp-registry)을 이용할 수 있습니다.
Hopper, Ghidra 또는 IDA가 필요한가요?
-심층 네이티브 분석에는 이 중 하나를 사용합니다. 정적 JavaScript 및 .NET 검사에는 네이티브 분석 엔진이 필요하지 않습니다. 설정 과정에서 승인 후 Hopper를 설치할 수 있으며, Ghidra와 IDA는 기존 설치를 사용합니다. [제공자 설정](docs/installation.md#hopper)을 참고하세요.
+심층 네이티브 분석에는 이 중 하나를 사용합니다. 정적 JavaScript 및 .NET 검사에는 네이티브 분석 엔진이 필요하지 않습니다. 설정 과정에서 승인 후 Hopper를 설치할 수 있으며, Ghidra와 IDA는 기존 설치를 사용합니다. [제공자 설정](../installation.md#hopper)을 참고하세요.
Hopper를 먼저 실행해야 하나요?
-REA는 작업에 필요할 때 Hopper를 실행합니다. macOS에서는 처음 실행할 때 데모 모드를 선택하거나 라이선스를 활성화하라는 대화 상자가 나타날 수 있습니다. [Hopper 실행 및 문제 해결](docs/installation.md#launcher-paths-and-troubleshooting)을 참고하세요.
+REA는 작업에 필요할 때 Hopper를 실행합니다. macOS에서는 처음 실행할 때 데모 모드를 선택하거나 라이선스를 활성화하라는 대화 상자가 나타날 수 있습니다. [Hopper 실행 및 문제 해결](../installation.md#launcher-paths-and-troubleshooting)을 참고하세요.
skills.sh에서 설치하는 스킬은 어떤 역할을 하나요?
-스킬은 에이전트에게 조사 지침을 제공합니다. `npx rea-agents setup`으로 REA의 MCP 서버를 등록하고 버전에 맞는 지침을 설치한 뒤 에이전트를 다시 시작하세요. [스킬만 설치하기](docs/installation.md#skill-only-installation)를 참고하세요.
+스킬은 에이전트에게 조사 지침을 제공합니다. `npx rea-agents setup`으로 REA의 MCP 서버를 등록하고 버전에 맞는 지침을 설치한 뒤 에이전트를 다시 시작하세요. [스킬만 설치하기](../installation.md#skill-only-installation)를 참고하세요.
@@ -253,14 +253,14 @@ npx rea-agents@latest setup
웹사이트의 [실습 가이드](https://rea.tools/ko/guides/)부터 읽어 보세요. 정확한 옵션, 사전 요구 사항, 결과 계약은 다음 문서를 참고하세요.
-- [설치 및 설정](docs/installation.md): 에이전트 등록, 제공자 설정, 업데이트, 제거.
-- [준비 상태 확인 및 문제 해결](docs/installation.md#check-readiness-for-your-task): 특정 에이전트 또는 분석 엔진 진단.
-- [CLI 및 Evidence](docs/cli.md): 명령, 제공자 선택, 스냅샷, 가져오기/내보내기, 종료 상태.
-- [MCP 계약](docs/mcp-contracts.md) 및 [에이전트 프롬프트](docs/mcp-prompts.md): 도구 결과, 세션, 안내에 따른 조사.
-- [도구 카탈로그](docs/mcp-contracts.md#generated-catalog): 빌드 시 생성되는 도구, 제공자, CLI 명령 목록.
-- [로드맵](docs/roadmap.md): 계획된 작업과 기능 진행 상황.
+- [설치 및 설정](../installation.md): 에이전트 등록, 제공자 설정, 업데이트, 제거.
+- [준비 상태 확인 및 문제 해결](../installation.md#check-readiness-for-your-task): 특정 에이전트 또는 분석 엔진 진단.
+- [CLI 및 Evidence](../cli.md): 명령, 제공자 선택, 스냅샷, 가져오기/내보내기, 종료 상태.
+- [MCP 계약](../mcp-contracts.md) 및 [에이전트 프롬프트](../mcp-prompts.md): 도구 결과, 세션, 안내에 따른 조사.
+- [도구 카탈로그](../mcp-contracts.md#generated-catalog): 빌드 시 생성되는 도구, 제공자, CLI 명령 목록.
+- [로드맵](../roadmap.md): 계획된 작업과 기능 진행 상황.
-취약점은 [SECURITY.md](SECURITY.md)에 따라 보고하세요.
+취약점은 [SECURITY.md](../../SECURITY.md)에 따라 보고하세요.
## 스타 기록
@@ -278,7 +278,7 @@ REA를 사용하고, 버그를 보고하고, 기능을 요청하고, 빌드를
## 면책 조항
-책임 있는 사용에 관한 안내는 [DISCLAIMER.md](DISCLAIMER.md)를 참고하세요.
+책임 있는 사용에 관한 안내는 [DISCLAIMER.md](../../DISCLAIMER.md)를 참고하세요.
REA는 법을 준수하는 연구, 교육, 상호운용성 확보, 소프트웨어 보존, 그리고 본인이 소유하거나 조사 권한을 가진 소프트웨어의 분석을 위한 도구입니다.
@@ -290,14 +290,14 @@ applicable licenses.
REA에 도움을 보태 주세요! [issue를 등록](https://github.com/morluto/rea/issues)해 버그나 기능을 제안하거나, [pull request를 보내](https://github.com/morluto/rea/pulls) 코드와 문서를 개선할 수 있습니다.
-개발 환경과 검사 항목은 [CONTRIBUTING.md](CONTRIBUTING.md), 검증 절차는 [테스트 가이드](docs/testing.md), 프로젝트 구조는 [아키텍처 지도](docs/architecture.mermaid)를 참고하세요.
+개발 환경과 검사 항목은 [CONTRIBUTING.md](../../CONTRIBUTING.md), 검증 절차는 [테스트 가이드](../testing.md), 프로젝트 구조는 [아키텍처 지도](../architecture.mermaid)를 참고하세요.
## 라이선스
-[MIT](LICENSE)
+[MIT](../../LICENSE)
REA-authored code is licensed under MIT. Third-party material retains its own
-licenses and copyright notices; see [Third-party notices](THIRD_PARTY_NOTICES.md)
+licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
for the dependency inventory, preserved license texts, and external-tool terms.
**You must legally obtain and be authorized to use commercial tools**, including
diff --git a/README_pl.md b/docs/readme/README_pl.md
similarity index 76%
rename from README_pl.md
rename to docs/readme/README_pl.md
index c54af50fe..48200087d 100644
--- a/README_pl.md
+++ b/docs/readme/README_pl.md
@@ -1,6 +1,6 @@
-[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · **Polski** · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · **Polski** · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Reverse Engineer Anything
@@ -9,10 +9,10 @@
**Widzisz funkcję, która Ci się podoba? Dowiedz się, jak działa, aż do poziomu kodu binarnego.**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#current-status)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
@@ -25,7 +25,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
Dołącz do społeczności inżynierii wstecznej
Discord · Pytania i odpowiedzi · Pokaż swój projekt
@@ -67,7 +67,7 @@ npx rea-agents setup
Wybierz swoich agentów, sprawdź proponowane zmiany i zatwierdź je. Konfigurator dodaje serwer MCP REA i pasujące instrukcje pracy, tworząc kopie zapasowe istniejącej konfiguracji. Następnie uruchom agenta ponownie.
-Konfigurator obsługuje Claude Code, Codex, Cursor, Gemini CLI, Grok Build i [innych agentów](docs/installation.md#supported-agents). Konfigurację dostawców analizy i ręczną rejestrację MCP opisuje przewodnik [instalacji i konfiguracji](docs/installation.md).
+Konfigurator obsługuje Claude Code, Codex, Cursor, Gemini CLI, Grok Build i [innych agentów](../installation.md#supported-agents). Konfigurację dostawców analizy i ręczną rejestrację MCP opisuje przewodnik [instalacji i konfiguracji](../installation.md).
### Zapytaj agenta
@@ -94,7 +94,7 @@ npm install --global rea-agents
rea --help
```
-Przed analizą kodu natywnego skonfiguruj dostawcę analizy. Polecenia analizy natywnej, wybór dostawcy, migawki i użycie w skryptach opisuje [przewodnik CLI i Evidence](docs/cli.md).
+Przed analizą kodu natywnego skonfiguruj dostawcę analizy. Polecenia analizy natywnej, wybór dostawcy, migawki i użycie w skryptach opisuje [przewodnik CLI i Evidence](../cli.md).
### Zaktualizuj REA
@@ -120,9 +120,9 @@ Sprawdź zmiany konfiguracji i uruchom agenta ponownie. Do jednorazowych polece
Twój agent wywołuje REA przez MCP, aby analizować cel i śledzić istotny kod. REA zwraca ustalenia wraz z dowodami. Agent wykorzystuje je do zadawania dalszych pytań, wyjaśniania zachowania lub pisania i testowania implementacji. Polecenia CLI korzystają z tych samych procedur.
-
+
-[Otwórz ilustrację w pełnym rozmiarze](website/public/assets/figures/rea-investigation-flow.svg).
+[Otwórz ilustrację w pełnym rozmiarze](../../website/public/assets/figures/rea-investigation-flow.svg).
@@ -130,37 +130,37 @@ Twój agent wywołuje REA przez MCP, aby analizować cel i śledzić istotny kod
REA wymaga Node.js 22.x (>=22.19), 24.x (>=24.11) lub 26+ oraz npm. Dodatkowe narzędzia i obsługiwane platformy zależą od celu analizy:
-| Cel | Co zwraca REA | Wymagania i przewodnik |
-| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
-| Natywne pliki binarne | Pseudokod, kod asemblera, ciągi znaków, symbole, wywołania i odwołania | Hopper, Ghidra lub IDA; [analiza kodu natywnego](https://rea.tools/pl/guides/native/) |
-| Układ pliku ELF w trybie offline | Sekcje, segmenty, oryginalne symbole/relokacje i potencjalne mechanizmy zabezpieczeń wskazane przez analizę statyczną | pwntools dostarczone przez użytkownika w systemie Linux x64; [diagnostyka plików binarnych](docs/binary-diagnostics.md) |
-| Kod bajtowy EVM | Selektory wywołań, przesunięcia bajtowe, wywnioskowane argumenty i mutowalność | Lokalny plik z surowymi bajtami lub zapisem szesnastkowym w systemie Linux x64; [przewodnik EVM offline](docs/evm-bytecode.md) |
-| Zarejestrowane awarie systemu Linux | Surowe rekordy note, rejestry/sygnały każdego zarejestrowanego wątku i opcjonalni kandydaci na mapowania | pwntools dostarczone przez użytkownika w systemie Linux x64; opcjonalnie GDB/pwndbg; [zarejestrowane awarie](docs/recorded-crashes.md) |
-| JavaScript / Electron | Moduły, importy, mapy źródeł, trasy, IPC i powiązania z natywnymi dodatkami | Node.js i npm; [analiza aplikacji](https://rea.tools/pl/guides/javascript/) |
-| Strony internetowe | Struktura strony, skrypty, obserwacje sieciowe i zamówione zrzuty ekranu | Przeglądarka z rodziny Chrome; [analiza w przeglądarce](https://rea.tools/pl/guides/browser/) |
-| Zapisane przechwycenia ruchu sieciowego | Żądania, odpowiedzi, dostępne dane przesyłane i lokalizacje źródłowe | HAR; mitmdump w systemie Linux dla natywnych przechwyceń mitmproxy; [przewodnik przechwytywania](docs/web-network-captures.md) |
-| Przechwytywanie pakietów | Punkty końcowe, DNS, TLS SNI, dystrybucja protokołów i żądania HTTP | Dostarczone przez wywołującego tshark i capinfos; [przewodnik pcap](docs/pcap-analysis.md) |
-| Zestawy .NET | Metadane, instrukcje CIL, zadeklarowane zależności natywne i porównania kompilacji | Analiza statyczna; [przewodnik kodu zarządzanego](docs/managed-code-analysis.md) |
-| Pakiety APK dla Androida | Deklaracje manifestu, klasy, zdekompilowane metody i odwołania | JADX bez interfejsu graficznego i pełny JDK w systemach Linux/macOS/Windows x64; [przewodnik Androida](docs/android-analysis.md) |
-| Urządzenia z Androidem | Urządzenia, pakiety, procesy, logi, zrzuty, zrzuty ekranu oraz transfery APK/plików | adb dostarczony przez wywołującego (emulator lub urządzenie); [przewodnik ADB](docs/adb-device-analysis.md) |
-| Projekty JEB | Jednostki projektu obsługiwane przez silnik, skróty artefaktów i pseudokod typów/metod | Uruchomiony przez wywołującego klient JEB udostępniający MCP, wybrany przez REA_JEB_MCP_URL; [przewodnik JEB](docs/jeb-analysis.md) |
-| Zasoby Androida | Zdekodowany manifest, tablice ciągów, języki i informacje o wersji | Należący do wywołującego launcher Apktool; [przewodnik zasobów](docs/apktool-resource-analysis.md) |
-| Aplikacje Flutter | Hash migawki Dart, sumy bibliotek per ABI i pochodzenie kompilacji silnika | Brak (statyczna analiza APK); [przewodnik Flutter](docs/flutter-analysis.md) |
-| Oprogramowanie układowe | Regiony, wyniki wyodrębniania i przekazanie do analizy kodu natywnego | Binwalk / Unblob w systemie Linux; [przewodnik oprogramowania układowego](docs/firmware-analysis.md) |
-| Zapisane pakiety | Wybrane pola, powtarzane wartości, filtry i reguły dekodowania | Linux: TShark 4.x + prlimit; [przewodnik po przechwyconych pakietach](docs/packet-captures.md) |
-| Zapisane sygnały logiczne | Adnotacje dekodera, zakresy próbek i diagnostyka | Linux: sigrok-cli 0.7.2 + prlimit; [przewodnik po sygnałach](docs/signal-recordings.md) |
-| Pakiety i zasoby | Spisy plików, skróty, pliki plist, struktura pakietów Apple i wyodrębnione zasoby | [Przewodnik artefaktów i JavaScript](docs/javascript-artifact-reconstruction.md), [aplikacje Apple](docs/apple-application-analysis.md) |
-| Zachowanie procesów | Wyjście terminala, interakcje, obserwacje zakończenia i systemu plików oraz porównania uruchomień | Linux/macOS z natywnym PTY; [przechwytywanie procesów](docs/process-capture.md) |
+| Cel | Co zwraca REA | Wymagania i przewodnik |
+| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
+| Natywne pliki binarne | Pseudokod, kod asemblera, ciągi znaków, symbole, wywołania i odwołania | Hopper, Ghidra lub IDA; [analiza kodu natywnego](https://rea.tools/pl/guides/native/) |
+| Układ pliku ELF w trybie offline | Sekcje, segmenty, oryginalne symbole/relokacje i potencjalne mechanizmy zabezpieczeń wskazane przez analizę statyczną | pwntools dostarczone przez użytkownika w systemie Linux x64; [diagnostyka plików binarnych](../binary-diagnostics.md) |
+| Kod bajtowy EVM | Selektory wywołań, przesunięcia bajtowe, wywnioskowane argumenty i mutowalność | Lokalny plik z surowymi bajtami lub zapisem szesnastkowym w systemie Linux x64; [przewodnik EVM offline](../evm-bytecode.md) |
+| Zarejestrowane awarie systemu Linux | Surowe rekordy note, rejestry/sygnały każdego zarejestrowanego wątku i opcjonalni kandydaci na mapowania | pwntools dostarczone przez użytkownika w systemie Linux x64; opcjonalnie GDB/pwndbg; [zarejestrowane awarie](../recorded-crashes.md) |
+| JavaScript / Electron | Moduły, importy, mapy źródeł, trasy, IPC i powiązania z natywnymi dodatkami | Node.js i npm; [analiza aplikacji](https://rea.tools/pl/guides/javascript/) |
+| Strony internetowe | Struktura strony, skrypty, obserwacje sieciowe i zamówione zrzuty ekranu | Przeglądarka z rodziny Chrome; [analiza w przeglądarce](https://rea.tools/pl/guides/browser/) |
+| Zapisane przechwycenia ruchu sieciowego | Żądania, odpowiedzi, dostępne dane przesyłane i lokalizacje źródłowe | HAR; mitmdump w systemie Linux dla natywnych przechwyceń mitmproxy; [przewodnik przechwytywania](../web-network-captures.md) |
+| Przechwytywanie pakietów | Punkty końcowe, DNS, TLS SNI, dystrybucja protokołów i żądania HTTP | Dostarczone przez wywołującego tshark i capinfos; [przewodnik pcap](../pcap-analysis.md) |
+| Zestawy .NET | Metadane, instrukcje CIL, zadeklarowane zależności natywne i porównania kompilacji | Analiza statyczna; [przewodnik kodu zarządzanego](../managed-code-analysis.md) |
+| Pakiety APK dla Androida | Deklaracje manifestu, klasy, zdekompilowane metody i odwołania | JADX bez interfejsu graficznego i pełny JDK w systemach Linux/macOS/Windows x64; [przewodnik Androida](../android-analysis.md) |
+| Urządzenia z Androidem | Urządzenia, pakiety, procesy, logi, zrzuty, zrzuty ekranu oraz transfery APK/plików | adb dostarczony przez wywołującego (emulator lub urządzenie); [przewodnik ADB](../adb-device-analysis.md) |
+| Projekty JEB | Jednostki projektu obsługiwane przez silnik, skróty artefaktów i pseudokod typów/metod | Uruchomiony przez wywołującego klient JEB udostępniający MCP, wybrany przez REA_JEB_MCP_URL; [przewodnik JEB](../jeb-analysis.md) |
+| Zasoby Androida | Zdekodowany manifest, tablice ciągów, języki i informacje o wersji | Należący do wywołującego launcher Apktool; [przewodnik zasobów](../apktool-resource-analysis.md) |
+| Aplikacje Flutter | Hash migawki Dart, sumy bibliotek per ABI i pochodzenie kompilacji silnika | Brak (statyczna analiza APK); [przewodnik Flutter](../flutter-analysis.md) |
+| Oprogramowanie układowe | Regiony, wyniki wyodrębniania i przekazanie do analizy kodu natywnego | Binwalk / Unblob w systemie Linux; [przewodnik oprogramowania układowego](../firmware-analysis.md) |
+| Zapisane pakiety | Wybrane pola, powtarzane wartości, filtry i reguły dekodowania | Linux: TShark 4.x + prlimit; [przewodnik po przechwyconych pakietach](../packet-captures.md) |
+| Zapisane sygnały logiczne | Adnotacje dekodera, zakresy próbek i diagnostyka | Linux: sigrok-cli 0.7.2 + prlimit; [przewodnik po sygnałach](../signal-recordings.md) |
+| Pakiety i zasoby | Spisy plików, skróty, pliki plist, struktura pakietów Apple i wyodrębnione zasoby | [Przewodnik artefaktów i JavaScript](../javascript-artifact-reconstruction.md), [aplikacje Apple](../apple-application-analysis.md) |
+| Zachowanie procesów | Wyjście terminala, interakcje, obserwacje zakończenia i systemu plików oraz porównania uruchomień | Linux/macOS z natywnym PTY; [przechwytywanie procesów](../process-capture.md) |
Statyczna analiza JavaScript i .NET odczytuje dostarczone pliki bez uruchamiania aplikacji. Przechwytywanie w czasie wykonywania uruchamia wybrany cel lub wchodzi z nim w interakcje z uprawnieniami Twojego użytkownika; skutki opisano w odpowiednich przewodnikach.
-Obsługiwane formaty natywne i platformy zależą od dostawcy analizy. Zobacz [konfigurację Hoppera i Ghidry](docs/installation.md#hopper), [przewodnik IDA](docs/ida-provider.md) oraz [eksperymentalną obsługę Ghidry w systemie Windows](docs/windows-ghidra-p0.md). Ghidra obsługuje też [analizę 16-bitowego DOS-a](docs/ghidra-dos.md). Wybór dostawcy opisuje [przewodnik CLI](docs/cli.md#choose-a-provider). Sprawdź [dostępność w wydaniach](docs/installation.md#released-package-and-main) dla funkcji dodanych po najnowszym wydaniu npm.
+Obsługiwane formaty natywne i platformy zależą od dostawcy analizy. Zobacz [konfigurację Hoppera i Ghidry](../installation.md#hopper), [przewodnik IDA](../ida-provider.md) oraz [eksperymentalną obsługę Ghidry w systemie Windows](../windows-ghidra-p0.md). Ghidra obsługuje też [analizę 16-bitowego DOS-a](../ghidra-dos.md). Wybór dostawcy opisuje [przewodnik CLI](../cli.md#choose-a-provider). Sprawdź [dostępność w wydaniach](../installation.md#released-package-and-main) dla funkcji dodanych po najnowszym wydaniu npm.
## Przykłady zastosowań
-[](https://rea.tools/pl/showcase/)
+[](https://rea.tools/pl/showcase/)
### DX-Ball: odtworzenie obliczania panoramy dźwięku
@@ -187,28 +187,28 @@ Jeśli użyłeś REA do czegoś ciekawego, chętnie to zobaczymy. Opisz swój pr
Którzy agenci mogą korzystać z REA?
-Każdy agent obsługujący lokalne serwery MCP. Konfigurator konfiguruje [obsługiwanych agentów](docs/installation.md#supported-agents); inne klienty mogą użyć [ręcznej rejestracji MCP](docs/installation.md#mcp-registry).
+Każdy agent obsługujący lokalne serwery MCP. Konfigurator konfiguruje [obsługiwanych agentów](../installation.md#supported-agents); inne klienty mogą użyć [ręcznej rejestracji MCP](../installation.md#mcp-registry).
Czy potrzebuję Hoppera, Ghidry lub IDA?
-Dogłębna analiza kodu natywnego używa jednego z nich. Statyczna analiza JavaScript i .NET działa bez silnika analizy natywnej. Konfigurator może zainstalować Hoppera po uzyskaniu zgody; Ghidra i IDA korzystają z istniejących instalacji. Zobacz [konfigurację dostawców analizy](docs/installation.md#hopper).
+Dogłębna analiza kodu natywnego używa jednego z nich. Statyczna analiza JavaScript i .NET działa bez silnika analizy natywnej. Konfigurator może zainstalować Hoppera po uzyskaniu zgody; Ghidra i IDA korzystają z istniejących instalacji. Zobacz [konfigurację dostawców analizy](../installation.md#hopper).
Czy muszę najpierw uruchomić Hoppera?
-REA uruchamia Hoppera, gdy wymaga tego operacja. W systemie macOS przy pierwszym uruchomieniu okno dialogowe może poprosić o wybór trybu demonstracyjnego lub aktywację licencji. Zobacz [uruchamianie Hoppera i rozwiązywanie problemów](docs/installation.md#launcher-paths-and-troubleshooting).
+REA uruchamia Hoppera, gdy wymaga tego operacja. W systemie macOS przy pierwszym uruchomieniu okno dialogowe może poprosić o wybór trybu demonstracyjnego lub aktywację licencji. Zobacz [uruchamianie Hoppera i rozwiązywanie problemów](../installation.md#launcher-paths-and-troubleshooting).
Co daje zainstalowanie umiejętności z skills.sh?
-Umiejętność dostarcza agentowi instrukcje prowadzenia analizy. Użyj `npx rea-agents setup`, aby zarejestrować serwer MCP REA i zainstalować pasujące instrukcje, a następnie uruchom agenta ponownie. Zobacz [instalację samej umiejętności](docs/installation.md#skill-only-installation).
+Umiejętność dostarcza agentowi instrukcje prowadzenia analizy. Użyj `npx rea-agents setup`, aby zarejestrować serwer MCP REA i zainstalować pasujące instrukcje, a następnie uruchom agenta ponownie. Zobacz [instalację samej umiejętności](../installation.md#skill-only-installation).
@@ -251,14 +251,14 @@ Jeśli używasz agenta, wykonaj [odświeżenie konfiguracji](#zaktualizuj-rea) i
Zacznij od [przewodników z przykładami](https://rea.tools/pl/guides/) na stronie internetowej. Dokładne opcje, wymagania wstępne i kontrakty wyników opisano tutaj:
-- [Instalacja i konfiguracja](docs/installation.md): rejestracja agentów, konfiguracja dostawców analizy, aktualizacje i odinstalowanie.
-- [Gotowość i rozwiązywanie problemów](docs/installation.md#check-readiness-for-your-task): diagnostyka jednego agenta lub silnika analizy.
-- [CLI i Evidence](docs/cli.md): polecenia, wybór dostawcy, migawki, import/eksport i kody zakończenia.
-- [Kontrakty MCP](docs/mcp-contracts.md) i [prompty dla agentów](docs/mcp-prompts.md): wyniki narzędzi, sesje i prowadzone analizy.
-- [Katalog narzędzi](docs/mcp-contracts.md#generated-catalog): generowany podczas kompilacji wykaz narzędzi, dostawców i poleceń CLI.
-- [Plan rozwoju](docs/roadmap.md): planowane prace i śledzenie funkcjonalności.
+- [Instalacja i konfiguracja](../installation.md): rejestracja agentów, konfiguracja dostawców analizy, aktualizacje i odinstalowanie.
+- [Gotowość i rozwiązywanie problemów](../installation.md#check-readiness-for-your-task): diagnostyka jednego agenta lub silnika analizy.
+- [CLI i Evidence](../cli.md): polecenia, wybór dostawcy, migawki, import/eksport i kody zakończenia.
+- [Kontrakty MCP](../mcp-contracts.md) i [prompty dla agentów](../mcp-prompts.md): wyniki narzędzi, sesje i prowadzone analizy.
+- [Katalog narzędzi](../mcp-contracts.md#generated-catalog): generowany podczas kompilacji wykaz narzędzi, dostawców i poleceń CLI.
+- [Plan rozwoju](../roadmap.md): planowane prace i śledzenie funkcjonalności.
-Zgłaszaj podatności zgodnie z [SECURITY.md](SECURITY.md).
+Zgłaszaj podatności zgodnie z [SECURITY.md](../../SECURITY.md).
## Historia gwiazdek
@@ -276,7 +276,7 @@ Dziękujemy wszystkim, którzy używają REA, zgłaszają błędy, proponują no
## Zastrzeżenie
-Wskazówki dotyczące odpowiedzialnego korzystania znajdziesz w [DISCLAIMER.md](DISCLAIMER.md).
+Wskazówki dotyczące odpowiedzialnego korzystania znajdziesz w [DISCLAIMER.md](../../DISCLAIMER.md).
REA służy zgodnym z prawem badaniom, edukacji, zapewnianiu interoperacyjności, zachowaniu oprogramowania oraz analizie oprogramowania, które posiadasz lub masz prawo badać.
@@ -288,14 +288,14 @@ applicable licenses.
Chętnie przyjmiemy Twoją pomoc w rozwoju REA! [Otwórz zgłoszenie](https://github.com/morluto/rea/issues), aby poinformować o błędzie lub zaproponować funkcję, albo [wyślij pull request](https://github.com/morluto/rea/pulls) z ulepszeniami kodu lub dokumentacji.
-Zobacz [CONTRIBUTING.md](CONTRIBUTING.md), aby poznać konfigurację środowiska programistycznego i kontrole, [testowanie](docs/testing.md), aby poznać ścieżki weryfikacji, oraz [mapę architektury](docs/architecture.mermaid), aby poznać strukturę projektu.
+Zobacz [CONTRIBUTING.md](../../CONTRIBUTING.md), aby poznać konfigurację środowiska programistycznego i kontrole, [testowanie](../testing.md), aby poznać ścieżki weryfikacji, oraz [mapę architektury](../architecture.mermaid), aby poznać strukturę projektu.
## Licencja
-[MIT](LICENSE)
+[MIT](../../LICENSE)
REA-authored code is licensed under MIT. Third-party material retains its own
-licenses and copyright notices; see [Third-party notices](THIRD_PARTY_NOTICES.md)
+licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
for the dependency inventory, preserved license texts, and external-tool terms.
**You must legally obtain and be authorized to use commercial tools**, including
diff --git a/README_pt-BR.md b/docs/readme/README_pt-BR.md
similarity index 75%
rename from README_pt-BR.md
rename to docs/readme/README_pt-BR.md
index dd138f7ff..6c3f34e35 100644
--- a/README_pt-BR.md
+++ b/docs/readme/README_pt-BR.md
@@ -1,6 +1,6 @@
-[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · **Português (Brasil)** · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · **Português (Brasil)** · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Faça engenharia reversa de qualquer coisa
@@ -9,10 +9,10 @@
**Viu uma funcionalidade de que gostou? Entenda como ela funciona, até o nível binário.**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#current-status)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
@@ -25,7 +25,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
Participe da comunidade de engenharia reversa
Discord · Perguntas e respostas · Compartilhe seus resultados
@@ -67,7 +67,7 @@ npx rea-agents setup
Escolha seus agentes, revise as alterações propostas e aprove-as. A configuração adiciona o servidor MCP do REA e as instruções de fluxo de trabalho correspondentes, com backups das configurações existentes. Reinicie seu agente depois.
-A configuração oferece suporte ao Claude Code, Codex, Cursor, Gemini CLI, Grok Build e [outros agentes](docs/installation.md#supported-agents). Consulte [instalação e configuração](docs/installation.md) para configurar provedores e registrar o MCP manualmente.
+A configuração oferece suporte ao Claude Code, Codex, Cursor, Gemini CLI, Grok Build e [outros agentes](../installation.md#supported-agents). Consulte [instalação e configuração](../installation.md) para configurar provedores e registrar o MCP manualmente.
### Peça ao seu agente
@@ -94,7 +94,7 @@ npm install --global rea-agents
rea --help
```
-Para análise nativa, configure primeiro um provedor. Consulte o [guia de CLI e Evidence](docs/cli.md) para comandos nativos, seleção de provedores, snapshots e uso em scripts.
+Para análise nativa, configure primeiro um provedor. Consulte o [guia de CLI e Evidence](../cli.md) para comandos nativos, seleção de provedores, snapshots e uso em scripts.
### Atualize o REA
@@ -120,9 +120,9 @@ Revise as alterações de configuração e reinicie seu agente. Para comandos av
Seu agente chama o REA por MCP para inspecionar o alvo e rastrear o código relevante. O REA retorna os resultados com suas evidências. O agente usa esses resultados para fazer novas perguntas, explicar o comportamento ou escrever e testar uma implementação. Os comandos da CLI usam os mesmos fluxos de trabalho.
-
+
-[Abra a figura em tamanho completo](website/public/assets/figures/rea-investigation-flow.svg).
+[Abra a figura em tamanho completo](../../website/public/assets/figures/rea-investigation-flow.svg).
@@ -130,37 +130,37 @@ Seu agente chama o REA por MCP para inspecionar o alvo e rastrear o código rele
O REA requer Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, além do npm. As ferramentas adicionais e os sistemas anfitriões compatíveis dependem do alvo:
-| Alvo | O que o REA retorna | Requisitos e guia |
-| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
-| Binários nativos | Pseudocódigo, assembly, strings, símbolos, chamadas e referências | Hopper, Ghidra ou IDA; [análise nativa](https://rea.tools/pt-BR/guides/native/) |
-| Estrutura ELF sem execução | Seções, segmentos, símbolos/relocações originais e possíveis mecanismos de mitigação identificados por análise estática | pwntools fornecido pelo chamador no Linux x64; [diagnóstico de binários](docs/binary-diagnostics.md) |
-| Bytecode EVM | Seletores de despacho, offsets de bytes, argumentos inferidos e mutabilidade | Entrada local com bytes brutos ou representação hexadecimal no Linux x64; [guia de EVM sem execução](docs/evm-bytecode.md) |
-| Falhas do Linux registradas | Registros note brutos, registradores/sinais de cada thread registrada e candidatos opcionais a mapeamentos | pwntools fornecido pelo chamador no Linux x64; GDB/pwndbg opcionais; [falhas registradas](docs/recorded-crashes.md) |
-| JavaScript / Electron | Módulos, importações, source maps, rotas, IPC e relações com complementos nativos | Node.js e npm; [análise de aplicativos](https://rea.tools/pt-BR/guides/javascript/) |
-| Sites | Estrutura da página, scripts, observações de rede e capturas de tela solicitadas | Um navegador da família Chrome; [análise do navegador](https://rea.tools/pt-BR/guides/browser/) |
-| Capturas de rede salvas | Requisições, respostas, payloads acessíveis e localizações de origem | HAR; mitmdump no Linux para capturas no formato nativo do mitmproxy; [guia de capturas](docs/web-network-captures.md) |
-| Capturas de pacotes | Endpoints, DNS, TLS SNI, distribuição de protocolos e requisições HTTP | tshark e capinfos fornecidos pelo chamador; [guia pcap](docs/pcap-analysis.md) |
-| Assemblies .NET | Metadados, instruções CIL, dependências nativas declaradas e comparações de builds | Inspeção estática; [guia de código gerenciado](docs/managed-code-analysis.md) |
-| APKs Android | Declarações do manifesto, classes, métodos descompilados e referências | JADX sem interface gráfica e um JDK completo no Linux/macOS/Windows x64; [guia de Android](docs/android-analysis.md) |
-| Dispositivos Android | Dispositivos, pacotes, processos, logs, despejos, capturas de tela e transferências de APK/arquivos | adb fornecido pelo chamador (emulador ou aparelho); [guia de ADB](docs/adb-device-analysis.md) |
-| Projetos JEB | Unidades de projeto do motor, hashes de artefatos e pseudocódigo de tipos/métodos | Um cliente JEB iniciado pelo chamador servindo MCP, selecionado com REA_JEB_MCP_URL; [guia de JEB](docs/jeb-analysis.md) |
-| Recursos do Android | Manifesto decodificado, tabelas de strings, idiomas e fatos de versão | Lançador do Apktool fornecido pelo chamador; [guia de recursos](docs/apktool-resource-analysis.md) |
-| Aplicativos Flutter | Hash de snapshot Dart, hashes de bibliotecas por ABI e procedência de build do motor | Nenhuma (análise estática de APK); [guia Flutter](docs/flutter-analysis.md) |
-| Firmware | Regiões, resultados de extração e encaminhamentos à análise nativa | Binwalk / Unblob no Linux; [guia de firmware](docs/firmware-analysis.md) |
-| Pacotes gravados | Campos selecionados, valores repetidos, filtros e regras de decodificação | Linux: TShark 4.x + prlimit; [guia de capturas](docs/packet-captures.md) |
-| Sinais lógicos gravados | Anotações do decodificador, intervalos de amostras e diagnósticos | Linux: sigrok-cli 0.7.2 + prlimit; [guia de sinais](docs/signal-recordings.md) |
-| Pacotes e recursos | Inventários de arquivos, digests, plists, estrutura de bundles Apple e recursos extraídos | [Guia de artefatos e JavaScript](docs/javascript-artifact-reconstruction.md), [aplicativos Apple](docs/apple-application-analysis.md) |
-| Comportamento de processos | Saída do terminal, interações, observações de encerramento e do sistema de arquivos, e comparações entre execuções | Linux/macOS com PTY nativo; [captura de processos](docs/process-capture.md) |
+| Alvo | O que o REA retorna | Requisitos e guia |
+| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
+| Binários nativos | Pseudocódigo, assembly, strings, símbolos, chamadas e referências | Hopper, Ghidra ou IDA; [análise nativa](https://rea.tools/pt-BR/guides/native/) |
+| Estrutura ELF sem execução | Seções, segmentos, símbolos/relocações originais e possíveis mecanismos de mitigação identificados por análise estática | pwntools fornecido pelo chamador no Linux x64; [diagnóstico de binários](../binary-diagnostics.md) |
+| Bytecode EVM | Seletores de despacho, offsets de bytes, argumentos inferidos e mutabilidade | Entrada local com bytes brutos ou representação hexadecimal no Linux x64; [guia de EVM sem execução](../evm-bytecode.md) |
+| Falhas do Linux registradas | Registros note brutos, registradores/sinais de cada thread registrada e candidatos opcionais a mapeamentos | pwntools fornecido pelo chamador no Linux x64; GDB/pwndbg opcionais; [falhas registradas](../recorded-crashes.md) |
+| JavaScript / Electron | Módulos, importações, source maps, rotas, IPC e relações com complementos nativos | Node.js e npm; [análise de aplicativos](https://rea.tools/pt-BR/guides/javascript/) |
+| Sites | Estrutura da página, scripts, observações de rede e capturas de tela solicitadas | Um navegador da família Chrome; [análise do navegador](https://rea.tools/pt-BR/guides/browser/) |
+| Capturas de rede salvas | Requisições, respostas, payloads acessíveis e localizações de origem | HAR; mitmdump no Linux para capturas no formato nativo do mitmproxy; [guia de capturas](../web-network-captures.md) |
+| Capturas de pacotes | Endpoints, DNS, TLS SNI, distribuição de protocolos e requisições HTTP | tshark e capinfos fornecidos pelo chamador; [guia pcap](../pcap-analysis.md) |
+| Assemblies .NET | Metadados, instruções CIL, dependências nativas declaradas e comparações de builds | Inspeção estática; [guia de código gerenciado](../managed-code-analysis.md) |
+| APKs Android | Declarações do manifesto, classes, métodos descompilados e referências | JADX sem interface gráfica e um JDK completo no Linux/macOS/Windows x64; [guia de Android](../android-analysis.md) |
+| Dispositivos Android | Dispositivos, pacotes, processos, logs, despejos, capturas de tela e transferências de APK/arquivos | adb fornecido pelo chamador (emulador ou aparelho); [guia de ADB](../adb-device-analysis.md) |
+| Projetos JEB | Unidades de projeto do motor, hashes de artefatos e pseudocódigo de tipos/métodos | Um cliente JEB iniciado pelo chamador servindo MCP, selecionado com REA_JEB_MCP_URL; [guia de JEB](../jeb-analysis.md) |
+| Recursos do Android | Manifesto decodificado, tabelas de strings, idiomas e fatos de versão | Lançador do Apktool fornecido pelo chamador; [guia de recursos](../apktool-resource-analysis.md) |
+| Aplicativos Flutter | Hash de snapshot Dart, hashes de bibliotecas por ABI e procedência de build do motor | Nenhuma (análise estática de APK); [guia Flutter](../flutter-analysis.md) |
+| Firmware | Regiões, resultados de extração e encaminhamentos à análise nativa | Binwalk / Unblob no Linux; [guia de firmware](../firmware-analysis.md) |
+| Pacotes gravados | Campos selecionados, valores repetidos, filtros e regras de decodificação | Linux: TShark 4.x + prlimit; [guia de capturas](../packet-captures.md) |
+| Sinais lógicos gravados | Anotações do decodificador, intervalos de amostras e diagnósticos | Linux: sigrok-cli 0.7.2 + prlimit; [guia de sinais](../signal-recordings.md) |
+| Pacotes e recursos | Inventários de arquivos, digests, plists, estrutura de bundles Apple e recursos extraídos | [Guia de artefatos e JavaScript](../javascript-artifact-reconstruction.md), [aplicativos Apple](../apple-application-analysis.md) |
+| Comportamento de processos | Saída do terminal, interações, observações de encerramento e do sistema de arquivos, e comparações entre execuções | Linux/macOS com PTY nativo; [captura de processos](../process-capture.md) |
A inspeção estática de JavaScript e .NET lê os arquivos fornecidos sem executar o aplicativo. A captura em execução inicia o alvo selecionado ou interage com ele usando as permissões do seu usuário; cada guia descreve seus efeitos.
-Os formatos nativos e os sistemas anfitriões compatíveis variam por provedor. Consulte [configuração do Hopper e Ghidra](docs/installation.md#hopper), o [guia do IDA](docs/ida-provider.md) e o [suporte experimental ao Ghidra no Windows](docs/windows-ghidra-p0.md). O Ghidra também oferece [análise de DOS de 16 bits](docs/ghidra-dos.md). Para selecionar um provedor, consulte o [guia da CLI](docs/cli.md#choose-a-provider). Verifique a [disponibilidade nas versões](docs/installation.md#released-package-and-main) das funcionalidades adicionadas após a versão mais recente no npm.
+Os formatos nativos e os sistemas anfitriões compatíveis variam por provedor. Consulte [configuração do Hopper e Ghidra](../installation.md#hopper), o [guia do IDA](../ida-provider.md) e o [suporte experimental ao Ghidra no Windows](../windows-ghidra-p0.md). O Ghidra também oferece [análise de DOS de 16 bits](../ghidra-dos.md). Para selecionar um provedor, consulte o [guia da CLI](../cli.md#choose-a-provider). Verifique a [disponibilidade nas versões](../installation.md#released-package-and-main) das funcionalidades adicionadas após a versão mais recente no npm.
## Casos de uso
-[](https://rea.tools/pt-BR/showcase/)
+[](https://rea.tools/pt-BR/showcase/)
### DX-Ball: reconstruir um cálculo de panorâmica de áudio
@@ -187,28 +187,28 @@ Se você usou o REA em algo interessante, gostaríamos de conhecer. Compartilhe
Quais agentes podem usar o REA?
-Qualquer agente que ofereça suporte a servidores MCP locais. A configuração prepara os [agentes compatíveis](docs/installation.md#supported-agents); outros clientes podem usar o [registro manual do MCP](docs/installation.md#mcp-registry).
+Qualquer agente que ofereça suporte a servidores MCP locais. A configuração prepara os [agentes compatíveis](../installation.md#supported-agents); outros clientes podem usar o [registro manual do MCP](../installation.md#mcp-registry).
Preciso do Hopper, Ghidra ou IDA?
-A análise nativa aprofundada usa um desses motores. A inspeção estática de JavaScript e .NET funciona sem um motor de análise nativa. A configuração pode instalar o Hopper após aprovação; Ghidra e IDA usam as instalações existentes. Consulte [configuração dos provedores](docs/installation.md#hopper).
+A análise nativa aprofundada usa um desses motores. A inspeção estática de JavaScript e .NET funciona sem um motor de análise nativa. A configuração pode instalar o Hopper após aprovação; Ghidra e IDA usam as instalações existentes. Consulte [configuração dos provedores](../installation.md#hopper).
Preciso iniciar o Hopper antes?
-O REA inicia o Hopper quando uma operação precisa dele. No macOS, um diálogo na primeira execução pode pedir que você escolha o modo de demonstração ou ative a licença. Consulte [inicialização do Hopper e solução de problemas](docs/installation.md#launcher-paths-and-troubleshooting).
+O REA inicia o Hopper quando uma operação precisa dele. No macOS, um diálogo na primeira execução pode pedir que você escolha o modo de demonstração ou ative a licença. Consulte [inicialização do Hopper e solução de problemas](../installation.md#launcher-paths-and-troubleshooting).
O que faz a instalação da skill pelo skills.sh?
-A skill fornece instruções de investigação ao seu agente. Use `npx rea-agents setup` para registrar o servidor MCP do REA e instalar as instruções correspondentes, depois reinicie seu agente. Consulte [instalação apenas da skill](docs/installation.md#skill-only-installation).
+A skill fornece instruções de investigação ao seu agente. Use `npx rea-agents setup` para registrar o servidor MCP do REA e instalar as instruções correspondentes, depois reinicie seu agente. Consulte [instalação apenas da skill](../installation.md#skill-only-installation).
@@ -251,14 +251,14 @@ Se você usa um agente, conclua a [atualização da configuração](#atualize-o-
Comece pelos [guias práticos](https://rea.tools/pt-BR/guides/) do site. Para opções exatas, pré-requisitos e contratos dos resultados:
-- [Instalação e configuração](docs/installation.md): registro de agentes, configuração de provedores, atualizações e desinstalação.
-- [Prontidão e solução de problemas](docs/installation.md#check-readiness-for-your-task): diagnóstico de um agente ou motor de análise específico.
-- [CLI e Evidence](docs/cli.md): comandos, seleção de provedores, snapshots, importação/exportação e status de saída.
-- [Contratos MCP](docs/mcp-contracts.md) e [prompts para agentes](docs/mcp-prompts.md): resultados das ferramentas, sessões e investigações guiadas.
-- [Catálogo de ferramentas](docs/mcp-contracts.md#generated-catalog): inventário de ferramentas, provedores e comandos da CLI gerado durante o build.
-- [Roadmap](docs/roadmap.md): trabalho planejado e acompanhamento das capacidades.
+- [Instalação e configuração](../installation.md): registro de agentes, configuração de provedores, atualizações e desinstalação.
+- [Prontidão e solução de problemas](../installation.md#check-readiness-for-your-task): diagnóstico de um agente ou motor de análise específico.
+- [CLI e Evidence](../cli.md): comandos, seleção de provedores, snapshots, importação/exportação e status de saída.
+- [Contratos MCP](../mcp-contracts.md) e [prompts para agentes](../mcp-prompts.md): resultados das ferramentas, sessões e investigações guiadas.
+- [Catálogo de ferramentas](../mcp-contracts.md#generated-catalog): inventário de ferramentas, provedores e comandos da CLI gerado durante o build.
+- [Roadmap](../roadmap.md): trabalho planejado e acompanhamento das capacidades.
-Relate vulnerabilidades conforme [SECURITY.md](SECURITY.md).
+Relate vulnerabilidades conforme [SECURITY.md](../../SECURITY.md).
## Histórico de estrelas
@@ -276,7 +276,7 @@ Obrigado a todos que usam o REA, relatam bugs, sugerem funcionalidades, testam b
## Aviso legal
-Consulte [DISCLAIMER.md](DISCLAIMER.md) para orientações sobre o uso responsável.
+Consulte [DISCLAIMER.md](../../DISCLAIMER.md) para orientações sobre o uso responsável.
O REA destina-se à pesquisa, à educação, à interoperabilidade, à preservação de software e à análise de software que você possui ou está autorizado a examinar, dentro da lei.
@@ -288,14 +288,14 @@ applicable licenses.
Sua ajuda com o REA é bem-vinda! [Abra uma issue](https://github.com/morluto/rea/issues) para relatar um bug ou sugerir uma funcionalidade, ou [envie um pull request](https://github.com/morluto/rea/pulls) para melhorar o código ou a documentação.
-Consulte [CONTRIBUTING.md](CONTRIBUTING.md) para configurar o ambiente de desenvolvimento e as verificações, o [guia de testes](docs/testing.md) para os fluxos de verificação e o [mapa da arquitetura](docs/architecture.mermaid) para a estrutura do projeto.
+Consulte [CONTRIBUTING.md](../../CONTRIBUTING.md) para configurar o ambiente de desenvolvimento e as verificações, o [guia de testes](../testing.md) para os fluxos de verificação e o [mapa da arquitetura](../architecture.mermaid) para a estrutura do projeto.
## Licença
-[MIT](LICENSE)
+[MIT](../../LICENSE)
REA-authored code is licensed under MIT. Third-party material retains its own
-licenses and copyright notices; see [Third-party notices](THIRD_PARTY_NOTICES.md)
+licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
for the dependency inventory, preserved license texts, and external-tool terms.
**You must legally obtain and be authorized to use commercial tools**, including
diff --git a/README_ru.md b/docs/readme/README_ru.md
similarity index 80%
rename from README_ru.md
rename to docs/readme/README_ru.md
index 34e7d3af2..744e26f6c 100644
--- a/README_ru.md
+++ b/docs/readme/README_ru.md
@@ -1,6 +1,6 @@
-[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · **Русский** · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · **Русский** · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Реверс-инжиниринг чего угодно
@@ -9,10 +9,10 @@
**Нашли интересную функцию? Разберитесь, как она работает, вплоть до двоичного кода.**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#current-status)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
@@ -25,7 +25,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
Присоединяйтесь к сообществу реверс-инжиниринга
Discord · Вопросы и ответы · Обмен результатами
@@ -67,7 +67,7 @@ npx rea-agents setup
Выберите агентов, проверьте предлагаемые изменения и подтвердите их. Настройка добавляет MCP-сервер REA и соответствующие рабочие инструкции, сохраняя резервные копии существующих настроек. После этого перезапустите агента.
-Настройка поддерживает Claude Code, Codex, Cursor, Gemini CLI, Grok Build и [других агентов](docs/installation.md#supported-agents). Настройка провайдеров и ручная регистрация MCP описаны в разделе [установка и настройка](docs/installation.md).
+Настройка поддерживает Claude Code, Codex, Cursor, Gemini CLI, Grok Build и [других агентов](../installation.md#supported-agents). Настройка провайдеров и ручная регистрация MCP описаны в разделе [установка и настройка](../installation.md).
### Задайте вопрос агенту
@@ -94,7 +94,7 @@ npm install --global rea-agents
rea --help
```
-Для нативного анализа сначала настройте провайдер. Нативные команды, выбор провайдера, снимки и работу со скриптами описывает [руководство по CLI и Evidence](docs/cli.md).
+Для нативного анализа сначала настройте провайдер. Нативные команды, выбор провайдера, снимки и работу со скриптами описывает [руководство по CLI и Evidence](../cli.md).
### Обновите REA
@@ -120,9 +120,9 @@ npx rea-agents@latest setup
Агент вызывает REA через MCP, чтобы исследовать цель и проследить связанный код. REA возвращает результаты вместе с подтверждающими данными. Агент использует их для уточняющих вопросов, объяснения поведения или написания и тестирования реализации. Команды CLI используют те же рабочие процессы.
-
+
-[Открыть рисунок в полном размере](website/public/assets/figures/rea-investigation-flow.svg).
+[Открыть рисунок в полном размере](../../website/public/assets/figures/rea-investigation-flow.svg).
@@ -130,37 +130,37 @@ npx rea-agents@latest setup
REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а также npm. Дополнительные инструменты и поддерживаемые системы зависят от цели:
-| Цель | Что возвращает REA | Требования и руководство |
-| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
-| Нативные бинарные файлы | Псевдокод, ассемблер, строки, символы, вызовы и ссылки | Hopper, Ghidra или IDA; [нативный анализ](https://rea.tools/ru/guides/native/) |
-| Структура ELF без запуска | Секции, сегменты, исходные символы/релокации и возможные механизмы защиты по данным статического анализа | pwntools, предоставленный вызывающей стороной, в Linux x64; [диагностика бинарных файлов](docs/binary-diagnostics.md) |
-| Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление в Linux x64; [руководство по EVM без запуска](docs/evm-bytecode.md) |
-| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](docs/recorded-crashes.md) |
-| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](https://rea.tools/ru/guides/javascript/) |
-| Сайты | Структура страницы, скрипты, наблюдения за сетью и запрошенные снимки экрана | Браузер семейства Chrome; [анализ браузера](https://rea.tools/ru/guides/browser/) |
-| Сохранённые сетевые захваты | Запросы, ответы, доступные полезные данные и позиции в источнике | HAR; mitmdump в Linux для захватов в собственном формате mitmproxy; [руководство по сетевым захватам](docs/web-network-captures.md) |
-| Сборки .NET | Метаданные, инструкции CIL, объявленные нативные зависимости и сравнения сборок | Статическое исследование; [руководство по управляемому коду](docs/managed-code-analysis.md) |
-| Android APK | Объявления манифеста, классы, декомпилированные методы и ссылки | JADX без графического интерфейса и полный JDK в Linux/macOS/Windows x64; [руководство по Android](docs/android-analysis.md) |
-| Устройства Android | Устройства, пакеты, процессы, журналы, дампы, снимки экрана и передача APK/файлов | adb, предоставленный вызывающей стороной (эмулятор или устройство); [руководство по ADB](docs/adb-device-analysis.md) |
-| Проекты JEB | Единицы проекта от движка, дайджесты артефактов и псевдокод типов/методов | Запущенный вызывающей стороной клиент JEB с интерфейсом MCP, выбирается через REA_JEB_MCP_URL; [руководство по JEB](docs/jeb-analysis.md) |
-| Ресурсы Android | Декодированный манифест, таблицы строк, языки и сведения о версии | Запуск Apktool, предоставленный вызывающей стороной; [руководство по ресурсам](docs/apktool-resource-analysis.md) |
-| Приложения Flutter | Хеш снапшота Dart, дайджесты библиотек по ABI и происхождение сборки движка | Не требуется (статический разбор APK); [руководство по Flutter](docs/flutter-analysis.md) |
-| Прошивки | Области, результаты извлечения и передача в нативный анализ | Binwalk / Unblob в Linux; [руководство по прошивкам](docs/firmware-analysis.md) |
-| Записанные пакеты | Выбранные поля, повторяющиеся значения, фильтры и правила декодирования | Linux: TShark 4.x + prlimit; [руководство по захватам](docs/packet-captures.md) |
-| Захват пакетов | Конечные точки, DNS, TLS SNI, распределение протоколов и HTTP-запросы | Предоставленные вызывающей стороной tshark и capinfos; [руководство по pcap](docs/pcap-analysis.md) |
-| Записанные логические сигналы | Аннотации декодера, диапазоны отсчётов и диагностика | Linux: sigrok-cli 0.7.2 + prlimit; [руководство по сигналам](docs/signal-recordings.md) |
-| Пакеты и ресурсы | Списки файлов, хеши, plist, структура пакетов Apple и извлечённые ресурсы | [Руководство по артефактам и JavaScript](docs/javascript-artifact-reconstruction.md), [приложения Apple](docs/apple-application-analysis.md) |
-| Поведение процессов | Вывод терминала, взаимодействия, наблюдения за завершением и файловой системой, сравнения запусков | Linux/macOS с нативным PTY; [захват процессов](docs/process-capture.md) |
+| Цель | Что возвращает REA | Требования и руководство |
+| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
+| Нативные бинарные файлы | Псевдокод, ассемблер, строки, символы, вызовы и ссылки | Hopper, Ghidra или IDA; [нативный анализ](https://rea.tools/ru/guides/native/) |
+| Структура ELF без запуска | Секции, сегменты, исходные символы/релокации и возможные механизмы защиты по данным статического анализа | pwntools, предоставленный вызывающей стороной, в Linux x64; [диагностика бинарных файлов](../binary-diagnostics.md) |
+| Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление в Linux x64; [руководство по EVM без запуска](../evm-bytecode.md) |
+| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](../recorded-crashes.md) |
+| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](https://rea.tools/ru/guides/javascript/) |
+| Сайты | Структура страницы, скрипты, наблюдения за сетью и запрошенные снимки экрана | Браузер семейства Chrome; [анализ браузера](https://rea.tools/ru/guides/browser/) |
+| Сохранённые сетевые захваты | Запросы, ответы, доступные полезные данные и позиции в источнике | HAR; mitmdump в Linux для захватов в собственном формате mitmproxy; [руководство по сетевым захватам](../web-network-captures.md) |
+| Сборки .NET | Метаданные, инструкции CIL, объявленные нативные зависимости и сравнения сборок | Статическое исследование; [руководство по управляемому коду](../managed-code-analysis.md) |
+| Android APK | Объявления манифеста, классы, декомпилированные методы и ссылки | JADX без графического интерфейса и полный JDK в Linux/macOS/Windows x64; [руководство по Android](../android-analysis.md) |
+| Устройства Android | Устройства, пакеты, процессы, журналы, дампы, снимки экрана и передача APK/файлов | adb, предоставленный вызывающей стороной (эмулятор или устройство); [руководство по ADB](../adb-device-analysis.md) |
+| Проекты JEB | Единицы проекта от движка, дайджесты артефактов и псевдокод типов/методов | Запущенный вызывающей стороной клиент JEB с интерфейсом MCP, выбирается через REA_JEB_MCP_URL; [руководство по JEB](../jeb-analysis.md) |
+| Ресурсы Android | Декодированный манифест, таблицы строк, языки и сведения о версии | Запуск Apktool, предоставленный вызывающей стороной; [руководство по ресурсам](../apktool-resource-analysis.md) |
+| Приложения Flutter | Хеш снапшота Dart, дайджесты библиотек по ABI и происхождение сборки движка | Не требуется (статический разбор APK); [руководство по Flutter](../flutter-analysis.md) |
+| Прошивки | Области, результаты извлечения и передача в нативный анализ | Binwalk / Unblob в Linux; [руководство по прошивкам](../firmware-analysis.md) |
+| Записанные пакеты | Выбранные поля, повторяющиеся значения, фильтры и правила декодирования | Linux: TShark 4.x + prlimit; [руководство по захватам](../packet-captures.md) |
+| Захват пакетов | Конечные точки, DNS, TLS SNI, распределение протоколов и HTTP-запросы | Предоставленные вызывающей стороной tshark и capinfos; [руководство по pcap](../pcap-analysis.md) |
+| Записанные логические сигналы | Аннотации декодера, диапазоны отсчётов и диагностика | Linux: sigrok-cli 0.7.2 + prlimit; [руководство по сигналам](../signal-recordings.md) |
+| Пакеты и ресурсы | Списки файлов, хеши, plist, структура пакетов Apple и извлечённые ресурсы | [Руководство по артефактам и JavaScript](../javascript-artifact-reconstruction.md), [приложения Apple](../apple-application-analysis.md) |
+| Поведение процессов | Вывод терминала, взаимодействия, наблюдения за завершением и файловой системой, сравнения запусков | Linux/macOS с нативным PTY; [захват процессов](../process-capture.md) |
Статическое исследование JavaScript и .NET читает предоставленные файлы, не запуская приложение. Захват во время выполнения запускает выбранную цель или взаимодействует с ней с правами вашего пользователя; соответствующие руководства описывают последствия.
-Нативные форматы и поддерживаемые системы различаются у разных провайдеров. См. [настройку Hopper и Ghidra](docs/installation.md#hopper), [руководство по IDA](docs/ida-provider.md) и [экспериментальную поддержку Ghidra в Windows](docs/windows-ghidra-p0.md). Ghidra также поддерживает [анализ 16-битного DOS](docs/ghidra-dos.md). Выбор провайдера описан в [руководстве по CLI](docs/cli.md#choose-a-provider). Для функций, добавленных после последней версии в npm, проверьте [доступность в релизах](docs/installation.md#released-package-and-main).
+Нативные форматы и поддерживаемые системы различаются у разных провайдеров. См. [настройку Hopper и Ghidra](../installation.md#hopper), [руководство по IDA](../ida-provider.md) и [экспериментальную поддержку Ghidra в Windows](../windows-ghidra-p0.md). Ghidra также поддерживает [анализ 16-битного DOS](../ghidra-dos.md). Выбор провайдера описан в [руководстве по CLI](../cli.md#choose-a-provider). Для функций, добавленных после последней версии в npm, проверьте [доступность в релизах](../installation.md#released-package-and-main).
## Примеры
-[](https://rea.tools/ru/showcase/)
+[](https://rea.tools/ru/showcase/)
### DX-Ball: восстановление расчёта звуковой панорамы
@@ -187,28 +187,28 @@ REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а так
Какие агенты могут использовать REA?
-Любой агент с поддержкой локальных MCP-серверов. Настройка конфигурирует [поддерживаемых агентов](docs/installation.md#supported-agents); другие клиенты могут использовать [ручную регистрацию MCP](docs/installation.md#mcp-registry).
+Любой агент с поддержкой локальных MCP-серверов. Настройка конфигурирует [поддерживаемых агентов](../installation.md#supported-agents); другие клиенты могут использовать [ручную регистрацию MCP](../installation.md#mcp-registry).
Нужны ли мне Hopper, Ghidra или IDA?
-Углублённый нативный анализ использует один из этих инструментов. Статическое исследование JavaScript и .NET работает без движка нативного анализа. Настройка может установить Hopper после подтверждения; Ghidra и IDA используют существующие установки. См. [настройку провайдеров](docs/installation.md#hopper).
+Углублённый нативный анализ использует один из этих инструментов. Статическое исследование JavaScript и .NET работает без движка нативного анализа. Настройка может установить Hopper после подтверждения; Ghidra и IDA используют существующие установки. См. [настройку провайдеров](../installation.md#hopper).
Нужно ли сначала запускать Hopper?
-REA запускает Hopper, когда он нужен для операции. В macOS при первом запуске может появиться диалог выбора деморежима или активации лицензии. См. [запуск Hopper и устранение неполадок](docs/installation.md#launcher-paths-and-troubleshooting).
+REA запускает Hopper, когда он нужен для операции. В macOS при первом запуске может появиться диалог выбора деморежима или активации лицензии. См. [запуск Hopper и устранение неполадок](../installation.md#launcher-paths-and-troubleshooting).
Что даёт установка skill с skills.sh?
-Skill предоставляет агенту инструкции по исследованию. Используйте `npx rea-agents setup`, чтобы зарегистрировать MCP-сервер REA и установить соответствующие инструкции, затем перезапустите агента. См. [установку только skill](docs/installation.md#skill-only-installation).
+Skill предоставляет агенту инструкции по исследованию. Используйте `npx rea-agents setup`, чтобы зарегистрировать MCP-сервер REA и установить соответствующие инструкции, затем перезапустите агента. См. [установку только skill](../installation.md#skill-only-installation).
@@ -251,14 +251,14 @@ npx rea-agents@latest setup
Начните с [практических руководств](https://rea.tools/ru/guides/) на сайте. Точные параметры, требования и контракты результатов описаны здесь:
-- [Установка и настройка](docs/installation.md): регистрация агентов, настройка провайдеров, обновление и удаление.
-- [Готовность и устранение неполадок](docs/installation.md#check-readiness-for-your-task): диагностика конкретного агента или движка анализа.
-- [CLI и Evidence](docs/cli.md): команды, выбор провайдера, снимки, импорт/экспорт и коды завершения.
-- [Контракты MCP](docs/mcp-contracts.md) и [промпты агентов](docs/mcp-prompts.md): результаты инструментов, сеансы и исследования с подсказками.
-- [Каталог инструментов](docs/mcp-contracts.md#generated-catalog): создаваемый при сборке перечень инструментов, провайдеров и команд CLI.
-- [План развития](docs/roadmap.md): запланированные работы и отслеживание возможностей.
+- [Установка и настройка](../installation.md): регистрация агентов, настройка провайдеров, обновление и удаление.
+- [Готовность и устранение неполадок](../installation.md#check-readiness-for-your-task): диагностика конкретного агента или движка анализа.
+- [CLI и Evidence](../cli.md): команды, выбор провайдера, снимки, импорт/экспорт и коды завершения.
+- [Контракты MCP](../mcp-contracts.md) и [промпты агентов](../mcp-prompts.md): результаты инструментов, сеансы и исследования с подсказками.
+- [Каталог инструментов](../mcp-contracts.md#generated-catalog): создаваемый при сборке перечень инструментов, провайдеров и команд CLI.
+- [План развития](../roadmap.md): запланированные работы и отслеживание возможностей.
-Сообщайте об уязвимостях согласно [SECURITY.md](SECURITY.md).
+Сообщайте об уязвимостях согласно [SECURITY.md](../../SECURITY.md).
## История звёзд
@@ -276,7 +276,7 @@ npx rea-agents@latest setup
## Отказ от ответственности
-Рекомендации по ответственному использованию приведены в [DISCLAIMER.md](DISCLAIMER.md).
+Рекомендации по ответственному использованию приведены в [DISCLAIMER.md](../../DISCLAIMER.md).
REA предназначен для законных исследований, обучения, обеспечения совместимости, сохранения программного обеспечения и анализа программ, которыми вы владеете или которые вам разрешено изучать.
@@ -288,14 +288,14 @@ applicable licenses.
Будем рады вашей помощи с REA! [Откройте issue](https://github.com/morluto/rea/issues), чтобы сообщить об ошибке или предложить функцию, либо [отправьте pull request](https://github.com/morluto/rea/pulls) с улучшениями кода или документации.
-Настройка среды разработки и проверки описаны в [CONTRIBUTING.md](CONTRIBUTING.md), направления проверки — в [руководстве по тестированию](docs/testing.md), а структура проекта — на [схеме архитектуры](docs/architecture.mermaid).
+Настройка среды разработки и проверки описаны в [CONTRIBUTING.md](../../CONTRIBUTING.md), направления проверки — в [руководстве по тестированию](../testing.md), а структура проекта — на [схеме архитектуры](../architecture.mermaid).
## Лицензия
-[MIT](LICENSE)
+[MIT](../../LICENSE)
REA-authored code is licensed under MIT. Third-party material retains its own
-licenses and copyright notices; see [Third-party notices](THIRD_PARTY_NOTICES.md)
+licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
for the dependency inventory, preserved license texts, and external-tool terms.
**You must legally obtain and be authorized to use commercial tools**, including
diff --git a/README_th.md b/docs/readme/README_th.md
similarity index 81%
rename from README_th.md
rename to docs/readme/README_th.md
index d07bbb6a3..734b28c86 100644
--- a/README_th.md
+++ b/docs/readme/README_th.md
@@ -1,6 +1,6 @@
-[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · **ไทย** · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · **ไทย** · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Reverse Engineer Anything
@@ -9,10 +9,10 @@
**เห็นฟีเจอร์ที่ชอบแล้วอยากรู้ว่าทำงานอย่างไร? เข้าใจได้ลึกถึงระดับไบนารี**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#current-status)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
@@ -25,7 +25,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
เข้าร่วมชุมชนวิศวกรรมย้อนกลับ
Discord · ถามตอบ · แบ่งปันผลงาน
@@ -67,7 +67,7 @@ npx rea-agents setup
เลือกเอเจนต์ ตรวจสอบการเปลี่ยนแปลงที่เสนอ แล้วอนุมัติ ขั้นตอนตั้งค่าจะเพิ่มเซิร์ฟเวอร์ MCP ของ REA และคำแนะนำการทำงานที่ตรงกัน พร้อมสำรองการตั้งค่าที่มีอยู่ จากนั้นเริ่มเอเจนต์ใหม่
-ขั้นตอนตั้งค่ารองรับ Claude Code, Codex, Cursor, Gemini CLI, Grok Build และ[เอเจนต์อื่น ๆ](docs/installation.md#supported-agents) ดู[การติดตั้งและตั้งค่า](docs/installation.md) สำหรับการตั้งค่าผู้ให้บริการวิเคราะห์และการลงทะเบียน MCP ด้วยตนเอง
+ขั้นตอนตั้งค่ารองรับ Claude Code, Codex, Cursor, Gemini CLI, Grok Build และ[เอเจนต์อื่น ๆ](../installation.md#supported-agents) ดู[การติดตั้งและตั้งค่า](../installation.md) สำหรับการตั้งค่าผู้ให้บริการวิเคราะห์และการลงทะเบียน MCP ด้วยตนเอง
### ถามเอเจนต์
@@ -94,7 +94,7 @@ npm install --global rea-agents
rea --help
```
-สำหรับการวิเคราะห์เนทีฟ ให้ตั้งค่าผู้ให้บริการวิเคราะห์ก่อน ดู[คู่มือ CLI และ Evidence](docs/cli.md) สำหรับคำสั่งเนทีฟ การเลือกผู้ให้บริการ สแนปช็อต และการใช้งานในสคริปต์
+สำหรับการวิเคราะห์เนทีฟ ให้ตั้งค่าผู้ให้บริการวิเคราะห์ก่อน ดู[คู่มือ CLI และ Evidence](../cli.md) สำหรับคำสั่งเนทีฟ การเลือกผู้ให้บริการ สแนปช็อต และการใช้งานในสคริปต์
### อัปเดต REA
@@ -120,9 +120,9 @@ npx rea-agents@latest setup
เอเจนต์เรียก REA ผ่าน MCP เพื่อตรวจสอบเป้าหมายและติดตามโค้ดที่เกี่ยวข้อง REA ส่งข้อค้นพบกลับมาพร้อมหลักฐาน เอเจนต์ใช้ข้อมูลนี้เพื่อถามคำถามต่อ อธิบายพฤติกรรม หรือเขียนและทดสอบการนำไปใช้งาน คำสั่ง CLI ใช้กระบวนการเดียวกัน
-
+
-[เปิดภาพขนาดเต็ม](website/public/assets/figures/rea-investigation-flow.svg)
+[เปิดภาพขนาดเต็ม](../../website/public/assets/figures/rea-investigation-flow.svg)
@@ -130,37 +130,37 @@ npx rea-agents@latest setup
REA ต้องใช้ Node.js 22.x (>=22.19), 24.x (>=24.11) หรือ 26+ และ npm เครื่องมือเพิ่มเติมและแพลตฟอร์มที่รองรับขึ้นอยู่กับเป้าหมาย:
-| เป้าหมาย | สิ่งที่ REA ส่งกลับ | ข้อกำหนดและคู่มือ |
-| ---------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
-| ไบนารีเนทีฟ | โค้ดเทียม แอสเซมบลี สตริง สัญลักษณ์ การเรียก และการอ้างอิง | Hopper, Ghidra หรือ IDA; [การวิเคราะห์เนทีฟ](https://rea.tools/th/guides/native/) |
-| โครงสร้าง ELF แบบออฟไลน์ | เซกชัน เซกเมนต์ สัญลักษณ์/รีโลเคชันต้นฉบับ และตัวเลือกกลไกป้องกันที่พบจากการวิเคราะห์แบบสถิต | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; [การวินิจฉัยไบนารี](docs/binary-diagnostics.md) |
-| ไบต์โค้ด EVM | ตัวเลือกฟังก์ชันสำหรับการเรียก ออฟเซ็ตไบต์ อาร์กิวเมนต์และความสามารถในการเปลี่ยนสถานะที่อนุมานได้ | ไฟล์บนเครื่องที่มีไบต์ดิบหรือข้อมูลเลขฐานสิบหกบน Linux x64; [คู่มือ EVM แบบออฟไลน์](docs/evm-bytecode.md) |
-| ข้อมูลการแครชของ Linux ที่บันทึกไว้ | ระเบียน note ดิบ รีจิสเตอร์/สัญญาณของทุกเธรดที่บันทึกไว้ และตัวเลือกการแมปเพิ่มเติม | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; เลือกใช้ GDB/pwndbg เพิ่มเติมได้; [การแครชที่บันทึกไว้](docs/recorded-crashes.md) |
-| JavaScript / Electron | โมดูล การนำเข้า ซอร์สแมป เส้นทาง IPC และความสัมพันธ์กับส่วนเสริมเนทีฟ | Node.js และ npm; [การวิเคราะห์แอปพลิเคชัน](https://rea.tools/th/guides/javascript/) |
-| เว็บไซต์ | โครงสร้างหน้า สคริปต์ ข้อมูลเครือข่ายที่สังเกตได้ และภาพหน้าจอที่ร้องขอ | เบราว์เซอร์ในตระกูล Chrome; [การวิเคราะห์ผ่านเบราว์เซอร์](https://rea.tools/th/guides/browser/) |
-| ข้อมูลเครือข่ายที่บันทึกไว้ | คำขอ คำตอบ เพย์โหลดที่ปรากฏ และตำแหน่งต้นทาง | HAR; mitmdump บน Linux สำหรับข้อมูลที่บันทึกในรูปแบบเนทีฟของ mitmproxy; [คู่มือการบันทึกข้อมูล](docs/web-network-captures.md) |
-| การจับแพ็กเก็ต | จุดเชื่อมต่อ, DNS, TLS SNI, การกระจายโปรโตคอล และคำขอ HTTP | tshark และ capinfos ที่ผู้เรียกจัดเตรียม; [คู่มือ pcap](docs/pcap-analysis.md) |
-| แอสเซมบลี .NET | เมทาดาทา คำสั่ง CIL การพึ่งพาเนทีฟที่ประกาศไว้ และการเปรียบเทียบผลการบิลด์ | การตรวจสอบแบบสถิต; [คู่มือโค้ดแบบจัดการ](docs/managed-code-analysis.md) |
-| APK ของ Android | การประกาศใน manifest คลาส เมธอดที่ดีคอมไพล์ และการอ้างอิง | JADX แบบไม่มี GUI และ JDK แบบเต็มบน Linux/macOS/Windows x64; [คู่มือ Android](docs/android-analysis.md) |
-| อุปกรณ์ Android | อุปกรณ์ แพ็กเกจ โพรเซส บันทึก ดัมพ์ ภาพหน้าจอ และการถ่ายโอน APK/ไฟล์ | adb ที่ผู้เรียกจัดเตรียม (โปรแกรมจำลองหรืออุปกรณ์จริง); [คู่มือ ADB](docs/adb-device-analysis.md) |
-| โปรเจกต์ JEB | หน่วยโปรเจกต์จากเอนจิน, แฮชของอาร์ติแฟกต์ และซูโดโค้ดของไทป์/เมท็อด | ไคลเอนต์ JEB ที่ผู้เรียกเริ่มและให้บริการ MCP เลือกด้วย REA_JEB_MCP_URL; [คู่มือ JEB](docs/jeb-analysis.md) |
-| ทรัพยากร Android | ไฟล์ manifest ที่ถอดรหัสแล้ว ตารางสตริง ภาษา และข้อมูลเวอร์ชัน | ตัวเรียก Apktool ที่ผู้เรียกจัดเตรียม; [คู่มือทรัพยากร](docs/apktool-resource-analysis.md) |
-| แอป Flutter | แฮชสแนปชอต Dart, แฮชไลบรารีราย ABI และที่มาของบิลด์เอนจิน | ไม่ต้อง (วิเคราะห์ APK แบบ static); [คู่มือ Flutter](docs/flutter-analysis.md) |
-| เฟิร์มแวร์ | พื้นที่ข้อมูล ผลการแตกไฟล์ และการส่งต่อไปวิเคราะห์เนทีฟ | Binwalk / Unblob บน Linux; [คู่มือเฟิร์มแวร์](docs/firmware-analysis.md) |
-| แพ็กเก็ตที่บันทึกไว้ | ฟิลด์ที่เลือก ค่าซ้ำ ตัวกรอง และกฎการถอดรหัส | Linux: TShark 4.x + prlimit; [คู่มือไฟล์แคปเจอร์](docs/packet-captures.md) |
-| สัญญาณลอจิกที่บันทึกไว้ | คำอธิบายจากตัวถอดรหัส ช่วงตัวอย่าง และข้อมูลวินิจฉัย | Linux: sigrok-cli 0.7.2 + prlimit; [คู่มือสัญญาณ](docs/signal-recordings.md) |
-| แพ็กเกจและทรัพยากร | รายการไฟล์ ค่าแฮช plist โครงสร้างบันเดิลของ Apple และทรัพยากรที่แตกออกมา | [คู่มืออาร์ติแฟกต์และ JavaScript](docs/javascript-artifact-reconstruction.md), [แอปพลิเคชัน Apple](docs/apple-application-analysis.md) |
-| พฤติกรรมของโปรเซส | เอาต์พุตเทอร์มินัล การโต้ตอบ ข้อมูลการสิ้นสุดและระบบไฟล์ที่สังเกตได้ และการเปรียบเทียบการรัน | Linux/macOS ที่มี PTY เนทีฟ; [การบันทึกโปรเซส](docs/process-capture.md) |
+| เป้าหมาย | สิ่งที่ REA ส่งกลับ | ข้อกำหนดและคู่มือ |
+| ---------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
+| ไบนารีเนทีฟ | โค้ดเทียม แอสเซมบลี สตริง สัญลักษณ์ การเรียก และการอ้างอิง | Hopper, Ghidra หรือ IDA; [การวิเคราะห์เนทีฟ](https://rea.tools/th/guides/native/) |
+| โครงสร้าง ELF แบบออฟไลน์ | เซกชัน เซกเมนต์ สัญลักษณ์/รีโลเคชันต้นฉบับ และตัวเลือกกลไกป้องกันที่พบจากการวิเคราะห์แบบสถิต | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; [การวินิจฉัยไบนารี](../binary-diagnostics.md) |
+| ไบต์โค้ด EVM | ตัวเลือกฟังก์ชันสำหรับการเรียก ออฟเซ็ตไบต์ อาร์กิวเมนต์และความสามารถในการเปลี่ยนสถานะที่อนุมานได้ | ไฟล์บนเครื่องที่มีไบต์ดิบหรือข้อมูลเลขฐานสิบหกบน Linux x64; [คู่มือ EVM แบบออฟไลน์](../evm-bytecode.md) |
+| ข้อมูลการแครชของ Linux ที่บันทึกไว้ | ระเบียน note ดิบ รีจิสเตอร์/สัญญาณของทุกเธรดที่บันทึกไว้ และตัวเลือกการแมปเพิ่มเติม | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; เลือกใช้ GDB/pwndbg เพิ่มเติมได้; [การแครชที่บันทึกไว้](../recorded-crashes.md) |
+| JavaScript / Electron | โมดูล การนำเข้า ซอร์สแมป เส้นทาง IPC และความสัมพันธ์กับส่วนเสริมเนทีฟ | Node.js และ npm; [การวิเคราะห์แอปพลิเคชัน](https://rea.tools/th/guides/javascript/) |
+| เว็บไซต์ | โครงสร้างหน้า สคริปต์ ข้อมูลเครือข่ายที่สังเกตได้ และภาพหน้าจอที่ร้องขอ | เบราว์เซอร์ในตระกูล Chrome; [การวิเคราะห์ผ่านเบราว์เซอร์](https://rea.tools/th/guides/browser/) |
+| ข้อมูลเครือข่ายที่บันทึกไว้ | คำขอ คำตอบ เพย์โหลดที่ปรากฏ และตำแหน่งต้นทาง | HAR; mitmdump บน Linux สำหรับข้อมูลที่บันทึกในรูปแบบเนทีฟของ mitmproxy; [คู่มือการบันทึกข้อมูล](../web-network-captures.md) |
+| การจับแพ็กเก็ต | จุดเชื่อมต่อ, DNS, TLS SNI, การกระจายโปรโตคอล และคำขอ HTTP | tshark และ capinfos ที่ผู้เรียกจัดเตรียม; [คู่มือ pcap](../pcap-analysis.md) |
+| แอสเซมบลี .NET | เมทาดาทา คำสั่ง CIL การพึ่งพาเนทีฟที่ประกาศไว้ และการเปรียบเทียบผลการบิลด์ | การตรวจสอบแบบสถิต; [คู่มือโค้ดแบบจัดการ](../managed-code-analysis.md) |
+| APK ของ Android | การประกาศใน manifest คลาส เมธอดที่ดีคอมไพล์ และการอ้างอิง | JADX แบบไม่มี GUI และ JDK แบบเต็มบน Linux/macOS/Windows x64; [คู่มือ Android](../android-analysis.md) |
+| อุปกรณ์ Android | อุปกรณ์ แพ็กเกจ โพรเซส บันทึก ดัมพ์ ภาพหน้าจอ และการถ่ายโอน APK/ไฟล์ | adb ที่ผู้เรียกจัดเตรียม (โปรแกรมจำลองหรืออุปกรณ์จริง); [คู่มือ ADB](../adb-device-analysis.md) |
+| โปรเจกต์ JEB | หน่วยโปรเจกต์จากเอนจิน, แฮชของอาร์ติแฟกต์ และซูโดโค้ดของไทป์/เมท็อด | ไคลเอนต์ JEB ที่ผู้เรียกเริ่มและให้บริการ MCP เลือกด้วย REA_JEB_MCP_URL; [คู่มือ JEB](../jeb-analysis.md) |
+| ทรัพยากร Android | ไฟล์ manifest ที่ถอดรหัสแล้ว ตารางสตริง ภาษา และข้อมูลเวอร์ชัน | ตัวเรียก Apktool ที่ผู้เรียกจัดเตรียม; [คู่มือทรัพยากร](../apktool-resource-analysis.md) |
+| แอป Flutter | แฮชสแนปชอต Dart, แฮชไลบรารีราย ABI และที่มาของบิลด์เอนจิน | ไม่ต้อง (วิเคราะห์ APK แบบ static); [คู่มือ Flutter](../flutter-analysis.md) |
+| เฟิร์มแวร์ | พื้นที่ข้อมูล ผลการแตกไฟล์ และการส่งต่อไปวิเคราะห์เนทีฟ | Binwalk / Unblob บน Linux; [คู่มือเฟิร์มแวร์](../firmware-analysis.md) |
+| แพ็กเก็ตที่บันทึกไว้ | ฟิลด์ที่เลือก ค่าซ้ำ ตัวกรอง และกฎการถอดรหัส | Linux: TShark 4.x + prlimit; [คู่มือไฟล์แคปเจอร์](../packet-captures.md) |
+| สัญญาณลอจิกที่บันทึกไว้ | คำอธิบายจากตัวถอดรหัส ช่วงตัวอย่าง และข้อมูลวินิจฉัย | Linux: sigrok-cli 0.7.2 + prlimit; [คู่มือสัญญาณ](../signal-recordings.md) |
+| แพ็กเกจและทรัพยากร | รายการไฟล์ ค่าแฮช plist โครงสร้างบันเดิลของ Apple และทรัพยากรที่แตกออกมา | [คู่มืออาร์ติแฟกต์และ JavaScript](../javascript-artifact-reconstruction.md), [แอปพลิเคชัน Apple](../apple-application-analysis.md) |
+| พฤติกรรมของโปรเซส | เอาต์พุตเทอร์มินัล การโต้ตอบ ข้อมูลการสิ้นสุดและระบบไฟล์ที่สังเกตได้ และการเปรียบเทียบการรัน | Linux/macOS ที่มี PTY เนทีฟ; [การบันทึกโปรเซส](../process-capture.md) |
การตรวจสอบ JavaScript และ .NET แบบสถิตอ่านไฟล์ที่ให้มาโดยไม่รันแอปพลิเคชัน การบันทึกขณะทำงานจะรันหรือโต้ตอบกับเป้าหมายที่เลือกโดยใช้สิทธิ์ของผู้ใช้คุณ คู่มือการทำงานแต่ละฉบับอธิบายผลที่เกิดขึ้นไว้
-รูปแบบไบนารีเนทีฟและแพลตฟอร์มที่รองรับแตกต่างกันตามผู้ให้บริการวิเคราะห์ ดู[การตั้งค่า Hopper และ Ghidra](docs/installation.md#hopper), [คู่มือ IDA](docs/ida-provider.md) และ[การรองรับ Ghidra บน Windows แบบทดลอง](docs/windows-ghidra-p0.md) Ghidra ยังรองรับ[การวิเคราะห์ DOS แบบ 16 บิต](docs/ghidra-dos.md) สำหรับการเลือกผู้ให้บริการ ดู[คู่มือ CLI](docs/cli.md#choose-a-provider) ตรวจสอบ[ความพร้อมใช้งานในแต่ละรุ่น](docs/installation.md#released-package-and-main) สำหรับฟีเจอร์ที่เพิ่มหลังรุ่นล่าสุดบน npm
+รูปแบบไบนารีเนทีฟและแพลตฟอร์มที่รองรับแตกต่างกันตามผู้ให้บริการวิเคราะห์ ดู[การตั้งค่า Hopper และ Ghidra](../installation.md#hopper), [คู่มือ IDA](../ida-provider.md) และ[การรองรับ Ghidra บน Windows แบบทดลอง](../windows-ghidra-p0.md) Ghidra ยังรองรับ[การวิเคราะห์ DOS แบบ 16 บิต](../ghidra-dos.md) สำหรับการเลือกผู้ให้บริการ ดู[คู่มือ CLI](../cli.md#choose-a-provider) ตรวจสอบ[ความพร้อมใช้งานในแต่ละรุ่น](../installation.md#released-package-and-main) สำหรับฟีเจอร์ที่เพิ่มหลังรุ่นล่าสุดบน npm
## ตัวอย่างการใช้งาน
-[](https://rea.tools/th/showcase/)
+[](https://rea.tools/th/showcase/)
### DX-Ball: สร้างการคำนวณแพนเสียงขึ้นใหม่
@@ -187,28 +187,28 @@ REA ต้องใช้ Node.js 22.x (>=22.19), 24.x (>=24.11) หรือ 26
เอเจนต์ใดใช้ REA ได้บ้าง?
-เอเจนต์ใดก็ตามที่รองรับเซิร์ฟเวอร์ MCP บนเครื่อง ขั้นตอนตั้งค่าจะกำหนดค่าให้กับ[เอเจนต์ที่รองรับ](docs/installation.md#supported-agents) ส่วนไคลเอนต์อื่นใช้[การลงทะเบียน MCP ด้วยตนเอง](docs/installation.md#mcp-registry) ได้
+เอเจนต์ใดก็ตามที่รองรับเซิร์ฟเวอร์ MCP บนเครื่อง ขั้นตอนตั้งค่าจะกำหนดค่าให้กับ[เอเจนต์ที่รองรับ](../installation.md#supported-agents) ส่วนไคลเอนต์อื่นใช้[การลงทะเบียน MCP ด้วยตนเอง](../installation.md#mcp-registry) ได้
จำเป็นต้องมี Hopper, Ghidra หรือ IDA หรือไม่?
-การวิเคราะห์เนทีฟเชิงลึกต้องใช้หนึ่งในเครื่องมือเหล่านี้ การตรวจสอบ JavaScript และ .NET แบบสถิตทำงานได้โดยไม่มีเอนจินวิเคราะห์เนทีฟ ขั้นตอนตั้งค่าสามารถติดตั้ง Hopper หลังได้รับอนุมัติ ส่วน Ghidra และ IDA ใช้การติดตั้งที่คุณมีอยู่แล้ว ดู[การตั้งค่าผู้ให้บริการวิเคราะห์](docs/installation.md#hopper)
+การวิเคราะห์เนทีฟเชิงลึกต้องใช้หนึ่งในเครื่องมือเหล่านี้ การตรวจสอบ JavaScript และ .NET แบบสถิตทำงานได้โดยไม่มีเอนจินวิเคราะห์เนทีฟ ขั้นตอนตั้งค่าสามารถติดตั้ง Hopper หลังได้รับอนุมัติ ส่วน Ghidra และ IDA ใช้การติดตั้งที่คุณมีอยู่แล้ว ดู[การตั้งค่าผู้ให้บริการวิเคราะห์](../installation.md#hopper)
ต้องเริ่ม Hopper ก่อนหรือไม่?
-REA เริ่ม Hopper เมื่อการดำเนินการต้องใช้ บน macOS อาจมีหน้าต่างเมื่อรันครั้งแรกให้เลือกโหมดทดลองหรือเปิดใช้ใบอนุญาต ดู[การเริ่ม Hopper และการแก้ไขปัญหา](docs/installation.md#launcher-paths-and-troubleshooting)
+REA เริ่ม Hopper เมื่อการดำเนินการต้องใช้ บน macOS อาจมีหน้าต่างเมื่อรันครั้งแรกให้เลือกโหมดทดลองหรือเปิดใช้ใบอนุญาต ดู[การเริ่ม Hopper และการแก้ไขปัญหา](../installation.md#launcher-paths-and-troubleshooting)
การติดตั้งสกิลจาก skills.sh ทำอะไร?
-สกิลให้คำแนะนำการตรวจสอบแก่เอเจนต์ ใช้ `npx rea-agents setup` เพื่อลงทะเบียนเซิร์ฟเวอร์ MCP ของ REA และติดตั้งคำแนะนำที่ตรงกัน แล้วเริ่มเอเจนต์ใหม่ ดู[การติดตั้งเฉพาะสกิล](docs/installation.md#skill-only-installation)
+สกิลให้คำแนะนำการตรวจสอบแก่เอเจนต์ ใช้ `npx rea-agents setup` เพื่อลงทะเบียนเซิร์ฟเวอร์ MCP ของ REA และติดตั้งคำแนะนำที่ตรงกัน แล้วเริ่มเอเจนต์ใหม่ ดู[การติดตั้งเฉพาะสกิล](../installation.md#skill-only-installation)
@@ -251,14 +251,14 @@ npx rea-agents@latest setup
เริ่มจาก[คู่มือพร้อมตัวอย่าง](https://rea.tools/th/guides/) บนเว็บไซต์ สำหรับตัวเลือก ข้อกำหนดเบื้องต้น และสัญญาผลลัพธ์ที่แน่นอน:
-- [การติดตั้งและตั้งค่า](docs/installation.md): การลงทะเบียนเอเจนต์ การตั้งค่าผู้ให้บริการ การอัปเดต และการถอนการติดตั้ง
-- [ความพร้อมใช้งานและการแก้ไขปัญหา](docs/installation.md#check-readiness-for-your-task): ตรวจวินิจฉัยเอเจนต์หรือเอนจินวิเคราะห์หนึ่งตัว
-- [CLI และ Evidence](docs/cli.md): คำสั่ง การเลือกผู้ให้บริการ สแนปช็อต การนำเข้า/ส่งออก และรหัสการสิ้นสุด
-- [สัญญา MCP](docs/mcp-contracts.md) และ[พรอมป์ต์สำหรับเอเจนต์](docs/mcp-prompts.md): ผลลัพธ์เครื่องมือ เซสชัน และการตรวจสอบตามคำแนะนำ
-- [แค็ตตาล็อกเครื่องมือ](docs/mcp-contracts.md#generated-catalog): รายการเครื่องมือ ผู้ให้บริการ และคำสั่ง CLI ที่สร้างระหว่างการบิลด์
-- [แผนพัฒนา](docs/roadmap.md): งานที่วางแผนไว้และรายการติดตามความสามารถ
+- [การติดตั้งและตั้งค่า](../installation.md): การลงทะเบียนเอเจนต์ การตั้งค่าผู้ให้บริการ การอัปเดต และการถอนการติดตั้ง
+- [ความพร้อมใช้งานและการแก้ไขปัญหา](../installation.md#check-readiness-for-your-task): ตรวจวินิจฉัยเอเจนต์หรือเอนจินวิเคราะห์หนึ่งตัว
+- [CLI และ Evidence](../cli.md): คำสั่ง การเลือกผู้ให้บริการ สแนปช็อต การนำเข้า/ส่งออก และรหัสการสิ้นสุด
+- [สัญญา MCP](../mcp-contracts.md) และ[พรอมป์ต์สำหรับเอเจนต์](../mcp-prompts.md): ผลลัพธ์เครื่องมือ เซสชัน และการตรวจสอบตามคำแนะนำ
+- [แค็ตตาล็อกเครื่องมือ](../mcp-contracts.md#generated-catalog): รายการเครื่องมือ ผู้ให้บริการ และคำสั่ง CLI ที่สร้างระหว่างการบิลด์
+- [แผนพัฒนา](../roadmap.md): งานที่วางแผนไว้และรายการติดตามความสามารถ
-รายงานช่องโหว่ผ่าน [SECURITY.md](SECURITY.md)
+รายงานช่องโหว่ผ่าน [SECURITY.md](../../SECURITY.md)
## ประวัติดาว
@@ -276,7 +276,7 @@ npx rea-agents@latest setup
## ข้อสงวนสิทธิ์
-ดูแนวทางการใช้งานอย่างรับผิดชอบใน [DISCLAIMER.md](DISCLAIMER.md)
+ดูแนวทางการใช้งานอย่างรับผิดชอบใน [DISCLAIMER.md](../../DISCLAIMER.md)
REA มีไว้สำหรับการวิจัย การศึกษา การทำงานร่วมกัน การอนุรักษ์ซอฟต์แวร์ และการวิเคราะห์ซอฟต์แวร์ที่คุณเป็นเจ้าของหรือได้รับอนุญาตให้ตรวจสอบ โดยปฏิบัติตามกฎหมาย
@@ -288,14 +288,14 @@ applicable licenses.
เรายินดีรับความช่วยเหลือในการพัฒนา REA! [เปิด issue](https://github.com/morluto/rea/issues) เพื่อรายงานบั๊กหรือเสนอฟีเจอร์ หรือ[ส่ง pull request](https://github.com/morluto/rea/pulls) เพื่อปรับปรุงโค้ดหรือเอกสาร
-ดู [CONTRIBUTING.md](CONTRIBUTING.md) สำหรับการตั้งค่าสภาพแวดล้อมพัฒนาและการตรวจสอบ ดู[การทดสอบ](docs/testing.md) สำหรับแนวทางการตรวจยืนยัน และ[แผนผังสถาปัตยกรรม](docs/architecture.mermaid) สำหรับโครงสร้างโปรเจกต์
+ดู [CONTRIBUTING.md](../../CONTRIBUTING.md) สำหรับการตั้งค่าสภาพแวดล้อมพัฒนาและการตรวจสอบ ดู[การทดสอบ](../testing.md) สำหรับแนวทางการตรวจยืนยัน และ[แผนผังสถาปัตยกรรม](../architecture.mermaid) สำหรับโครงสร้างโปรเจกต์
## สัญญาอนุญาต
-[MIT](LICENSE)
+[MIT](../../LICENSE)
REA-authored code is licensed under MIT. Third-party material retains its own
-licenses and copyright notices; see [Third-party notices](THIRD_PARTY_NOTICES.md)
+licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
for the dependency inventory, preserved license texts, and external-tool terms.
**You must legally obtain and be authorized to use commercial tools**, including
diff --git a/README_tr.md b/docs/readme/README_tr.md
similarity index 65%
rename from README_tr.md
rename to docs/readme/README_tr.md
index 1daaaf9d0..152c81819 100644
--- a/README_tr.md
+++ b/docs/readme/README_tr.md
@@ -1,6 +1,6 @@
-[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · **Türkçe** · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · **Türkçe** · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Her şeyi tersine mühendislikle inceleyin
@@ -9,10 +9,10 @@
**Beğendiğiniz bir özellik mi gördünüz? İkili kod düzeyine kadar nasıl çalıştığını anlayın.**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#current-status)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
@@ -25,7 +25,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
Tersine mühendislik topluluğuna katılın
Discord · Soru-cevap · Sonuç paylaşımı
@@ -51,7 +51,7 @@ Bir uygulamada kendi ürününüze eklemek istediğiniz bir özellik mi gördün
REA, ajanınızı yerel makine kodu içeren ikili dosyaları, JavaScript ve Electron uygulamalarını, .NET derlemelerini ve web sitelerini inceleyen araçlara bağlar. Aynı araçları terminalden de kullanabilirsiniz. Analiz yerel olarak çalışır; sonuçlar her çıkarımın dayandığı kanıtları ve sınırlamaları içerir.
-Kurulum, REA'yı ajanınıza kaydeder ve uyumlu iş akışı talimatlarını yükler. Yerel makine kodu analizi mevcut bir Hopper veya Ghidra kurulumunu kullanabilir; kurulum işlemi, onayınızla isteğe bağlı olarak Hopper da yükleyebilir. Statik JavaScript analizi bu iki motora da ihtiyaç duymaz.
+Kurulum, REA'yı ajanınıza kaydeder ve uyumlu iş akışı talimatlarını yükler. Yerel makine kodu analizi mevcut bir Hopper, Ghidra veya IDA kurulumunu kullanabilir; kurulum işlemi, onayınızla isteğe bağlı olarak Hopper da yükleyebilir. Statik JavaScript analizi bu yerel analiz motorlarından hiçbirine ihtiyaç duymaz.
> Kurulum talimatları, görselli rehberler ve gerçek örnek çalışmalar için **[REA web sitesini ziyaret edin](https://rea.tools/tr/)**.
@@ -67,7 +67,7 @@ npx rea-agents setup
Ajanlarınızı seçin, önerilen değişiklikleri inceleyin ve onaylayın. Kurulum, mevcut yapılandırmaları yedekleyerek REA'nın MCP sunucusunu ve uyumlu iş akışı talimatlarını ekler. Ardından ajanınızı yeniden başlatın.
-Kurulum Claude Code, Codex, Cursor, Gemini CLI, Grok Build ve [diğer ajanları](docs/installation.md#supported-agents) destekler. Sağlayıcı yapılandırması ve elle MCP kaydı için [yükleme ve kurulum](docs/installation.md) belgesine bakın.
+Kurulum Claude Code, Codex, Cursor, Gemini CLI, Grok Build ve [diğer ajanları](../installation.md#supported-agents) destekler. Sağlayıcı yapılandırması ve elle MCP kaydı için [yükleme ve kurulum](../installation.md) belgesine bakın.
### Ajanınıza sorun
@@ -94,18 +94,20 @@ npm install --global rea-agents
rea --help
```
-Yerel makine kodu analizi için önce bir sağlayıcı yapılandırın. Yerel analiz komutları, sağlayıcı seçimi, anlık görüntüler ve betik kullanımı için [CLI ve Evidence rehberine](docs/cli.md) bakın.
+Yerel makine kodu analizi için önce bir sağlayıcı yapılandırın. Yerel analiz komutları, sağlayıcı seçimi, anlık görüntüler ve betik kullanımı için [CLI ve Evidence rehberine](../cli.md) bakın.
### REA'yı güncelleyin
REA hızla değişir ve yeni sürümler sık sık hata düzeltmeleri içerir. Kurulumunuzu güncel tutun.
-npm ile yüklenen CLI için:
+`npm install --global` veya `bun add --global` ile yüklenen CLI için:
```bash
rea update
```
+Bun ile yapılan global güncellemeler için Bun 1.2.17 veya üzeri gerekir. Daha eski bir Bun sürümünü kullanıyorsanız `rea update` komutunu çalıştırmadan önce Bun'u manuel olarak yükseltin.
+
Ajan kayıtlarını ve skill'i güncellemek için güncellemenin çıktısında gösterilen kurulum komutunu çalıştırın.
`npx` kullanıyorsanız ajan kurulumunu şu komutla güncelleyin:
@@ -114,15 +116,15 @@ Ajan kayıtlarını ve skill'i güncellemek için güncellemenin çıktısında
npx rea-agents@latest setup
```
-Kurulum değişikliklerini inceleyin ve ajanınızı yeniden başlatın. Tek seferlik CLI işlemleri için `npx rea-agents@latest` komutunun ardından istediğiniz komutu yazın.
+Kurulum değişikliklerini inceleyin ve ajanınızı yeniden başlatın. Tek seferlik CLI işlemleri için `npx rea-agents@latest` komutunun ardından istediğiniz komutu yazın. Bun kullanıyorsanız `npx rea-agents@latest` yerine `bunx rea-agents@latest` kullanın.
## REA nasıl çalışır
Ajanınız, hedefi incelemek ve ilgili kodu izlemek için MCP üzerinden REA'yı çağırır. REA bulguları kanıtlarıyla birlikte döndürür. Ajan bunları takip soruları sormak, davranışı açıklamak veya bir uygulama yazıp test etmek için kullanır. CLI komutları aynı iş akışlarını kullanır.
-
+
-[Şekli tam boyutuyla açın](website/public/assets/figures/rea-investigation-flow.svg).
+[Şekli tam boyutuyla açın](../../website/public/assets/figures/rea-investigation-flow.svg).
@@ -130,37 +132,40 @@ Ajanınız, hedefi incelemek ve ilgili kodu izlemek için MCP üzerinden REA'yı
REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araçlar ve desteklenen ana bilgisayar sistemleri hedefe bağlıdır:
-| Hedef | REA'nın döndürdükleri | Gereksinimler ve rehber |
-| --------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
-| Yerel makine kodu içeren ikili dosyalar | Sözde kod, assembly kodu, dizeler, semboller, çağrılar ve referanslar | Hopper, Ghidra veya IDA; [yerel kod analizi](https://rea.tools/tr/guides/native/) |
-| Çevrimdışı ELF yapısı | Bölümler, segmentler, özgün semboller/yeniden konumlandırmalar ve statik analizden olası güvenlik önlemleri | Linux x64 üzerinde çağıranın sağladığı pwntools; [ikili dosya tanılama](docs/binary-diagnostics.md) |
-| EVM bayt kodu | Yönlendirme seçicileri, bayt ofsetleri, çıkarımla belirlenen argümanlar ve durum değiştirilebilirliği | Linux x64 üzerinde yerel ham bayt/onaltılık girdi; [çevrimdışı EVM rehberi](docs/evm-bytecode.md) |
-| Kaydedilmiş Linux çökmeleri | Ham note kayıtları, kaydedilen her iş parçacığının yazmaçları/sinyalleri ve isteğe bağlı eşleme adayları | Linux x64 üzerinde çağıranın sağladığı pwntools; isteğe bağlı GDB/pwndbg; [kaydedilmiş çökmeler](docs/recorded-crashes.md) |
-| JavaScript / Electron | Modüller, içe aktarmalar, kaynak haritaları, rotalar, IPC ve yerel eklenti ilişkileri | Node.js ve npm; [uygulama analizi](https://rea.tools/tr/guides/javascript/) |
-| Web siteleri | Sayfa yapısı, betikler, ağ gözlemleri ve istenen ekran görüntüleri | Chrome ailesinden bir tarayıcı; [tarayıcı analizi](https://rea.tools/tr/guides/browser/) |
-| Kaydedilmiş ağ yakalamaları | İstekler, yanıtlar, erişilebilir yük içerikleri ve kaynak konumları | HAR; mitmproxy'nin kendi biçimindeki yakalamalar için Linux üzerinde mitmdump; [ağ yakalama rehberi](docs/web-network-captures.md) |
-| Paket yakalamaları | Uç noktalar, DNS, TLS SNI, protokol dağılımı ve HTTP istekleri | Çağıranın sağladığı tshark ve capinfos; [pcap rehberi](docs/pcap-analysis.md) |
-| .NET derlemeleri | Meta veriler, CIL talimatları, bildirilmiş yerel bağımlılıklar ve derleme karşılaştırmaları | Statik inceleme; [yönetilen kod rehberi](docs/managed-code-analysis.md) |
-| Android APK'leri | Manifest bildirimleri, sınıflar, tersine derlenmiş metotlar ve referanslar | Linux/macOS/Windows x64 üzerinde arayüzsüz JADX ve tam bir JDK; [Android rehberi](docs/android-analysis.md) |
-| Android cihazları | Cihazlar, paketler, süreçler, günlükler, dökümler, ekran görüntüleri ve APK/dosya aktarımları | Çağıranın sağladığı adb (emülatör veya cihaz); [ADB rehberi](docs/adb-device-analysis.md) |
-| JEB projeleri | Motor destekli proje birimleri, artefakt özetleri ve tür/metot sözdekodu | Çağıranın başlattığı, MCP sunan bir JEB istemcisi, REA_JEB_MCP_URL ile seçilir; [JEB rehberi](docs/jeb-analysis.md) |
-| Android kaynakları | Çözülmüş manifest, dize tabloları, diller ve sürüm bilgileri | Çağıranın sağladığı Apktool başlatıcısı; [kaynak rehberi](docs/apktool-resource-analysis.md) |
-| Flutter uygulamaları | Dart anlık görüntü karması, ABI başına kitaplık özetleri ve motor derleme kökeni | Yok (statik APK çözümleme); [Flutter rehberi](docs/flutter-analysis.md) |
-| Donanım yazılımı | Bölgeler, çıkarma sonuçları ve yerel kod analizine devirler | Linux üzerinde Binwalk / Unblob; [donanım yazılımı rehberi](docs/firmware-analysis.md) |
-| Kaydedilmiş paketler | Seçilen alanlar, yinelenen değerler, filtreler ve çözümleme kuralları | Linux: TShark 4.x + prlimit; [yakalama kılavuzu](docs/packet-captures.md) |
-| Kaydedilmiş mantık sinyalleri | Kod çözücü açıklamaları, örnek aralıkları ve tanılama | Linux: sigrok-cli 0.7.2 + prlimit; [sinyal kılavuzu](docs/signal-recordings.md) |
-| Paketler ve kaynaklar | Dosya envanterleri, özet değerleri, plist'ler, Apple paket yapısı ve çıkarılmış kaynaklar | [Artefakt ve JavaScript rehberi](docs/javascript-artifact-reconstruction.md), [Apple uygulamaları](docs/apple-application-analysis.md) |
-| Süreç davranışı | Terminal çıktısı, etkileşimler, sonlanma ve dosya sistemi gözlemleri, çalıştırma karşılaştırmaları | Yerel PTY ile Linux/macOS; [süreç yakalama](docs/process-capture.md) |
+| Hedef | REA'nın döndürdükleri | Gereksinimler ve rehber |
+| --------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
+| Yerel makine kodu içeren ikili dosyalar | Sözde kod, assembly kodu, dizeler, semboller, çağrılar ve referanslar | Hopper, Ghidra veya IDA; [yerel kod analizi](https://rea.tools/tr/guides/native/) |
+| Çevrimdışı ELF yapısı | Bölümler, segmentler, özgün semboller/yeniden konumlandırmalar ve statik analizden olası güvenlik önlemleri | Linux x64 üzerinde çağıranın sağladığı pwntools; [ikili dosya tanılama](../binary-diagnostics.md) |
+| Go ikili dosyaları | Gömülü compiler sürümü, modüller, replacement'lar ve build ayarları | Yerleşik statik ELF/PE/thin Mach-O incelemesi; Go kurulumu gerekmez |
+| WASM artefaktı | Doğrulama, bölümler, tam import/export WAT biçimleri ve decode edilmiş WAT | Çağıranın sağladığı WABT 1.0.42; `inspect_wasm_artifact` |
+| EVM bayt kodu | Yönlendirme seçicileri, bayt ofsetleri, çıkarımla belirlenen argümanlar ve durum değiştirilebilirliği | Linux x64 üzerinde yerel ham bayt/onaltılık girdi; [çevrimdışı EVM rehberi](../evm-bytecode.md) |
+| SQLite veritabanları | Tablolar, sütunlar, index'ler, view'lar, trigger'lar ve seçilen sıradan tablo satırları | Yerel database/WAL snapshot'ı; native SQLite sınırlarıyla Node.js 24.x >=24.15 veya 26+ |
+| Kaydedilmiş Linux çökmeleri | Ham note kayıtları, kaydedilen her iş parçacığının yazmaçları/sinyalleri ve isteğe bağlı eşleme adayları | Linux x64 üzerinde çağıranın sağladığı pwntools; isteğe bağlı GDB/pwndbg; [kaydedilmiş çökmeler](../recorded-crashes.md) |
+| JavaScript / Electron | Modüller, içe aktarmalar, kaynak haritaları, rotalar, IPC ve yerel eklenti ilişkileri | Node.js ve npm; [uygulama analizi](https://rea.tools/tr/guides/javascript/) |
+| Web siteleri | Sayfa yapısı, betikler, ağ gözlemleri ve istenen ekran görüntüleri | Chrome ailesinden bir tarayıcı; [tarayıcı analizi](https://rea.tools/tr/guides/browser/) |
+| Kaydedilmiş ağ yakalamaları | İstekler, yanıtlar, erişilebilir yük içerikleri ve kaynak konumları | HAR veya Fiddler Everywhere SAZ; Linux'ta native mitmproxy için mitmdump; [ağ yakalama rehberi](../web-network-captures.md) |
+| .NET derlemeleri | Meta veriler, CIL talimatları, bildirilmiş yerel bağımlılıklar ve derleme karşılaştırmaları | Statik inceleme; [yönetilen kod rehberi](../managed-code-analysis.md) |
+| Android APK'leri | Manifest bildirimleri, sınıflar, tersine derlenmiş metotlar ve referanslar | Linux/macOS/Windows x64 üzerinde arayüzsüz JADX ve tam bir JDK; [Android rehberi](../android-analysis.md) |
+| Android cihazları | Cihazlar, paketler, süreçler, günlükler, dökümler, ekran görüntüleri ve APK/dosya aktarımları | Çağıranın sağladığı adb (emülatör veya cihaz); [ADB rehberi](../adb-device-analysis.md) |
+| JEB projeleri | Motor destekli proje birimleri, artefakt özetleri ve tür/metot sözdekodu | Çağıranın başlattığı, MCP sunan bir JEB istemcisi, REA_JEB_MCP_URL ile seçilir; [JEB rehberi](../jeb-analysis.md) |
+| Android kaynakları | Çözülmüş manifest, dize tabloları, diller ve sürüm bilgileri | Çağıranın sağladığı Apktool başlatıcısı; [kaynak rehberi](../apktool-resource-analysis.md) |
+| Flutter uygulamaları | Dart anlık görüntü karması, ABI başına kitaplık özetleri ve motor derleme kökeni | Yok (statik APK çözümleme); [Flutter rehberi](../flutter-analysis.md) |
+| Donanım yazılımı | Bölgeler, çıkarma sonuçları ve yerel kod analizine devirler | Linux üzerinde Binwalk / Unblob; [donanım yazılımı rehberi](../firmware-analysis.md) |
+| Kaydedilmiş paketler | Seçilen alanlar, yinelenen değerler, filtreler ve çözümleme kuralları | Linux: TShark 4.x + prlimit; [yakalama kılavuzu](../packet-captures.md) |
+| Paket özetleri | Uç noktalar, DNS, TLS SNI, protokol dağılımı ve HTTP istekleri | Çağıranın sağladığı tshark ve capinfos (Linux üzerinde doğrulandı); [pcap rehberi](../pcap-analysis.md) |
+| Kaydedilmiş mantık sinyalleri | Kod çözücü açıklamaları, örnek aralıkları ve tanılama | Linux: sigrok-cli 0.7.2 + prlimit; [sinyal kılavuzu](../signal-recordings.md) |
+| Paketler ve kaynaklar | Dosya envanterleri, özet değerleri, plist'ler, Apple paket yapısı ve çıkarılmış kaynaklar | [Artefakt ve JavaScript rehberi](../javascript-artifact-reconstruction.md), [Apple uygulamaları](../apple-application-analysis.md) |
+| Süreç davranışı | Terminal çıktısı, etkileşimler, sonlanma ve dosya sistemi gözlemleri, çalıştırma karşılaştırmaları | Yerel PTY ile Linux/macOS; [süreç yakalama](../process-capture.md) |
Statik JavaScript ve .NET incelemesi, uygulamayı çalıştırmadan sağlanan dosyaları okur. Çalışma zamanı yakalaması, seçilen hedefi kullanıcı izinlerinizle çalıştırır veya onunla etkileşime girer; her çalışma zamanı rehberi etkilerini açıklar.
-Yerel makine kodu içeren ikili dosya biçimleri ve ana bilgisayar desteği sağlayıcıya göre değişir. [Hopper ve Ghidra kurulumu](docs/installation.md#hopper), [IDA rehberi](docs/ida-provider.md) ve [Windows için deneysel Ghidra desteği](docs/windows-ghidra-p0.md) belgelerine bakın. Ghidra ayrıca [16 bit DOS analizini](docs/ghidra-dos.md) destekler. Sağlayıcı seçimi için [CLI rehberine](docs/cli.md#choose-a-provider) bakın. En son npm sürümünden sonra eklenen özellikler için [sürümlerde kullanılabilirliği](docs/installation.md#released-package-and-main) kontrol edin.
+Yerel makine kodu içeren ikili dosya biçimleri ve ana bilgisayar desteği sağlayıcıya göre değişir. [Hopper ve Ghidra kurulumu](../installation.md#hopper), isteğe bağlı native MCP bridge'i de açıklayan [IDA rehberi](../ida-provider.md) ve [Windows için deneysel Ghidra desteği](../windows-ghidra-p0.md) belgelerine bakın. Ghidra ayrıca [16 bit DOS analizini](../ghidra-dos.md) destekler. Büyük ikili dosyalar için sağlayıcının başlatma zaman aşımını `REA_GHIDRA_STARTUP_TIMEOUT_MS` veya `REA_HOPPER_STARTUP_TIMEOUT_MS` ile artırın. Sağlayıcı seçimi için [CLI rehberine](../cli.md#choose-a-provider) bakın. En son npm sürümünden sonra eklenen özellikler için [sürümlerde kullanılabilirliği](../installation.md#released-package-and-main) kontrol edin.
## Örnek çalışmalar
-[](https://rea.tools/tr/showcase/)
+[](https://rea.tools/tr/showcase/)
### DX-Ball: Sesin stereo konumlandırma hesabını yeniden oluşturma
@@ -187,28 +192,28 @@ REA'yı ilginç bir konuda kullandıysanız görmek isteriz. Hedefi, sorunuzu, R
Hangi ajanlar REA'yı kullanabilir?
-Yerel MCP sunucularını destekleyen her ajan REA'yı kullanabilir. Kurulum, [desteklenen ajanları](docs/installation.md#supported-agents) yapılandırır; diğer istemciler [elle MCP kaydını](docs/installation.md#mcp-registry) kullanabilir.
+Yerel MCP sunucularını destekleyen her ajan REA'yı kullanabilir. Kurulum, [desteklenen ajanları](../installation.md#supported-agents) yapılandırır; diğer istemciler [elle MCP kaydını](../installation.md#mcp-registry) kullanabilir.
Hopper, Ghidra veya IDA gerekiyor mu?
-Derin yerel kod analizi bunlardan birini kullanır. Statik JavaScript ve .NET incelemesi yerel analiz motoru olmadan çalışır. Kurulum onaydan sonra Hopper yükleyebilir; Ghidra ve IDA mevcut kurulumlarınızı kullanır. [Sağlayıcı kurulumuna](docs/installation.md#hopper) bakın.
+Derin yerel kod analizi bunlardan birini kullanır. Statik JavaScript ve .NET incelemesi yerel analiz motoru olmadan çalışır. Kurulum onaydan sonra Hopper yükleyebilir; Ghidra ve IDA mevcut kurulumlarınızı kullanır. [Sağlayıcı kurulumuna](../installation.md#hopper) bakın.
Önce Hopper'ı başlatmam gerekiyor mu?
-REA, bir işlem gerektirdiğinde Hopper'ı başlatır. macOS'ta ilk çalıştırmada demo modunu seçmenizi veya lisansınızı etkinleştirmenizi isteyen bir iletişim kutusu açılabilir. [Hopper başlatma ve sorun giderme](docs/installation.md#launcher-paths-and-troubleshooting) belgesine bakın.
+REA, bir işlem gerektirdiğinde Hopper'ı başlatır. macOS'ta ilk çalıştırmada demo modunu seçmenizi veya lisansınızı etkinleştirmenizi isteyen bir iletişim kutusu açılabilir. [Hopper başlatma ve sorun giderme](../installation.md#launcher-paths-and-troubleshooting) belgesine bakın.
skills.sh üzerinden skill yüklemek ne yapar?
-Skill, ajanınıza araştırma talimatları sağlar. REA'nın MCP sunucusunu kaydetmek ve uyumlu talimatları yüklemek için `npx rea-agents setup` kullanın, ardından ajanınızı yeniden başlatın. [Yalnızca skill yükleme](docs/installation.md#skill-only-installation) belgesine bakın.
+Skill, ajanınıza araştırma talimatları sağlar. REA'nın MCP sunucusunu kaydetmek ve uyumlu talimatları yüklemek için `npx rea-agents setup` kullanın, ardından ajanınızı yeniden başlatın. [Yalnızca skill yükleme](../installation.md#skill-only-installation) belgesine bakın.
@@ -251,14 +256,14 @@ Ajan kullanıyorsanız [kurulum yenilemesini](#reayı-güncelleyin) tamamlayın
Web sitesindeki [uygulamalı rehberlerden](https://rea.tools/tr/guides/) başlayın. Kesin seçenekler, ön koşullar ve sonuç sözleşmeleri için:
-- [Yükleme ve kurulum](docs/installation.md): ajan kaydı, sağlayıcı yapılandırması, güncellemeler ve kaldırma.
-- [Hazırlık ve sorun giderme](docs/installation.md#check-readiness-for-your-task): belirli bir ajanı veya analiz motorunu tanılama.
-- [CLI ve Evidence](docs/cli.md): komutlar, sağlayıcı seçimi, anlık görüntüler, içe/dışa aktarma ve çıkış durumları.
-- [MCP sözleşmeleri](docs/mcp-contracts.md) ve [ajan istemleri](docs/mcp-prompts.md): araç sonuçları, oturumlar ve yönlendirmeli araştırmalar.
-- [Araç kataloğu](docs/mcp-contracts.md#generated-catalog): derleme sırasında oluşturulan araç, sağlayıcı ve CLI komutu envanteri.
-- [Yol haritası](docs/roadmap.md): planlanan çalışmalar ve yetenek takibi.
+- [Yükleme ve kurulum](../installation.md): ajan kaydı, sağlayıcı yapılandırması, güncellemeler ve kaldırma.
+- [Hazırlık ve sorun giderme](../installation.md#check-readiness-for-your-task): belirli bir ajanı veya analiz motorunu tanılama.
+- [CLI ve Evidence](../cli.md): komutlar, sağlayıcı seçimi, anlık görüntüler, içe/dışa aktarma ve çıkış durumları.
+- [MCP sözleşmeleri](../mcp-contracts.md) ve [ajan istemleri](../mcp-prompts.md): araç sonuçları, oturumlar ve yönlendirmeli araştırmalar.
+- [Araç kataloğu](../mcp-contracts.md#generated-catalog): derleme sırasında oluşturulan araç, sağlayıcı ve CLI komutu envanteri.
+- [Yol haritası](../roadmap.md): planlanan çalışmalar ve yetenek takibi.
-Güvenlik açıklarını [SECURITY.md](SECURITY.md) üzerinden bildirin.
+Güvenlik açıklarını [SECURITY.md](../../SECURITY.md) üzerinden bildirin.
## Yıldız geçmişi
@@ -276,32 +281,34 @@ REA'yı kullanan, hata bildiren, özellik talep eden, derlemeleri test eden ve d
## Sorumluluk reddi
-Sorumlu kullanım yönergeleri için [DISCLAIMER.md](DISCLAIMER.md) dosyasına bakın.
+Sorumlu kullanım yönergeleri için [DISCLAIMER.md](../../DISCLAIMER.md) dosyasına bakın.
REA; yasal araştırma, eğitim, birlikte çalışabilirlik, yazılımın korunması ve sahibi olduğunuz ya da inceleme yetkiniz bulunan yazılımların analizi için tasarlanmıştır.
-The project's position on illegal or unauthorized use covers all REA
-versions, distribution channels, and platforms; each copy retains its
-applicable licenses.
+Projenin yasa dışı veya yetkisiz kullanıma ilişkin tutumu tüm REA sürümleri,
+dağıtım kanalları ve platformlar için geçerlidir; her kopya geçerli lisanslarını
+korur.
## Katkıda bulunma
REA'ya katkınızı bekliyoruz! Hata bildirmek veya özellik önermek için bir [issue açın](https://github.com/morluto/rea/issues); kodu veya belgeleri geliştirmek için bir [pull request gönderin](https://github.com/morluto/rea/pulls).
-Geliştirme ortamı ve kontroller için [CONTRIBUTING.md](CONTRIBUTING.md), doğrulama süreçleri için [test rehberi](docs/testing.md) ve proje yapısı için [mimari harita](docs/architecture.mermaid) belgelerine bakın.
+Geliştirme ortamı ve kontroller için [CONTRIBUTING.md](../../CONTRIBUTING.md), doğrulama süreçleri için [test rehberi](../testing.md) ve proje yapısı için [mimari harita](../architecture.mermaid) belgelerine bakın.
## Lisans
-[MIT](LICENSE)
+[MIT](../../LICENSE)
-REA-authored code is licensed under MIT. Third-party material retains its own
-licenses and copyright notices; see [Third-party notices](THIRD_PARTY_NOTICES.md)
-for the dependency inventory, preserved license texts, and external-tool terms.
+REA tarafından yazılan kod MIT lisansı altında lisanslanır. Üçüncü taraf
+materyaller kendi lisanslarını ve telif hakkı bildirimlerini korur; bağımlılık
+envanteri, korunan lisans metinleri ve harici araç koşulları için [Üçüncü taraf
+bildirimleri](../../THIRD_PARTY_NOTICES.md) dosyasına bakın.
-**You must legally obtain and be authorized to use commercial tools**, including
-IDA Pro / Hex-Rays decompilers, Hopper, and JEB, under their applicable licenses.
-REA grants no tool license, key, or activation entitlement. Trials and demos
-remain subject to vendor terms, including user, feature, automation, and output
-sharing restrictions. Cracked copies, stolen keys, unauthorized key sharing,
-and bypassing license restrictions are not authorized. This guidance does not
-amend MIT or third-party licenses.
+**IDA Pro / Hex-Rays decompiler'ları, Hopper ve JEB dahil olmak üzere ticari
+araçları yasal olarak edinmeli ve ilgili lisanslar kapsamında kullanma
+yetkiniz olmalıdır.** REA hiçbir araç lisansı, anahtarı veya etkinleştirme hakkı
+vermez. Deneme sürümleri ve demolar; kullanıcı, özellik, otomasyon ve çıktı
+paylaşımı kısıtlamaları dahil olmak üzere satıcı koşullarına tabidir. Kırılmış
+kopyalar, çalınmış anahtarlar, yetkisiz anahtar paylaşımı ve lisans
+kısıtlamalarını aşma yetkili kullanım kapsamında değildir. Bu yönlendirme MIT
+veya üçüncü taraf lisanslarını değiştirmez.
diff --git a/README_uk.md b/docs/readme/README_uk.md
similarity index 80%
rename from README_uk.md
rename to docs/readme/README_uk.md
index 14c3d4984..5c77d80b0 100644
--- a/README_uk.md
+++ b/docs/readme/README_uk.md
@@ -1,6 +1,6 @@
-[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · **Українська** · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · **Українська** · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Реверс-інжиніринг будь-чого
@@ -9,10 +9,10 @@
**Побачили цікаву функцію? Зрозумійте, як вона працює, аж до рівня двійкового коду.**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#current-status)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
@@ -25,7 +25,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
Приєднуйтеся до спільноти реверс-інжинірингу
Discord · Запитання й відповіді · Обмін результатами
@@ -67,7 +67,7 @@ npx rea-agents setup
Виберіть агентів, перевірте запропоновані зміни та схваліть їх. Налаштування додає MCP-сервер REA та відповідні інструкції робочих процесів, створюючи резервні копії наявної конфігурації. Після цього перезапустіть агента.
-Налаштування підтримує Claude Code, Codex, Cursor, Gemini CLI, Grok Build та [інших агентів](docs/installation.md#supported-agents). Конфігурацію провайдерів і ручну реєстрацію MCP описано в розділі [встановлення й налаштування](docs/installation.md).
+Налаштування підтримує Claude Code, Codex, Cursor, Gemini CLI, Grok Build та [інших агентів](../installation.md#supported-agents). Конфігурацію провайдерів і ручну реєстрацію MCP описано в розділі [встановлення й налаштування](../installation.md).
### Запитайте агента
@@ -94,7 +94,7 @@ npm install --global rea-agents
rea --help
```
-Для нативного аналізу спочатку налаштуйте провайдера. Нативні команди, вибір провайдера, знімки та використання у скриптах описує [посібник CLI та Evidence](docs/cli.md).
+Для нативного аналізу спочатку налаштуйте провайдера. Нативні команди, вибір провайдера, знімки та використання у скриптах описує [посібник CLI та Evidence](../cli.md).
### Оновіть REA
@@ -120,9 +120,9 @@ npx rea-agents@latest setup
Агент викликає REA через MCP, щоб перевірити ціль і простежити пов'язаний код. REA повертає знахідки разом із підтвердженнями. Агент використовує їх для уточнювальних запитань, пояснення поведінки або написання й тестування реалізації. Команди CLI використовують ті самі робочі процеси.
-
+
-[Відкрити зображення в повному розмірі](website/public/assets/figures/rea-investigation-flow.svg).
+[Відкрити зображення в повному розмірі](../../website/public/assets/figures/rea-investigation-flow.svg).
@@ -130,37 +130,37 @@ npx rea-agents@latest setup
REA потребує Node.js 22.x (>=22.19), 24.x (>=24.11) або 26+, а також npm. Додаткові інструменти й підтримувані системи залежать від цілі:
-| Ціль | Що повертає REA | Вимоги й посібник |
-| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](https://rea.tools/uk/guides/native/) |
-| Структура ELF без запуску | Секції, сегменти, початкові символи/релокації та можливі захисні механізми за даними статичного аналізу | pwntools, наданий стороною, що викликає інструмент, у Linux x64; [діагностика бінарних файлів](docs/binary-diagnostics.md) |
-| Байт-код EVM | Селектори диспетчеризації, зміщення байтів, виведені припущення про аргументи та змінюваність стану | Локальні необроблені байти або шістнадцяткове представлення у Linux x64; [посібник EVM без запуску](docs/evm-bytecode.md) |
-| Записані аварійні завершення в Linux | Необроблені записи note, регістри/сигнали кожного записаного потоку й необов'язкові кандидати зіставлення пам'яті | pwntools, наданий стороною, що викликає інструмент, у Linux x64; необов'язкові GDB/pwndbg; [записані аварійні завершення](docs/recorded-crashes.md) |
-| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](https://rea.tools/uk/guides/javascript/) |
-| Сайти | Структура сторінки, скрипти, мережеві спостереження й запитані знімки екрана | Браузер сімейства Chrome; [аналіз браузера](https://rea.tools/uk/guides/browser/) |
-| Збережені мережеві захоплення | Запити, відповіді, доступні корисні дані та місця в джерелі | HAR; mitmdump у Linux для захоплень у власному форматі mitmproxy; [посібник мережевих захоплень](docs/web-network-captures.md) |
-| Захоплення пакетів | Кінцеві точки, DNS, TLS SNI, розподіл протоколів та HTTP-запити | Надані викликаючою стороною tshark та capinfos; [підручник pcap](docs/pcap-analysis.md) |
-| Збірки .NET | Метадані, інструкції CIL, оголошені нативні залежності й порівняння збірок | Статична перевірка; [посібник керованого коду](docs/managed-code-analysis.md) |
-| Android APK | Оголошення маніфесту, класи, декомпільовані методи й посилання | JADX без графічного інтерфейсу й повний JDK у Linux/macOS/Windows x64; [посібник Android](docs/android-analysis.md) |
-| Пристрої Android | Пристрої, пакети, процеси, журнали, дампи, знімки екрана та передавання APK/файлів | adb, наданий викликаючою стороною (емулятор або пристрій); [підручник ADB](docs/adb-device-analysis.md) |
-| Проєкти JEB | Єдиниці проєкту від рушія, дайджести артефактів і псевдокод типів/методів | Запущений викликаною стороною клієнт JEB з MCP, обирається через REA_JEB_MCP_URL; [посібник JEB](docs/jeb-analysis.md) |
-| Ресурси Android | Декодований маніфест, таблиці рядків, мови та відомості про версію | Засіб запуску Apktool, наданий викликаючою стороною; [підручник ресурсів](docs/apktool-resource-analysis.md) |
-| Додатки Flutter | Хеш знімка Dart, дайджести бібліотек за ABI та походження збірки рушія | Не потрібні (статичний розбір APK); [підручник Flutter](docs/flutter-analysis.md) |
-| Прошивки | Області, результати видобування й передавання до нативного аналізу | Binwalk / Unblob у Linux; [посібник прошивок](docs/firmware-analysis.md) |
-| Записані пакети | Вибрані поля, повторювані значення, фільтри та правила декодування | Linux: TShark 4.x + prlimit; [посібник із записів пакетів](docs/packet-captures.md) |
-| Записані логічні сигнали | Анотації декодера, діапазони відліків і діагностика | Linux: sigrok-cli 0.7.2 + prlimit; [посібник із сигналів](docs/signal-recordings.md) |
-| Пакети й ресурси | Переліки файлів, хеші, plist, структура пакетів Apple та видобуті ресурси | [Посібник артефактів і JavaScript](docs/javascript-artifact-reconstruction.md), [застосунки Apple](docs/apple-application-analysis.md) |
-| Поведінка процесів | Вивід термінала, взаємодії, спостереження за завершенням і файловою системою та порівняння запусків | Linux/macOS із нативним PTY; [захоплення процесів](docs/process-capture.md) |
+| Ціль | Що повертає REA | Вимоги й посібник |
+| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](https://rea.tools/uk/guides/native/) |
+| Структура ELF без запуску | Секції, сегменти, початкові символи/релокації та можливі захисні механізми за даними статичного аналізу | pwntools, наданий стороною, що викликає інструмент, у Linux x64; [діагностика бінарних файлів](../binary-diagnostics.md) |
+| Байт-код EVM | Селектори диспетчеризації, зміщення байтів, виведені припущення про аргументи та змінюваність стану | Локальні необроблені байти або шістнадцяткове представлення у Linux x64; [посібник EVM без запуску](../evm-bytecode.md) |
+| Записані аварійні завершення в Linux | Необроблені записи note, регістри/сигнали кожного записаного потоку й необов'язкові кандидати зіставлення пам'яті | pwntools, наданий стороною, що викликає інструмент, у Linux x64; необов'язкові GDB/pwndbg; [записані аварійні завершення](../recorded-crashes.md) |
+| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](https://rea.tools/uk/guides/javascript/) |
+| Сайти | Структура сторінки, скрипти, мережеві спостереження й запитані знімки екрана | Браузер сімейства Chrome; [аналіз браузера](https://rea.tools/uk/guides/browser/) |
+| Збережені мережеві захоплення | Запити, відповіді, доступні корисні дані та місця в джерелі | HAR; mitmdump у Linux для захоплень у власному форматі mitmproxy; [посібник мережевих захоплень](../web-network-captures.md) |
+| Захоплення пакетів | Кінцеві точки, DNS, TLS SNI, розподіл протоколів та HTTP-запити | Надані викликаючою стороною tshark та capinfos; [підручник pcap](../pcap-analysis.md) |
+| Збірки .NET | Метадані, інструкції CIL, оголошені нативні залежності й порівняння збірок | Статична перевірка; [посібник керованого коду](../managed-code-analysis.md) |
+| Android APK | Оголошення маніфесту, класи, декомпільовані методи й посилання | JADX без графічного інтерфейсу й повний JDK у Linux/macOS/Windows x64; [посібник Android](../android-analysis.md) |
+| Пристрої Android | Пристрої, пакети, процеси, журнали, дампи, знімки екрана та передавання APK/файлів | adb, наданий викликаючою стороною (емулятор або пристрій); [підручник ADB](../adb-device-analysis.md) |
+| Проєкти JEB | Єдиниці проєкту від рушія, дайджести артефактів і псевдокод типів/методів | Запущений викликаною стороною клієнт JEB з MCP, обирається через REA_JEB_MCP_URL; [посібник JEB](../jeb-analysis.md) |
+| Ресурси Android | Декодований маніфест, таблиці рядків, мови та відомості про версію | Засіб запуску Apktool, наданий викликаючою стороною; [підручник ресурсів](../apktool-resource-analysis.md) |
+| Додатки Flutter | Хеш знімка Dart, дайджести бібліотек за ABI та походження збірки рушія | Не потрібні (статичний розбір APK); [підручник Flutter](../flutter-analysis.md) |
+| Прошивки | Області, результати видобування й передавання до нативного аналізу | Binwalk / Unblob у Linux; [посібник прошивок](../firmware-analysis.md) |
+| Записані пакети | Вибрані поля, повторювані значення, фільтри та правила декодування | Linux: TShark 4.x + prlimit; [посібник із записів пакетів](../packet-captures.md) |
+| Записані логічні сигнали | Анотації декодера, діапазони відліків і діагностика | Linux: sigrok-cli 0.7.2 + prlimit; [посібник із сигналів](../signal-recordings.md) |
+| Пакети й ресурси | Переліки файлів, хеші, plist, структура пакетів Apple та видобуті ресурси | [Посібник артефактів і JavaScript](../javascript-artifact-reconstruction.md), [застосунки Apple](../apple-application-analysis.md) |
+| Поведінка процесів | Вивід термінала, взаємодії, спостереження за завершенням і файловою системою та порівняння запусків | Linux/macOS із нативним PTY; [захоплення процесів](../process-capture.md) |
Статична перевірка JavaScript і .NET читає надані файли, не запускаючи застосунок. Захоплення під час виконання запускає вибрану ціль або взаємодіє з нею з правами вашого користувача; відповідні посібники описують його вплив.
-Нативні формати й підтримка систем відрізняються залежно від провайдера. Дивіться [налаштування Hopper і Ghidra](docs/installation.md#hopper), [посібник IDA](docs/ida-provider.md) та [експериментальну підтримку Ghidra у Windows](docs/windows-ghidra-p0.md). Ghidra також підтримує [аналіз 16-бітного DOS](docs/ghidra-dos.md). Для вибору провайдера дивіться [посібник CLI](docs/cli.md#choose-a-provider). Для функцій, доданих після найновішої версії в npm, перевірте [доступність у випусках](docs/installation.md#released-package-and-main).
+Нативні формати й підтримка систем відрізняються залежно від провайдера. Дивіться [налаштування Hopper і Ghidra](../installation.md#hopper), [посібник IDA](../ida-provider.md) та [експериментальну підтримку Ghidra у Windows](../windows-ghidra-p0.md). Ghidra також підтримує [аналіз 16-бітного DOS](../ghidra-dos.md). Для вибору провайдера дивіться [посібник CLI](../cli.md#choose-a-provider). Для функцій, доданих після найновішої версії в npm, перевірте [доступність у випусках](../installation.md#released-package-and-main).
## Приклади
-[](https://rea.tools/uk/showcase/)
+[](https://rea.tools/uk/showcase/)
### DX-Ball: відновлення розрахунку звукової панорами
@@ -187,28 +187,28 @@ REA потребує Node.js 22.x (>=22.19), 24.x (>=24.11) або 26+, а та
Які агенти можуть використовувати REA?
-Будь-який агент, що підтримує локальні MCP-сервери. Налаштування конфігурує [підтримуваних агентів](docs/installation.md#supported-agents); інші клієнти можуть використати [ручну реєстрацію MCP](docs/installation.md#mcp-registry).
+Будь-який агент, що підтримує локальні MCP-сервери. Налаштування конфігурує [підтримуваних агентів](../installation.md#supported-agents); інші клієнти можуть використати [ручну реєстрацію MCP](../installation.md#mcp-registry).
Чи потрібні мені Hopper, Ghidra або IDA?
-Поглиблений нативний аналіз використовує один із цих інструментів. Статична перевірка JavaScript і .NET працює без рушія нативного аналізу. Налаштування може встановити Hopper після схвалення; Ghidra й IDA використовують наявні встановлення. Дивіться [налаштування провайдерів](docs/installation.md#hopper).
+Поглиблений нативний аналіз використовує один із цих інструментів. Статична перевірка JavaScript і .NET працює без рушія нативного аналізу. Налаштування може встановити Hopper після схвалення; Ghidra й IDA використовують наявні встановлення. Дивіться [налаштування провайдерів](../installation.md#hopper).
Чи потрібно спочатку запускати Hopper?
-REA запускає Hopper, коли він потрібен для операції. У macOS під час першого запуску може з'явитися діалог вибору деморежиму або активації ліцензії. Дивіться [запуск Hopper та усунення неполадок](docs/installation.md#launcher-paths-and-troubleshooting).
+REA запускає Hopper, коли він потрібен для операції. У macOS під час першого запуску може з'явитися діалог вибору деморежиму або активації ліцензії. Дивіться [запуск Hopper та усунення неполадок](../installation.md#launcher-paths-and-troubleshooting).
Що дає встановлення skill із skills.sh?
-Skill надає агенту інструкції дослідження. Використовуйте `npx rea-agents setup`, щоб зареєструвати MCP-сервер REA та встановити відповідні інструкції, а потім перезапустіть агента. Дивіться [встановлення лише skill](docs/installation.md#skill-only-installation).
+Skill надає агенту інструкції дослідження. Використовуйте `npx rea-agents setup`, щоб зареєструвати MCP-сервер REA та встановити відповідні інструкції, а потім перезапустіть агента. Дивіться [встановлення лише skill](../installation.md#skill-only-installation).
@@ -251,14 +251,14 @@ npx rea-agents@latest setup
Почніть із [практичних посібників](https://rea.tools/uk/guides/) на сайті. Точні параметри, передумови й контракти результатів описано тут:
-- [Встановлення й налаштування](docs/installation.md): реєстрація агентів, конфігурація провайдерів, оновлення та видалення.
-- [Готовність та усунення неполадок](docs/installation.md#check-readiness-for-your-task): діагностика конкретного агента або рушія аналізу.
-- [CLI та Evidence](docs/cli.md): команди, вибір провайдера, знімки, імпорт/експорт і статуси завершення.
-- [Контракти MCP](docs/mcp-contracts.md) і [промпти агентів](docs/mcp-prompts.md): результати інструментів, сеанси й керовані дослідження.
-- [Каталог інструментів](docs/mcp-contracts.md#generated-catalog): створюваний під час збірки перелік інструментів, провайдерів і команд CLI.
-- [План розвитку](docs/roadmap.md): заплановані роботи й відстеження можливостей.
+- [Встановлення й налаштування](../installation.md): реєстрація агентів, конфігурація провайдерів, оновлення та видалення.
+- [Готовність та усунення неполадок](../installation.md#check-readiness-for-your-task): діагностика конкретного агента або рушія аналізу.
+- [CLI та Evidence](../cli.md): команди, вибір провайдера, знімки, імпорт/експорт і статуси завершення.
+- [Контракти MCP](../mcp-contracts.md) і [промпти агентів](../mcp-prompts.md): результати інструментів, сеанси й керовані дослідження.
+- [Каталог інструментів](../mcp-contracts.md#generated-catalog): створюваний під час збірки перелік інструментів, провайдерів і команд CLI.
+- [План розвитку](../roadmap.md): заплановані роботи й відстеження можливостей.
-Повідомляйте про вразливості згідно з [SECURITY.md](SECURITY.md).
+Повідомляйте про вразливості згідно з [SECURITY.md](../../SECURITY.md).
## Історія зірок
@@ -276,7 +276,7 @@ npx rea-agents@latest setup
## Відмова від відповідальності
-Рекомендації щодо відповідального використання наведено в [DISCLAIMER.md](DISCLAIMER.md).
+Рекомендації щодо відповідального використання наведено в [DISCLAIMER.md](../../DISCLAIMER.md).
REA призначений для законних досліджень, навчання, забезпечення сумісності, збереження програмного забезпечення й аналізу програм, якими ви володієте або які маєте дозвіл досліджувати.
@@ -288,14 +288,14 @@ applicable licenses.
Будемо раді вашій допомозі з REA! [Відкрийте issue](https://github.com/morluto/rea/issues), щоб повідомити про помилку чи запропонувати функцію, або [надішліть pull request](https://github.com/morluto/rea/pulls) з поліпшеннями коду чи документації.
-Налаштування середовища розробки й перевірки описано в [CONTRIBUTING.md](CONTRIBUTING.md), шляхи верифікації — у [посібнику тестування](docs/testing.md), а структуру проєкту — на [схемі архітектури](docs/architecture.mermaid).
+Налаштування середовища розробки й перевірки описано в [CONTRIBUTING.md](../../CONTRIBUTING.md), шляхи верифікації — у [посібнику тестування](../testing.md), а структуру проєкту — на [схемі архітектури](../architecture.mermaid).
## Ліцензія
-[MIT](LICENSE)
+[MIT](../../LICENSE)
REA-authored code is licensed under MIT. Third-party material retains its own
-licenses and copyright notices; see [Third-party notices](THIRD_PARTY_NOTICES.md)
+licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
for the dependency inventory, preserved license texts, and external-tool terms.
**You must legally obtain and be authorized to use commercial tools**, including
diff --git a/README_vi.md b/docs/readme/README_vi.md
similarity index 75%
rename from README_vi.md
rename to docs/readme/README_vi.md
index 86544a535..3d2062da6 100644
--- a/README_vi.md
+++ b/docs/readme/README_vi.md
@@ -1,6 +1,6 @@
-[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · **Tiếng Việt** · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · **Tiếng Việt** · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Kỹ thuật dịch ngược cho mọi thứ
@@ -9,10 +9,10 @@
**Thấy một tính năng bạn thích? Hiểu cách nó hoạt động, đến tận mức mã nhị phân.**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#current-status)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
@@ -25,7 +25,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
Tham gia cộng đồng kỹ thuật dịch ngược
Discord · Hỏi đáp · Chia sẻ kết quả
@@ -67,7 +67,7 @@ npx rea-agents setup
Chọn các tác nhân, xem lại những thay đổi được đề xuất và chấp thuận. Quá trình thiết lập thêm máy chủ MCP của REA cùng hướng dẫn quy trình tương ứng, đồng thời sao lưu cấu hình hiện có. Sau đó khởi động lại tác nhân.
-Quá trình thiết lập hỗ trợ Claude Code, Codex, Cursor, Gemini CLI, Grok Build và [các tác nhân khác](docs/installation.md#supported-agents). Xem [cài đặt và thiết lập](docs/installation.md) để cấu hình trình phân tích và đăng ký MCP thủ công.
+Quá trình thiết lập hỗ trợ Claude Code, Codex, Cursor, Gemini CLI, Grok Build và [các tác nhân khác](../installation.md#supported-agents). Xem [cài đặt và thiết lập](../installation.md) để cấu hình trình phân tích và đăng ký MCP thủ công.
### Hỏi tác nhân
@@ -94,7 +94,7 @@ npm install --global rea-agents
rea --help
```
-Để phân tích mã máy, trước tiên hãy cấu hình trình phân tích. Xem [hướng dẫn CLI và Evidence](docs/cli.md) về các lệnh phân tích mã máy, lựa chọn trình phân tích, ảnh chụp trạng thái và sử dụng trong script.
+Để phân tích mã máy, trước tiên hãy cấu hình trình phân tích. Xem [hướng dẫn CLI và Evidence](../cli.md) về các lệnh phân tích mã máy, lựa chọn trình phân tích, ảnh chụp trạng thái và sử dụng trong script.
### Cập nhật REA
@@ -120,9 +120,9 @@ Xem lại thay đổi cấu hình và khởi động lại tác nhân. Với l
Tác nhân gọi REA qua MCP để kiểm tra mục tiêu và lần theo mã liên quan. REA trả về các phát hiện kèm bằng chứng. Tác nhân dùng chúng để đặt câu hỏi tiếp theo, giải thích hành vi hoặc viết và kiểm thử một cách triển khai. Các lệnh CLI sử dụng cùng quy trình.
-
+
-[Mở hình ở kích thước đầy đủ](website/public/assets/figures/rea-investigation-flow.svg).
+[Mở hình ở kích thước đầy đủ](../../website/public/assets/figures/rea-investigation-flow.svg).
@@ -130,37 +130,37 @@ Tác nhân gọi REA qua MCP để kiểm tra mục tiêu và lần theo mã li
REA yêu cầu Node.js 22.x (>=22.19), 24.x (>=24.11) hoặc 26+, cùng với npm. Công cụ bổ sung và nền tảng chạy REA được hỗ trợ tùy thuộc vào mục tiêu:
-| Mục tiêu | REA trả về gì | Yêu cầu và hướng dẫn |
-| ----------------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
-| Tệp nhị phân mã máy | Mã giả, hợp ngữ, chuỗi, ký hiệu, lời gọi và tham chiếu | Hopper, Ghidra hoặc IDA; [phân tích mã máy](https://rea.tools/vi/guides/native/) |
-| Bố cục ELF ngoại tuyến | Các section, segment, ký hiệu/thông tin tái định vị gốc và các biện pháp giảm thiểu có thể có từ phân tích tĩnh | pwntools do bên gọi cung cấp trên Linux x64; [chẩn đoán tệp nhị phân](docs/binary-diagnostics.md) |
-| Bytecode EVM | Bộ chọn điều phối, độ lệch byte, tham số suy luận và khả năng thay đổi trạng thái | Đầu vào cục bộ chứa byte thô hoặc dạng thập lục phân trên Linux x64; [hướng dẫn EVM ngoại tuyến](docs/evm-bytecode.md) |
-| Sự cố Linux đã ghi lại | Bản ghi note thô, thanh ghi/tín hiệu của mọi luồng được ghi lại và các ứng viên ánh xạ tùy chọn | pwntools do bên gọi cung cấp trên Linux x64; GDB/pwndbg tùy chọn; [sự cố đã ghi lại](docs/recorded-crashes.md) |
-| JavaScript / Electron | Mô-đun, các khai báo import, source map, tuyến, IPC và quan hệ với tiện ích mở rộng mã máy | Node.js và npm; [phân tích ứng dụng](https://rea.tools/vi/guides/javascript/) |
-| Trang web | Cấu trúc trang, script, quan sát mạng và ảnh chụp màn hình được yêu cầu | Trình duyệt thuộc họ Chrome; [phân tích trình duyệt](https://rea.tools/vi/guides/browser/) |
-| Bản ghi lưu lượng mạng đã lưu | Yêu cầu, phản hồi, payload có thể truy cập và vị trí nguồn | HAR; mitmdump trên Linux cho bản ghi định dạng gốc của mitmproxy; [hướng dẫn bản ghi mạng](docs/web-network-captures.md) |
-| Bắt gói tin | Điểm cuối, DNS, TLS SNI, phân bố giao thức và yêu cầu HTTP | tshark và capinfos do nơi gọi cung cấp; [hướng dẫn pcap](docs/pcap-analysis.md) |
-| Assembly .NET | Siêu dữ liệu, lệnh CIL, các phụ thuộc mã máy được khai báo và so sánh bản dựng | Kiểm tra tĩnh; [hướng dẫn mã được quản lý](docs/managed-code-analysis.md) |
-| APK Android | Khai báo manifest, lớp, phương thức được dịch ngược và tham chiếu | JADX không giao diện đồ họa và JDK đầy đủ trên Linux/macOS/Windows x64; [hướng dẫn Android](docs/android-analysis.md) |
-| Thiết bị Android | Thiết bị, gói, tiến trình, nhật ký, dump, ảnh chụp màn hình và truyền APK/tệp | adb do nơi gọi cung cấp (trình giả lập hoặc thiết bị); [hướng dẫn ADB](docs/adb-device-analysis.md) |
-| Dự án JEB | Đơn vị dự án từ engine, hash của artifact và mã giả của kiểu/phương thức | Một ứng dụng JEB do nơi gọi khởi động, phục vụ MCP, chọn bằng REA_JEB_MCP_URL; [hướng dẫn JEB](docs/jeb-analysis.md) |
-| Tài nguyên Android | Manifest đã giải mã, bảng chuỗi, ngôn ngữ và thông tin phiên bản | Trình khởi chạy Apktool do nơi gọi cung cấp; [hướng dẫn tài nguyên](docs/apktool-resource-analysis.md) |
-| Ứng dụng Flutter | Băm snapshot Dart, hash thư viện theo ABI và nguồn gốc build engine | Không cần (phân tích APK tĩnh); [hướng dẫn Flutter](docs/flutter-analysis.md) |
-| Phần sụn | Các vùng, kết quả trích xuất và dữ liệu chuyển sang phân tích mã máy | Binwalk / Unblob trên Linux; [hướng dẫn phần sụn](docs/firmware-analysis.md) |
-| Gói tin đã ghi | Trường được chọn, giá trị lặp, bộ lọc và quy tắc giải mã | Linux: TShark 4.x + prlimit; [hướng dẫn bản ghi gói tin](docs/packet-captures.md) |
-| Tín hiệu logic đã ghi | Chú thích bộ giải mã, phạm vi mẫu và chẩn đoán | Linux: sigrok-cli 0.7.2 + prlimit; [hướng dẫn tín hiệu](docs/signal-recordings.md) |
-| Gói và tài nguyên | Danh sách tệp, giá trị băm, plist, cấu trúc bundle Apple và tài nguyên được trích xuất | [Hướng dẫn hiện vật phần mềm và JavaScript](docs/javascript-artifact-reconstruction.md), [ứng dụng Apple](docs/apple-application-analysis.md) |
-| Hành vi tiến trình | Đầu ra terminal, tương tác, quan sát khi thoát và hệ thống tệp, cùng so sánh các lần chạy | Linux/macOS có PTY gốc; [ghi lại tiến trình](docs/process-capture.md) |
+| Mục tiêu | REA trả về gì | Yêu cầu và hướng dẫn |
+| ----------------------------- | --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
+| Tệp nhị phân mã máy | Mã giả, hợp ngữ, chuỗi, ký hiệu, lời gọi và tham chiếu | Hopper, Ghidra hoặc IDA; [phân tích mã máy](https://rea.tools/vi/guides/native/) |
+| Bố cục ELF ngoại tuyến | Các section, segment, ký hiệu/thông tin tái định vị gốc và các biện pháp giảm thiểu có thể có từ phân tích tĩnh | pwntools do bên gọi cung cấp trên Linux x64; [chẩn đoán tệp nhị phân](../binary-diagnostics.md) |
+| Bytecode EVM | Bộ chọn điều phối, độ lệch byte, tham số suy luận và khả năng thay đổi trạng thái | Đầu vào cục bộ chứa byte thô hoặc dạng thập lục phân trên Linux x64; [hướng dẫn EVM ngoại tuyến](../evm-bytecode.md) |
+| Sự cố Linux đã ghi lại | Bản ghi note thô, thanh ghi/tín hiệu của mọi luồng được ghi lại và các ứng viên ánh xạ tùy chọn | pwntools do bên gọi cung cấp trên Linux x64; GDB/pwndbg tùy chọn; [sự cố đã ghi lại](../recorded-crashes.md) |
+| JavaScript / Electron | Mô-đun, các khai báo import, source map, tuyến, IPC và quan hệ với tiện ích mở rộng mã máy | Node.js và npm; [phân tích ứng dụng](https://rea.tools/vi/guides/javascript/) |
+| Trang web | Cấu trúc trang, script, quan sát mạng và ảnh chụp màn hình được yêu cầu | Trình duyệt thuộc họ Chrome; [phân tích trình duyệt](https://rea.tools/vi/guides/browser/) |
+| Bản ghi lưu lượng mạng đã lưu | Yêu cầu, phản hồi, payload có thể truy cập và vị trí nguồn | HAR; mitmdump trên Linux cho bản ghi định dạng gốc của mitmproxy; [hướng dẫn bản ghi mạng](../web-network-captures.md) |
+| Bắt gói tin | Điểm cuối, DNS, TLS SNI, phân bố giao thức và yêu cầu HTTP | tshark và capinfos do nơi gọi cung cấp; [hướng dẫn pcap](../pcap-analysis.md) |
+| Assembly .NET | Siêu dữ liệu, lệnh CIL, các phụ thuộc mã máy được khai báo và so sánh bản dựng | Kiểm tra tĩnh; [hướng dẫn mã được quản lý](../managed-code-analysis.md) |
+| APK Android | Khai báo manifest, lớp, phương thức được dịch ngược và tham chiếu | JADX không giao diện đồ họa và JDK đầy đủ trên Linux/macOS/Windows x64; [hướng dẫn Android](../android-analysis.md) |
+| Thiết bị Android | Thiết bị, gói, tiến trình, nhật ký, dump, ảnh chụp màn hình và truyền APK/tệp | adb do nơi gọi cung cấp (trình giả lập hoặc thiết bị); [hướng dẫn ADB](../adb-device-analysis.md) |
+| Dự án JEB | Đơn vị dự án từ engine, hash của artifact và mã giả của kiểu/phương thức | Một ứng dụng JEB do nơi gọi khởi động, phục vụ MCP, chọn bằng REA_JEB_MCP_URL; [hướng dẫn JEB](../jeb-analysis.md) |
+| Tài nguyên Android | Manifest đã giải mã, bảng chuỗi, ngôn ngữ và thông tin phiên bản | Trình khởi chạy Apktool do nơi gọi cung cấp; [hướng dẫn tài nguyên](../apktool-resource-analysis.md) |
+| Ứng dụng Flutter | Băm snapshot Dart, hash thư viện theo ABI và nguồn gốc build engine | Không cần (phân tích APK tĩnh); [hướng dẫn Flutter](../flutter-analysis.md) |
+| Phần sụn | Các vùng, kết quả trích xuất và dữ liệu chuyển sang phân tích mã máy | Binwalk / Unblob trên Linux; [hướng dẫn phần sụn](../firmware-analysis.md) |
+| Gói tin đã ghi | Trường được chọn, giá trị lặp, bộ lọc và quy tắc giải mã | Linux: TShark 4.x + prlimit; [hướng dẫn bản ghi gói tin](../packet-captures.md) |
+| Tín hiệu logic đã ghi | Chú thích bộ giải mã, phạm vi mẫu và chẩn đoán | Linux: sigrok-cli 0.7.2 + prlimit; [hướng dẫn tín hiệu](../signal-recordings.md) |
+| Gói và tài nguyên | Danh sách tệp, giá trị băm, plist, cấu trúc bundle Apple và tài nguyên được trích xuất | [Hướng dẫn hiện vật phần mềm và JavaScript](../javascript-artifact-reconstruction.md), [ứng dụng Apple](../apple-application-analysis.md) |
+| Hành vi tiến trình | Đầu ra terminal, tương tác, quan sát khi thoát và hệ thống tệp, cùng so sánh các lần chạy | Linux/macOS có PTY gốc; [ghi lại tiến trình](../process-capture.md) |
Kiểm tra tĩnh JavaScript và .NET đọc các tệp được cung cấp mà không chạy ứng dụng. Ghi lại khi chạy sẽ khởi chạy hoặc tương tác với mục tiêu đã chọn bằng quyền người dùng của bạn; từng hướng dẫn mô tả tác động của nó.
-Định dạng mã máy và nền tảng chạy REA được hỗ trợ khác nhau tùy trình phân tích. Xem [thiết lập Hopper và Ghidra](docs/installation.md#hopper), [hướng dẫn IDA](docs/ida-provider.md) và [hỗ trợ Ghidra thử nghiệm trên Windows](docs/windows-ghidra-p0.md). Ghidra cũng hỗ trợ [phân tích DOS 16 bit](docs/ghidra-dos.md). Để chọn trình phân tích, xem [hướng dẫn CLI](docs/cli.md#choose-a-provider). Kiểm tra [khả năng sử dụng theo bản phát hành](docs/installation.md#released-package-and-main) đối với tính năng được bổ sung sau phiên bản npm mới nhất.
+Định dạng mã máy và nền tảng chạy REA được hỗ trợ khác nhau tùy trình phân tích. Xem [thiết lập Hopper và Ghidra](../installation.md#hopper), [hướng dẫn IDA](../ida-provider.md) và [hỗ trợ Ghidra thử nghiệm trên Windows](../windows-ghidra-p0.md). Ghidra cũng hỗ trợ [phân tích DOS 16 bit](../ghidra-dos.md). Để chọn trình phân tích, xem [hướng dẫn CLI](../cli.md#choose-a-provider). Kiểm tra [khả năng sử dụng theo bản phát hành](../installation.md#released-package-and-main) đối với tính năng được bổ sung sau phiên bản npm mới nhất.
## Các nghiên cứu điển hình
-[](https://rea.tools/vi/showcase/)
+[](https://rea.tools/vi/showcase/)
### DX-Ball: tái tạo phép tính phân bố âm thanh trái phải
@@ -187,28 +187,28 @@ Nếu bạn dùng REA cho một mục tiêu thú vị, chúng tôi muốn biết
Tác nhân nào có thể dùng REA?
-Bất kỳ tác nhân nào hỗ trợ máy chủ MCP cục bộ. Quá trình thiết lập cấu hình các [tác nhân được hỗ trợ](docs/installation.md#supported-agents); ứng dụng khách khác có thể dùng [đăng ký MCP thủ công](docs/installation.md#mcp-registry).
+Bất kỳ tác nhân nào hỗ trợ máy chủ MCP cục bộ. Quá trình thiết lập cấu hình các [tác nhân được hỗ trợ](../installation.md#supported-agents); ứng dụng khách khác có thể dùng [đăng ký MCP thủ công](../installation.md#mcp-registry).
Tôi có cần Hopper, Ghidra hoặc IDA không?
-Phân tích mã máy chuyên sâu sử dụng một trong những công cụ đó. Kiểm tra tĩnh JavaScript và .NET hoạt động mà không cần bộ máy phân tích mã máy. Quá trình thiết lập có thể cài Hopper sau khi được chấp thuận; Ghidra và IDA dùng bản cài đặt sẵn có. Xem [thiết lập trình phân tích](docs/installation.md#hopper).
+Phân tích mã máy chuyên sâu sử dụng một trong những công cụ đó. Kiểm tra tĩnh JavaScript và .NET hoạt động mà không cần bộ máy phân tích mã máy. Quá trình thiết lập có thể cài Hopper sau khi được chấp thuận; Ghidra và IDA dùng bản cài đặt sẵn có. Xem [thiết lập trình phân tích](../installation.md#hopper).
Tôi có cần khởi chạy Hopper trước không?
-REA khởi chạy Hopper khi một thao tác cần đến nó. Trên macOS, hộp thoại lần chạy đầu có thể yêu cầu chọn chế độ dùng thử hoặc kích hoạt giấy phép. Xem [khởi động Hopper và khắc phục sự cố](docs/installation.md#launcher-paths-and-troubleshooting).
+REA khởi chạy Hopper khi một thao tác cần đến nó. Trên macOS, hộp thoại lần chạy đầu có thể yêu cầu chọn chế độ dùng thử hoặc kích hoạt giấy phép. Xem [khởi động Hopper và khắc phục sự cố](../installation.md#launcher-paths-and-troubleshooting).
Cài skill từ skills.sh có tác dụng gì?
-Skill cung cấp hướng dẫn điều tra cho tác nhân. Dùng `npx rea-agents setup` để đăng ký máy chủ MCP của REA và cài hướng dẫn tương ứng, sau đó khởi động lại tác nhân. Xem [chỉ cài skill](docs/installation.md#skill-only-installation).
+Skill cung cấp hướng dẫn điều tra cho tác nhân. Dùng `npx rea-agents setup` để đăng ký máy chủ MCP của REA và cài hướng dẫn tương ứng, sau đó khởi động lại tác nhân. Xem [chỉ cài skill](../installation.md#skill-only-installation).
@@ -251,14 +251,14 @@ Nếu dùng tác nhân, hãy hoàn tất [làm mới thiết lập](#cập-nhậ
Bắt đầu với [hướng dẫn thực hành](https://rea.tools/vi/guides/) trên trang web. Để biết tùy chọn cụ thể, điều kiện tiên quyết và hợp đồng kết quả:
-- [Cài đặt và thiết lập](docs/installation.md): đăng ký tác nhân, cấu hình trình phân tích, cập nhật và gỡ cài đặt.
-- [Kiểm tra mức sẵn sàng và khắc phục sự cố](docs/installation.md#check-readiness-for-your-task): chẩn đoán một tác nhân hoặc bộ máy phân tích cụ thể.
-- [CLI và Evidence](docs/cli.md): lệnh, lựa chọn trình phân tích, ảnh chụp trạng thái, nhập/xuất và trạng thái thoát.
-- [Hợp đồng MCP](docs/mcp-contracts.md) và [prompt cho tác nhân](docs/mcp-prompts.md): kết quả công cụ, phiên làm việc và điều tra có hướng dẫn.
-- [Danh mục công cụ](docs/mcp-contracts.md#generated-catalog): danh sách công cụ, trình phân tích và lệnh CLI được tạo khi dựng dự án.
-- [Lộ trình](docs/roadmap.md): công việc dự kiến và theo dõi các khả năng.
+- [Cài đặt và thiết lập](../installation.md): đăng ký tác nhân, cấu hình trình phân tích, cập nhật và gỡ cài đặt.
+- [Kiểm tra mức sẵn sàng và khắc phục sự cố](../installation.md#check-readiness-for-your-task): chẩn đoán một tác nhân hoặc bộ máy phân tích cụ thể.
+- [CLI và Evidence](../cli.md): lệnh, lựa chọn trình phân tích, ảnh chụp trạng thái, nhập/xuất và trạng thái thoát.
+- [Hợp đồng MCP](../mcp-contracts.md) và [prompt cho tác nhân](../mcp-prompts.md): kết quả công cụ, phiên làm việc và điều tra có hướng dẫn.
+- [Danh mục công cụ](../mcp-contracts.md#generated-catalog): danh sách công cụ, trình phân tích và lệnh CLI được tạo khi dựng dự án.
+- [Lộ trình](../roadmap.md): công việc dự kiến và theo dõi các khả năng.
-Báo cáo lỗ hổng theo [SECURITY.md](SECURITY.md).
+Báo cáo lỗ hổng theo [SECURITY.md](../../SECURITY.md).
## Lịch sử sao
@@ -276,7 +276,7 @@ Cảm ơn mọi người đã sử dụng REA, báo lỗi, đề xuất tính n
## Miễn trừ trách nhiệm
-Xem [DISCLAIMER.md](DISCLAIMER.md) để biết hướng dẫn sử dụng có trách nhiệm.
+Xem [DISCLAIMER.md](../../DISCLAIMER.md) để biết hướng dẫn sử dụng có trách nhiệm.
REA dành cho nghiên cứu, giáo dục, khả năng tương tác, bảo tồn phần mềm và phân tích phần mềm bạn sở hữu hoặc được phép kiểm tra, trong khuôn khổ pháp luật.
@@ -288,14 +288,14 @@ applicable licenses.
Chúng tôi hoan nghênh bạn giúp phát triển REA! [Mở issue](https://github.com/morluto/rea/issues) để báo lỗi hoặc đề xuất tính năng, hoặc [gửi pull request](https://github.com/morluto/rea/pulls) để cải thiện mã hay tài liệu.
-Xem [CONTRIBUTING.md](CONTRIBUTING.md) về thiết lập môi trường phát triển và các bước kiểm tra, [hướng dẫn kiểm thử](docs/testing.md) về các luồng xác minh và [sơ đồ kiến trúc](docs/architecture.mermaid) về cấu trúc dự án.
+Xem [CONTRIBUTING.md](../../CONTRIBUTING.md) về thiết lập môi trường phát triển và các bước kiểm tra, [hướng dẫn kiểm thử](../testing.md) về các luồng xác minh và [sơ đồ kiến trúc](../architecture.mermaid) về cấu trúc dự án.
## Giấy phép
-[MIT](LICENSE)
+[MIT](../../LICENSE)
REA-authored code is licensed under MIT. Third-party material retains its own
-licenses and copyright notices; see [Third-party notices](THIRD_PARTY_NOTICES.md)
+licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
for the dependency inventory, preserved license texts, and external-tool terms.
**You must legally obtain and be authorized to use commercial tools**, including
diff --git a/README_zh-TW.md b/docs/readme/README_zh-TW.md
similarity index 69%
rename from README_zh-TW.md
rename to docs/readme/README_zh-TW.md
index 62c9ab2f3..1ddbbd26e 100644
--- a/README_zh-TW.md
+++ b/docs/readme/README_zh-TW.md
@@ -1,6 +1,6 @@
-[English](README.md) · [简体中文](README_zh.md) · **繁體中文** · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+[English](../../README.md) · [简体中文](README_zh.md) · **繁體中文** · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA:逆向一切
@@ -9,10 +9,10 @@
**看到喜歡的功能,弄清它的工作原理,深入到二進位層面。**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#current-status)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
@@ -25,7 +25,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
加入逆向工程社群
Discord · 問答 · 成果分享
@@ -51,7 +51,7 @@
REA 將 AI 代理連接到分析工具,用於檢查原生二進位檔案、JavaScript 和 Electron 應用、.NET 程式集以及網站。你也可以在終端中使用同樣的工具。分析在本機執行,結果包含支撐各項結論的證據和相關限制。
-設定流程會向 AI 代理註冊 REA,並安裝對應的工作流程指引。原生分析可以使用已有的 Hopper 或 Ghidra;設定流程也可以在你核准後安裝 Hopper。靜態 JavaScript 分析不需要這兩個引擎。
+設定流程會向 AI 代理註冊 REA,並安裝對應的工作流程指引。原生分析可以使用已有的 Hopper、Ghidra 或 IDA;設定流程也可以在你核准後安裝 Hopper。靜態 JavaScript 分析不需要原生分析引擎。
> **[訪問 REA 網站](https://rea.tools/zh-TW/)**,查看設定說明、圖解指南和真實案例。
@@ -67,7 +67,7 @@ npx rea-agents setup
選擇 AI 代理,檢查計劃中的變更並核准。設定流程會添加 REA 的 MCP 服務和對應的工作流程指引,並備份已有組態。完成後重新啟動 AI 代理。
-設定流程支援 Claude Code、Codex、Cursor、Gemini CLI、Grok Build 和[其他 AI 代理](docs/installation.md#supported-agents)。提供者組態和手動註冊 MCP 的方法見[安裝與設定](docs/installation.md)。
+設定流程支援 Claude Code、Codex、Cursor、Gemini CLI、Grok Build 和[其他 AI 代理](../installation.md#supported-agents)。提供者組態和手動註冊 MCP 的方法見[安裝與設定](../installation.md)。
### 詢問 AI 代理
@@ -94,18 +94,20 @@ npm install --global rea-agents
rea --help
```
-原生分析需要先設定分析提供者。原生命令、提供者選擇、快照和腳本用法見 [CLI 與 Evidence 指南](docs/cli.md)。
+原生分析需要先設定分析提供者。原生命令、提供者選擇、快照和腳本用法見 [CLI 與 Evidence 指南](../cli.md)。
### 更新 REA
REA 更新較快,新版本經常包含錯誤修復。請將安裝的版本保持最新。
-通過 npm 安裝的 CLI 使用:
+通過 `npm install --global` 或 `bun add --global` 安裝的 CLI 使用:
```bash
rea update
```
+Bun 全域更新需要 Bun 1.2.17 或更新版本。如果使用較舊的 Bun 版本,請先手動升級 Bun,再執行 `rea update`。
+
要更新 AI 代理註冊和 skill,請執行更新命令輸出的設定命令。
如果使用 `npx`,通過以下命令更新 AI 代理設定:
@@ -114,15 +116,15 @@ rea update
npx rea-agents@latest setup
```
-檢查設定變更並重新啟動 AI 代理。單次 CLI 操作可使用 `npx rea-agents@latest`,後接所需命令。
+檢查設定變更並重新啟動 AI 代理。單次 CLI 操作可使用 `npx rea-agents@latest`,後接所需命令。使用 Bun 時,用 `bunx rea-agents@latest` 代替 `npx rea-agents@latest`。
## REA 的工作原理
AI 代理通過 MCP 呼叫 REA,檢查目標並追蹤相關程式碼。REA 返回發現及對應證據。AI 代理據此提出後續問題、解釋行為,或編寫並測試實現。CLI 命令使用同樣的工作流程。
-
+
-[打開完整尺寸的圖示](website/public/assets/figures/rea-investigation-flow.svg)。
+[打開完整尺寸的圖示](../../website/public/assets/figures/rea-investigation-flow.svg)。
@@ -130,37 +132,39 @@ AI 代理通過 MCP 呼叫 REA,檢查目標並追蹤相關程式碼。REA 返
REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。額外工具和主機支援取決於分析目標:
-| 目標 | REA 返回的內容 | 要求與指南 |
-| --------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
-| 原生二進位檔案 | 偽程式碼、組合語言、字串、符號、呼叫和引用 | Hopper、Ghidra 或 IDA;[原生分析](https://rea.tools/zh-TW/guides/native/) |
-| 離線 ELF 佈局 | 節、段、原始符號/重定位資訊及靜態防護機制候選項 | Linux x64 上由呼叫方提供的 pwntools;[二進位診斷](docs/binary-diagnostics.md) |
-| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | Linux x64 上的本地原始位元組/十六進位輸入載體;[離線 EVM 指南](docs/evm-bytecode.md) |
-| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](docs/recorded-crashes.md) |
-| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](https://rea.tools/zh-TW/guides/javascript/) |
-| 網站 | 頁面結構、腳本、網路觀察結果和按請求獲取的螢幕擷取畫面 | Chrome 系瀏覽器;[瀏覽器分析](https://rea.tools/zh-TW/guides/browser/) |
-| 已保存的網路擷取 | 請求、回應、可訪問的酬載和來源位置 | HAR;原生 mitmproxy 擷取需要 Linux 上的 mitmdump;[擷取指南](docs/web-network-captures.md) |
-| 封包擷取 | 端點、DNS、TLS SNI、通訊協定分佈和 HTTP 請求 | 呼叫端提供的 tshark 和 capinfos;[pcap 指南](docs/pcap-analysis.md) |
-| .NET 程式集 | 中繼資料、CIL 指令、聲明的原生依賴和建置對比 | 靜態檢查;[托管程式碼指南](docs/managed-code-analysis.md) |
-| Android APK | 清單聲明、類別、反編譯的方法和引用 | Linux/macOS/Windows x64 上的無界面 JADX 和完整 JDK;[Android 指南](docs/android-analysis.md) |
-| Android 裝置 | 裝置、套件、程序、日誌、傾印、螢幕擷取以及 APK/檔案傳輸 | 呼叫端提供的 adb(模擬器或實體裝置);[ADB 指南](docs/adb-device-analysis.md) |
-| JEB 專案 | 引擎支援的專案單元、成品摘要和型別/方法的虛擬碼 | 由呼叫端啟動、提供 MCP 的 JEB 用戶端,透過 REA_JEB_MCP_URL 選擇;[JEB 指南](docs/jeb-analysis.md) |
-| Android 資源 | 解碼後的清單、字串表、語系和版本資訊 | 呼叫端提供的 Apktool 啟動器;[資源指南](docs/apktool-resource-analysis.md) |
-| Flutter 應用 | Dart 快照雜湊、依 ABI 的程式庫雜湊及引擎建置來源 | 無(靜態 APK 解析);[Flutter 指南](docs/flutter-analysis.md) |
-| 韌體 | 區域、提取結果和轉交原生分析的內容 | Linux 上的 Binwalk / Unblob;[韌體指南](docs/firmware-analysis.md) |
-| 封包記錄 | 指定欄位、重複值、篩選條件與解碼規則 | Linux: TShark 4.x + prlimit; [封包分析指南](docs/packet-captures.md) |
-| 錄製的邏輯訊號 | 解碼註解、樣本範圍與診斷資訊 | Linux: sigrok-cli 0.7.2 + prlimit; [訊號記錄指南](docs/signal-recordings.md) |
-| 軟體包與資源 | 檔案清單、摘要、plist、Apple bundle 結構和提取的資源 | [製品與 JavaScript 指南](docs/javascript-artifact-reconstruction.md)、[Apple 應用](docs/apple-application-analysis.md) |
-| 處理程序行為 | 終端輸出、互動、退出和檔案系統觀察結果,以及執行對比 | 支援原生 PTY 的 Linux/macOS;[處理程序擷取](docs/process-capture.md) |
+| 目標 | REA 返回的內容 | 要求與指南 |
+| --------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
+| 原生二進位檔案 | 偽程式碼、組合語言、字串、符號、呼叫和引用 | Hopper、Ghidra 或 IDA;[原生分析](https://rea.tools/zh-TW/guides/native/) |
+| 離線 ELF 佈局 | 節、段、原始符號/重定位資訊及靜態防護機制候選項 | Linux x64 上由呼叫方提供的 pwntools;[二進位診斷](../binary-diagnostics.md) |
+| Go 二進位檔案 | 內嵌編譯器版本、模組、替換項和建置設定 | 內建靜態 ELF/PE/thin Mach-O 檢查;無需安裝 Go |
+| WASM 工件 | 驗證、節、精確的匯入/匯出 WAT 形式及解碼後的 WAT | 呼叫方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
+| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | Linux x64 上的本地原始位元組/十六進位輸入載體;[離線 EVM 指南](../evm-bytecode.md) |
+| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](../recorded-crashes.md) |
+| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](https://rea.tools/zh-TW/guides/javascript/) |
+| 網站 | 頁面結構、腳本、網路觀察結果和按請求獲取的螢幕擷取畫面 | Chrome 系瀏覽器;[瀏覽器分析](https://rea.tools/zh-TW/guides/browser/) |
+| 已保存的網路擷取 | 請求、回應、可訪問的酬載和來源位置 | HAR;原生 mitmproxy 擷取需要 Linux 上的 mitmdump;[擷取指南](../web-network-captures.md) |
+| 封包擷取 | 端點、DNS、TLS SNI、通訊協定分佈和 HTTP 請求 | 呼叫端提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
+| .NET 程式集 | 中繼資料、CIL 指令、聲明的原生依賴和建置對比 | 靜態檢查;[托管程式碼指南](../managed-code-analysis.md) |
+| Android APK | 清單聲明、類別、反編譯的方法和引用 | Linux/macOS/Windows x64 上的無界面 JADX 和完整 JDK;[Android 指南](../android-analysis.md) |
+| Android 裝置 | 裝置、套件、程序、日誌、傾印、螢幕擷取以及 APK/檔案傳輸 | 呼叫端提供的 adb(模擬器或實體裝置);[ADB 指南](../adb-device-analysis.md) |
+| JEB 專案 | 引擎支援的專案單元、成品摘要和型別/方法的虛擬碼 | 由呼叫端啟動、提供 MCP 的 JEB 用戶端,透過 REA_JEB_MCP_URL 選擇;[JEB 指南](../jeb-analysis.md) |
+| Android 資源 | 解碼後的清單、字串表、語系和版本資訊 | 呼叫端提供的 Apktool 啟動器;[資源指南](../apktool-resource-analysis.md) |
+| Flutter 應用 | Dart 快照雜湊、依 ABI 的程式庫雜湊及引擎建置來源 | 無(靜態 APK 解析);[Flutter 指南](../flutter-analysis.md) |
+| 韌體 | 區域、提取結果和轉交原生分析的內容 | Linux 上的 Binwalk / Unblob;[韌體指南](../firmware-analysis.md) |
+| 封包記錄 | 指定欄位、重複值、篩選條件與解碼規則 | Linux: TShark 4.x + prlimit; [封包分析指南](../packet-captures.md) |
+| 錄製的邏輯訊號 | 解碼註解、樣本範圍與診斷資訊 | Linux: sigrok-cli 0.7.2 + prlimit; [訊號記錄指南](../signal-recordings.md) |
+| 軟體包與資源 | 檔案清單、摘要、plist、Apple bundle 結構和提取的資源 | [製品與 JavaScript 指南](../javascript-artifact-reconstruction.md)、[Apple 應用](../apple-application-analysis.md) |
+| 處理程序行為 | 終端輸出、互動、退出和檔案系統觀察結果,以及執行對比 | 支援原生 PTY 的 Linux/macOS;[處理程序擷取](../process-capture.md) |
靜態 JavaScript 和 .NET 檢查讀取提供的檔案,不執行應用。執行階段擷取會以你的使用者權限執行目標或與之互動;各執行階段指南說明瞭具體影響。
-不同提供者支援的原生格式和主機系統有所不同。請參閱 [Hopper 與 Ghidra 設定](docs/installation.md#hopper)、[IDA 指南](docs/ida-provider.md)和[實驗性 Windows Ghidra 支援](docs/windows-ghidra-p0.md)。Ghidra 也支援 [16 位 DOS 分析](docs/ghidra-dos.md)。提供者選擇見 [CLI 指南](docs/cli.md#choose-a-provider)。對於最新 npm 版本發佈後新增的功能,請查看[發佈可用性](docs/installation.md#released-package-and-main)。
+不同提供者支援的原生格式和主機系統有所不同。請參閱 [Hopper 與 Ghidra 設定](../installation.md#hopper)、[IDA 指南](../ida-provider.md)和[實驗性 Windows Ghidra 支援](../windows-ghidra-p0.md)。Ghidra 也支援 [16 位 DOS 分析](../ghidra-dos.md)。提供者選擇見 [CLI 指南](../cli.md#choose-a-provider)。對於最新 npm 版本發佈後新增的功能,請查看[發佈可用性](../installation.md#released-package-and-main)。
## 案例
-[](https://rea.tools/zh-TW/showcase/)
+[](https://rea.tools/zh-TW/showcase/)
### DX-Ball:重建聲像計算
@@ -189,28 +193,28 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
哪些 AI 代理可以使用 REA?
-任何支援本地 MCP 服務的 AI 代理都可以使用。設定流程會設定[受支援的 AI 代理](docs/installation.md#supported-agents);其他客戶端可通過[手動註冊 MCP](docs/installation.md#mcp-registry) 使用。
+任何支援本地 MCP 服務的 AI 代理都可以使用。設定流程會設定[受支援的 AI 代理](../installation.md#supported-agents);其他客戶端可通過[手動註冊 MCP](../installation.md#mcp-registry) 使用。
需要 Hopper、Ghidra 或 IDA 嗎?
-深入的原生分析需要其中一個引擎。靜態 JavaScript 和 .NET 檢查不需要原生分析引擎。設定流程可以在你核准後安裝 Hopper;Ghidra 和 IDA 則使用已有安裝。見[提供者設定](docs/installation.md#hopper)。
+深入的原生分析需要其中一個引擎。靜態 JavaScript 和 .NET 檢查不需要原生分析引擎。設定流程可以在你核准後安裝 Hopper;Ghidra 和 IDA 則使用已有安裝。見[提供者設定](../installation.md#hopper)。
需要先啟動 Hopper 嗎?
-REA 會在操作需要時啟動 Hopper。在 macOS 上,首次執行時可能會出現對話框,讓你選擇演示模式或啟用授權。見 [Hopper 啟動與故障排查](docs/installation.md#launcher-paths-and-troubleshooting)。
+REA 會在操作需要時啟動 Hopper。在 macOS 上,首次執行時可能會出現對話框,讓你選擇演示模式或啟用授權。見 [Hopper 啟動與故障排查](../installation.md#launcher-paths-and-troubleshooting)。
從 skills.sh 安裝 skill 有什麼作用?
-skill 為 AI 代理提供調查指引。使用 `npx rea-agents setup` 註冊 REA 的 MCP 服務並安裝對應的指引,然後重新啟動 AI 代理。見[僅安裝 skill](docs/installation.md#skill-only-installation)。
+skill 為 AI 代理提供調查指引。使用 `npx rea-agents setup` 註冊 REA 的 MCP 服務並安裝對應的指引,然後重新啟動 AI 代理。見[僅安裝 skill](../installation.md#skill-only-installation)。
@@ -253,14 +257,14 @@ npx rea-agents@latest setup
可以先閱讀網站上的[實操指南](https://rea.tools/zh-TW/guides/)。具體選項、前提條件和結果契約見:
-- [安裝與設定](docs/installation.md):AI 代理註冊、提供者組態、更新和卸載。
-- [就緒檢查與故障排查](docs/installation.md#check-readiness-for-your-task):診斷特定 AI 代理或分析引擎。
-- [CLI 與 Evidence](docs/cli.md):命令、提供者選擇、快照、匯入/匯出和退出狀態。
-- [MCP 契約](docs/mcp-contracts.md)和[AI 代理提示詞](docs/mcp-prompts.md):工具結果、會話和引導式調查。
-- [工具目錄](docs/mcp-contracts.md#generated-catalog):建置時生成的工具、提供者和 CLI 命令清單。
-- [路線圖](docs/roadmap.md):計劃中的工作和能力跟蹤。
+- [安裝與設定](../installation.md):AI 代理註冊、提供者組態、更新和卸載。
+- [就緒檢查與故障排查](../installation.md#check-readiness-for-your-task):診斷特定 AI 代理或分析引擎。
+- [CLI 與 Evidence](../cli.md):命令、提供者選擇、快照、匯入/匯出和退出狀態。
+- [MCP 契約](../mcp-contracts.md)和[AI 代理提示詞](../mcp-prompts.md):工具結果、會話和引導式調查。
+- [工具目錄](../mcp-contracts.md#generated-catalog):建置時生成的工具、提供者和 CLI 命令清單。
+- [路線圖](../roadmap.md):計劃中的工作和能力跟蹤。
-請按照 [SECURITY.md](SECURITY.md) 報告漏洞。
+請按照 [SECURITY.md](../../SECURITY.md) 報告漏洞。
## Star 歷史
@@ -278,7 +282,7 @@ npx rea-agents@latest setup
## 免責聲明
-負責任使用的相關指引請見 [DISCLAIMER.md](DISCLAIMER.md)。
+負責任使用的相關指引請見 [DISCLAIMER.md](../../DISCLAIMER.md)。
REA 用於合法的研究、教育、互通性、軟體保存,以及分析你擁有或獲准檢查的軟體。
@@ -289,14 +293,14 @@ REA 用於合法的研究、教育、互通性、軟體保存,以及分析你
歡迎幫助改進 REA!你可以[提交 issue](https://github.com/morluto/rea/issues) 報告錯誤或建議功能,也可以[提交 pull request](https://github.com/morluto/rea/pulls) 改進程式碼或文件。
-開發設定和檢查見 [CONTRIBUTING.md](CONTRIBUTING.md),驗證流程見[測試指南](docs/testing.md),專案結構見[架構圖](docs/architecture.mermaid)。
+開發設定和檢查見 [CONTRIBUTING.md](../../CONTRIBUTING.md),驗證流程見[測試指南](../testing.md),專案結構見[架構圖](../architecture.mermaid)。
## 授權條款
-[MIT](LICENSE)
+[MIT](../../LICENSE)
REA 自行編寫的程式碼採用 MIT 授權;第三方內容保留各自的授權和著作權聲明。
-依賴清單、授權原文及外部工具條款見[第三方聲明](THIRD_PARTY_NOTICES.md)。
+依賴清單、授權原文及外部工具條款見[第三方聲明](../../THIRD_PARTY_NOTICES.md)。
**使用者須自行合法取得商用工具及所需使用權**,包括 IDA Pro / Hex-Rays 反編譯器、
Hopper 和 JEB,並遵守各自授權。REA 不提供工具授權、金鑰或啟用權利;試用版和
diff --git a/README_zh.md b/docs/readme/README_zh.md
similarity index 69%
rename from README_zh.md
rename to docs/readme/README_zh.md
index c8d66ce08..6e70fd3c3 100644
--- a/README_zh.md
+++ b/docs/readme/README_zh.md
@@ -1,6 +1,6 @@
-[English](README.md) · **简体中文** · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+[English](../../README.md) · **简体中文** · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA:逆向一切
@@ -9,10 +9,10 @@
**看到喜欢的功能,弄清它的工作原理,深入到二进制层面。**
[](https://www.npmjs.com/package/rea-agents)
-[](docs/mcp-contracts.md#generated-catalog)
+[](../mcp-contracts.md#generated-catalog)
[](#current-status)
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
-[](LICENSE)
+[](../../LICENSE)
[](https://discord.gg/GkcryMnJDM)
@@ -25,7 +25,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
加入逆向工程社区
Discord · 问答 · 成果分享
@@ -51,7 +51,7 @@
REA 将智能体连接到分析工具,用于检查原生二进制文件、JavaScript 和 Electron 应用、.NET 程序集以及网站。你也可以在终端中使用同样的工具。分析在本机运行,结果包含支撑各项结论的证据和相关限制。
-设置流程会向智能体注册 REA,并安装匹配的工作流指引。原生分析可以使用已有的 Hopper 或 Ghidra;设置流程也可以在你批准后安装 Hopper。静态 JavaScript 分析不需要这两个引擎。
+设置流程会向智能体注册 REA,并安装匹配的工作流指引。原生分析可以使用已有的 Hopper、Ghidra 或 IDA;设置流程也可以在你批准后安装 Hopper。静态 JavaScript 分析不需要原生分析引擎。
> **[访问 REA 网站](https://rea.tools/zh-CN/)**,查看设置说明、图解指南和真实案例。
@@ -67,7 +67,7 @@ npx rea-agents setup
选择智能体,检查计划中的变更并批准。设置流程会添加 REA 的 MCP 服务和匹配的工作流指引,并备份已有配置。完成后重启智能体。
-设置流程支持 Claude Code、Codex、Cursor、Gemini CLI、Grok Build 和[其他智能体](docs/installation.md#supported-agents)。提供方配置和手动注册 MCP 的方法见[安装与设置](docs/installation.md)。
+设置流程支持 Claude Code、Codex、Cursor、Gemini CLI、Grok Build 和[其他智能体](../installation.md#supported-agents)。提供方配置和手动注册 MCP 的方法见[安装与设置](../installation.md)。
### 询问智能体
@@ -94,18 +94,20 @@ npm install --global rea-agents
rea --help
```
-原生分析需要先配置分析提供方。原生命令、提供方选择、快照和脚本用法见 [CLI 与 Evidence 指南](docs/cli.md)。
+原生分析需要先配置分析提供方。原生命令、提供方选择、快照和脚本用法见 [CLI 与 Evidence 指南](../cli.md)。
### 更新 REA
REA 更新较快,新版本经常包含错误修复。请保持安装的版本为最新。
-通过 npm 安装的 CLI 使用:
+通过 `npm install --global` 或 `bun add --global` 安装的 CLI 使用:
```bash
rea update
```
+Bun 全局更新需要 Bun 1.2.17 或更高版本。如果使用较旧的 Bun 版本,请先手动升级 Bun,再运行 `rea update`。
+
要更新智能体注册和 skill,请运行更新命令输出的设置命令。
如果使用 `npx`,通过以下命令更新智能体设置:
@@ -114,15 +116,15 @@ rea update
npx rea-agents@latest setup
```
-检查设置变更并重启智能体。单次 CLI 操作可使用 `npx rea-agents@latest`,后接所需命令。
+检查设置变更并重启智能体。单次 CLI 操作可使用 `npx rea-agents@latest`,后接所需命令。使用 Bun 时,用 `bunx rea-agents@latest` 代替 `npx rea-agents@latest`。
## REA 的工作原理
智能体通过 MCP 调用 REA,检查目标并追踪相关代码。REA 返回发现及对应证据。智能体据此提出后续问题、解释行为,或编写并测试实现。CLI 命令使用同样的工作流。
-
+
-[打开完整尺寸的图示](website/public/assets/figures/rea-investigation-flow.svg)。
+[打开完整尺寸的图示](../../website/public/assets/figures/rea-investigation-flow.svg)。
@@ -130,37 +132,39 @@ npx rea-agents@latest setup
REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。额外工具和主机支持取决于分析目标:
-| 目标 | REA 返回的内容 | 要求与指南 |
-| --------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
-| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](https://rea.tools/zh-CN/guides/native/) |
-| 离线 ELF 布局 | 节、段、原始符号/重定位信息及静态防护机制候选项 | Linux x64 上由调用方提供的 pwntools;[二进制诊断](docs/binary-diagnostics.md) |
-| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | Linux x64 上的本地原始字节/十六进制输入载体;[离线 EVM 指南](docs/evm-bytecode.md) |
-| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](docs/recorded-crashes.md) |
-| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](https://rea.tools/zh-CN/guides/javascript/) |
-| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](https://rea.tools/zh-CN/guides/browser/) |
-| 已保存的网络捕获 | 请求、响应、可访问的载荷和来源位置 | HAR;原生 mitmproxy 捕获需要 Linux 上的 mitmdump;[捕获指南](docs/web-network-captures.md) |
-| 数据包捕获 | 端点、DNS、TLS SNI、协议分布和 HTTP 请求 | 调用方提供的 tshark 和 capinfos;[pcap 指南](docs/pcap-analysis.md) |
-| .NET 程序集 | 元数据、CIL 指令、声明的原生依赖和构建对比 | 静态检查;[托管代码指南](docs/managed-code-analysis.md) |
-| Android APK | 清单声明、类、反编译的方法和引用 | Linux/macOS/Windows x64 上的无界面 JADX 和完整 JDK;[Android 指南](docs/android-analysis.md) |
-| Android 设备 | 设备、软件包、进程、日志、转储、屏幕截图以及 APK/文件传输 | 调用方提供的 adb(模拟器或实体设备);[ADB 指南](docs/adb-device-analysis.md) |
-| JEB 项目 | 引擎支持的项目单元、工件摘要和类型/方法的伪代码 | 由调用方启动、提供 MCP 的 JEB 客户端,通过 REA_JEB_MCP_URL 选择;[JEB 指南](docs/jeb-analysis.md) |
-| Android 资源 | 解码后的清单、字符串表、语言区域和版本信息 | 调用方提供的 Apktool 启动器;[资源指南](docs/apktool-resource-analysis.md) |
-| Flutter 应用 | Dart 快照哈希、按 ABI 的库摘要及引擎构建溯源 | 无(静态 APK 解析);[Flutter 指南](docs/flutter-analysis.md) |
-| 固件 | 区域、提取结果和转交原生分析的内容 | Linux 上的 Binwalk / Unblob;[固件指南](docs/firmware-analysis.md) |
-| 抓包文件 | 指定字段、重复值、过滤条件与解码规则 | Linux: TShark 4.x + prlimit; [抓包分析指南](docs/packet-captures.md) |
-| 录制的逻辑信号 | 解码注释、样本范围和诊断信息 | Linux: sigrok-cli 0.7.2 + prlimit; [信号记录指南](docs/signal-recordings.md) |
-| 软件包与资源 | 文件清单、摘要、plist、Apple bundle 结构和提取的资源 | [制品与 JavaScript 指南](docs/javascript-artifact-reconstruction.md)、[Apple 应用](docs/apple-application-analysis.md) |
-| 进程行为 | 终端输出、交互、退出和文件系统观察结果,以及运行对比 | 支持原生 PTY 的 Linux/macOS;[进程捕获](docs/process-capture.md) |
+| 目标 | REA 返回的内容 | 要求与指南 |
+| --------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
+| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](https://rea.tools/zh-CN/guides/native/) |
+| 离线 ELF 布局 | 节、段、原始符号/重定位信息及静态防护机制候选项 | Linux x64 上由调用方提供的 pwntools;[二进制诊断](../binary-diagnostics.md) |
+| Go 二进制文件 | 内嵌编译器版本、模块、替换项和构建设置 | 内置静态 ELF/PE/thin Mach-O 检查;无需安装 Go |
+| WASM 工件 | 验证、节、精确的导入/导出 WAT 形式及解码后的 WAT | 调用方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
+| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | Linux x64 上的本地原始字节/十六进制输入载体;[离线 EVM 指南](../evm-bytecode.md) |
+| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](../recorded-crashes.md) |
+| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](https://rea.tools/zh-CN/guides/javascript/) |
+| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](https://rea.tools/zh-CN/guides/browser/) |
+| 已保存的网络捕获 | 请求、响应、可访问的载荷和来源位置 | HAR;原生 mitmproxy 捕获需要 Linux 上的 mitmdump;[捕获指南](../web-network-captures.md) |
+| 数据包捕获 | 端点、DNS、TLS SNI、协议分布和 HTTP 请求 | 调用方提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
+| .NET 程序集 | 元数据、CIL 指令、声明的原生依赖和构建对比 | 静态检查;[托管代码指南](../managed-code-analysis.md) |
+| Android APK | 清单声明、类、反编译的方法和引用 | Linux/macOS/Windows x64 上的无界面 JADX 和完整 JDK;[Android 指南](../android-analysis.md) |
+| Android 设备 | 设备、软件包、进程、日志、转储、屏幕截图以及 APK/文件传输 | 调用方提供的 adb(模拟器或实体设备);[ADB 指南](../adb-device-analysis.md) |
+| JEB 项目 | 引擎支持的项目单元、工件摘要和类型/方法的伪代码 | 由调用方启动、提供 MCP 的 JEB 客户端,通过 REA_JEB_MCP_URL 选择;[JEB 指南](../jeb-analysis.md) |
+| Android 资源 | 解码后的清单、字符串表、语言区域和版本信息 | 调用方提供的 Apktool 启动器;[资源指南](../apktool-resource-analysis.md) |
+| Flutter 应用 | Dart 快照哈希、按 ABI 的库摘要及引擎构建溯源 | 无(静态 APK 解析);[Flutter 指南](../flutter-analysis.md) |
+| 固件 | 区域、提取结果和转交原生分析的内容 | Linux 上的 Binwalk / Unblob;[固件指南](../firmware-analysis.md) |
+| 抓包文件 | 指定字段、重复值、过滤条件与解码规则 | Linux: TShark 4.x + prlimit; [抓包分析指南](../packet-captures.md) |
+| 录制的逻辑信号 | 解码注释、样本范围和诊断信息 | Linux: sigrok-cli 0.7.2 + prlimit; [信号记录指南](../signal-recordings.md) |
+| 软件包与资源 | 文件清单、摘要、plist、Apple bundle 结构和提取的资源 | [制品与 JavaScript 指南](../javascript-artifact-reconstruction.md)、[Apple 应用](../apple-application-analysis.md) |
+| 进程行为 | 终端输出、交互、退出和文件系统观察结果,以及运行对比 | 支持原生 PTY 的 Linux/macOS;[进程捕获](../process-capture.md) |
静态 JavaScript 和 .NET 检查读取提供的文件,不运行应用。运行时捕获会以你的用户权限运行目标或与之交互;各运行时指南说明了具体影响。
-不同提供方支持的原生格式和主机系统有所不同。请参阅 [Hopper 与 Ghidra 设置](docs/installation.md#hopper)、[IDA 指南](docs/ida-provider.md)和[实验性 Windows Ghidra 支持](docs/windows-ghidra-p0.md)。Ghidra 也支持 [16 位 DOS 分析](docs/ghidra-dos.md)。提供方选择见 [CLI 指南](docs/cli.md#choose-a-provider)。对于最新 npm 版本发布后新增的功能,请查看[发布可用性](docs/installation.md#released-package-and-main)。
+不同提供方支持的原生格式和主机系统有所不同。请参阅 [Hopper 与 Ghidra 设置](../installation.md#hopper)、[IDA 指南](../ida-provider.md)和[实验性 Windows Ghidra 支持](../windows-ghidra-p0.md)。Ghidra 也支持 [16 位 DOS 分析](../ghidra-dos.md)。提供方选择见 [CLI 指南](../cli.md#choose-a-provider)。对于最新 npm 版本发布后新增的功能,请查看[发布可用性](../installation.md#released-package-and-main)。
## 案例
-[](https://rea.tools/zh-CN/showcase/)
+[](https://rea.tools/zh-CN/showcase/)
### DX-Ball:重建声像计算
@@ -189,28 +193,28 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
哪些智能体可以使用 REA?
-任何支持本地 MCP 服务的智能体都可以使用。设置流程会配置[受支持的智能体](docs/installation.md#supported-agents);其他客户端可通过[手动注册 MCP](docs/installation.md#mcp-registry) 使用。
+任何支持本地 MCP 服务的智能体都可以使用。设置流程会配置[受支持的智能体](../installation.md#supported-agents);其他客户端可通过[手动注册 MCP](../installation.md#mcp-registry) 使用。
需要 Hopper、Ghidra 或 IDA 吗?
-深入的原生分析需要其中一个引擎。静态 JavaScript 和 .NET 检查不需要原生分析引擎。设置流程可以在你批准后安装 Hopper;Ghidra 和 IDA 则使用已有安装。见[提供方设置](docs/installation.md#hopper)。
+深入的原生分析需要其中一个引擎。静态 JavaScript 和 .NET 检查不需要原生分析引擎。设置流程可以在你批准后安装 Hopper;Ghidra 和 IDA 则使用已有安装。见[提供方设置](../installation.md#hopper)。
需要先启动 Hopper 吗?
-REA 会在操作需要时启动 Hopper。在 macOS 上,首次运行时可能会出现对话框,让你选择演示模式或激活许可证。见 [Hopper 启动与故障排查](docs/installation.md#launcher-paths-and-troubleshooting)。
+REA 会在操作需要时启动 Hopper。在 macOS 上,首次运行时可能会出现对话框,让你选择演示模式或激活许可证。见 [Hopper 启动与故障排查](../installation.md#launcher-paths-and-troubleshooting)。
从 skills.sh 安装 skill 有什么作用?
-skill 为智能体提供调查指引。使用 `npx rea-agents setup` 注册 REA 的 MCP 服务并安装匹配的指引,然后重启智能体。见[仅安装 skill](docs/installation.md#skill-only-installation)。
+skill 为智能体提供调查指引。使用 `npx rea-agents setup` 注册 REA 的 MCP 服务并安装匹配的指引,然后重启智能体。见[仅安装 skill](../installation.md#skill-only-installation)。
@@ -253,14 +257,14 @@ npx rea-agents@latest setup
可以先阅读网站上的[实操指南](https://rea.tools/zh-CN/guides/)。具体选项、前提条件和结果契约见:
-- [安装与设置](docs/installation.md):智能体注册、提供方配置、更新和卸载。
-- [就绪检查与故障排查](docs/installation.md#check-readiness-for-your-task):诊断特定智能体或分析引擎。
-- [CLI 与 Evidence](docs/cli.md):命令、提供方选择、快照、导入/导出和退出状态。
-- [MCP 契约](docs/mcp-contracts.md)和[智能体提示词](docs/mcp-prompts.md):工具结果、会话和引导式调查。
-- [工具目录](docs/mcp-contracts.md#generated-catalog):构建时生成的工具、提供方和 CLI 命令清单。
-- [路线图](docs/roadmap.md):计划中的工作和能力跟踪。
+- [安装与设置](../installation.md):智能体注册、提供方配置、更新和卸载。
+- [就绪检查与故障排查](../installation.md#check-readiness-for-your-task):诊断特定智能体或分析引擎。
+- [CLI 与 Evidence](../cli.md):命令、提供方选择、快照、导入/导出和退出状态。
+- [MCP 契约](../mcp-contracts.md)和[智能体提示词](../mcp-prompts.md):工具结果、会话和引导式调查。
+- [工具目录](../mcp-contracts.md#generated-catalog):构建时生成的工具、提供方和 CLI 命令清单。
+- [路线图](../roadmap.md):计划中的工作和能力跟踪。
-请按照 [SECURITY.md](SECURITY.md) 报告漏洞。
+请按照 [SECURITY.md](../../SECURITY.md) 报告漏洞。
## Star 历史
@@ -278,7 +282,7 @@ npx rea-agents@latest setup
## 免责声明
-负责任使用的相关指引请见 [DISCLAIMER.md](DISCLAIMER.md)。
+负责任使用的相关指引请见 [DISCLAIMER.md](../../DISCLAIMER.md)。
REA 用于合法的研究、教育、互操作性、软件保存,以及分析你拥有或获准检查的软件。
@@ -289,14 +293,14 @@ REA 用于合法的研究、教育、互操作性、软件保存,以及分析
欢迎帮助改进 REA!你可以[提交 issue](https://github.com/morluto/rea/issues) 报告错误或建议功能,也可以[提交 pull request](https://github.com/morluto/rea/pulls) 改进代码或文档。
-开发设置和检查见 [CONTRIBUTING.md](CONTRIBUTING.md),验证流程见[测试指南](docs/testing.md),项目结构见[架构图](docs/architecture.mermaid)。
+开发设置和检查见 [CONTRIBUTING.md](../../CONTRIBUTING.md),验证流程见[测试指南](../testing.md),项目结构见[架构图](../architecture.mermaid)。
## 许可证
-[MIT](LICENSE)
+[MIT](../../LICENSE)
REA 自行编写的代码采用 MIT 许可;第三方内容保留各自的许可和著作权声明。
-依赖清单、许可原文及外部工具条款见[第三方声明](THIRD_PARTY_NOTICES.md)。
+依赖清单、许可原文及外部工具条款见[第三方声明](../../THIRD_PARTY_NOTICES.md)。
**用户须自行合法取得商用工具及所需使用权**,包括 IDA Pro / Hex-Rays 反编译器、
Hopper 和 JEB,并遵守各自许可。REA 不提供工具许可、密钥或激活权利;试用版和
diff --git a/docs/recorded-crashes.md b/docs/recorded-crashes.md
index 8b1a51662..bd508d051 100644
--- a/docs/recorded-crashes.md
+++ b/docs/recorded-crashes.md
@@ -1,7 +1,10 @@
# Recorded crash evidence
`inspect_recorded_crash` / `rea inspect-recorded-crash` inspect a caller-selected
-Linux ELF64 x86-64 little-endian core independently of an active binary target.
+Linux ELF64 little-endian core independently of an active binary target. Two
+architecture profiles are interpreted, x86-64 and AArch64; each binds registers
+to its own `NT_PRSTATUS` layout, and any other `e_machine` is refused as
+`unsupported_target` before note interpretation.
```sh
export REA_PWNTOOLS_PYTHON=/absolute/existing/venv/bin/python
@@ -10,8 +13,8 @@ rea inspect-recorded-crash ./crash.core --json
The selected Python needs unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2.
REA does not install these dependencies. ELFFile/its original ELF structures and
-pwntools' original Linux amd64 ctypes provide parsing and ABI layout. The adapter
-adds bounds, representation preservation and lifecycle ownership.
+pwntools' original Linux amd64 and aarch64 ctypes provide parsing and ABI layout.
+The adapter adds bounds, representation preservation and lifecycle ownership.
The initial Linux note profile accepts System V/unspecified and GNU/Linux OSABI
with ABI version zero. Other declared ABIs return `unsupported_target` before
note interpretation; an unspecified OSABI alone does not prove OS origin.
@@ -27,7 +30,8 @@ note interpretation; an unspecified OSABI alone does not prove OS origin.
- Every exact Linux CORE PRSTATUS note as a separate recorded thread, with
historical PID, signal and lossless uint64 registers bound to source bytes.
Each supported note has exactly one interpretation; each thread contains the
- complete 27-register ABI set, with names bound to their original ABI offsets.
+ complete ABI register set for its architecture (27 registers for amd64,
+ 34 for AArch64), with names bound to their original ABI offsets.
Physical note ranges cannot overlap or leave segment gaps. ELF and note headers
are independently bound to the snapshot, including extended program counts.
- Signed SIGINFO number/code/errno. Initially only SIGSEGV codes 1/2 establish the
diff --git a/docs/specs/2356-wasm-interface-comparison/plan.md b/docs/specs/2356-wasm-interface-comparison/plan.md
new file mode 100644
index 000000000..a5b9e8ac3
--- /dev/null
+++ b/docs/specs/2356-wasm-interface-comparison/plan.md
@@ -0,0 +1,10 @@
+# Plan
+
+1. Check issue/timeline and complete open/closed PR searches; review #2406.
+2. Extend the existing core-module WABT inspection in an isolated worktree.
+3. Extend producer-owned inspection with optional typed interface facts;
+ preserve unknown unsupported signatures and exact WAT forms.
+4. Add shared Evidence-only comparator and strict input/output contracts.
+5. Register additive CLI/MCP workflow and documentation.
+6. Validate original-module workflows, malformed/unknown/cancellation cases;
+ run checks, independent review, repair findings and rerun affected checks.
diff --git a/docs/specs/2356-wasm-interface-comparison/spec.md b/docs/specs/2356-wasm-interface-comparison/spec.md
new file mode 100644
index 000000000..b4dfb7778
--- /dev/null
+++ b/docs/specs/2356-wasm-interface-comparison/spec.md
@@ -0,0 +1,61 @@
+# Core WebAssembly interface comparison (#2356)
+
+## Outcome and scope
+
+Compare two exact authenticated `inspect_wasm_artifact` Evidence inputs, inline
+or retained session references, under required `scope: imports | exports | both`.
+No module acquisition or execution occurs during comparison. This is a derived
+workflow within existing WABT inspection authority, not a new provider.
+
+Producer-owned typed `interface` facts are additive and optional in historical
+inspection Evidence. WABT's validated canonical WAT owns decoding; exact original
+forms and names remain available. Comparison never reparses retained display text.
+Function/tag signatures resolve type uses; memory page size defaults to 65536 bytes and explicit custom page sizes are retained; table/memory limits and global types
+are compared exactly. Unsupported type/proposal forms remain unknown rather than
+being compared by raw text or inferred compatible. Names are exact decoded UTF-8,
+without normalization, including empty names, escapes, punctuation and BOM.
+
+## Comparison contract
+
+Return both Evidence IDs and original artifact path/digest/length. Group imports
+by exact module/name and exports by exact name, independently of kind, so a kind
+change is reported as changed. Unique admitted signatures yield added, removed,
+changed or unchanged. Duplicate keys retain every original occurrence and yield
+unknown; no Cartesian pairing or deduplication. Missing historical facets and
+incomplete inventories prevent absence claims. Unknown signatures do not erase
+independently observed entries. Ordering is deterministic by direction and names.
+
+Both subject digest/path and inspection parameters must match normalized artifact
+identity. Authenticate Evidence before projection; summary Evidence must be
+resolved to its retained complete parent by the caller. Report unknown coverage
+and explicit limitations. Exact static equality is not runtime compatibility,
+ABI compatibility, linking success or source identity.
+
+## Authority and overlap
+
+Reads supplied Evidence only. MCP records derived Evidence through the existing
+handler/ledger; CLI accepts portable inline Evidence from an explicit JSON input.
+No new provider, implicit fetch, target mutation or foreign-code execution.
+PR #2406 (head 86865035) adds component inspection, preamble routing and embedded
+core-module inspection. Its component parser is not required or duplicated here;
+shared contract/CLI registration changes remain additive. Core GC/recursive types
+are outside admitted signature semantics and keep unknown mappings.
+
+## Resource admission
+
+Typed producer rows, names and declaration tokenization are admitted before
+allocation against the existing 32 MiB WABT output representation capacity.
+Comparison uses linear grouping and cooperative transport yields; it never
+builds Cartesian duplicate pairings. CLI JSON input retains the shared file
+admission gate. Token usage and peak process-family memory remain unmeasured.
+
+## Verification
+
+Original owned modules assembled using WABT 1.0.42 cover semantically equivalent
+encodings, changes of kind/signature, names/duplicates, imported index spaces,
+tables/memory/globals/tags, unknown types and historical incomplete Evidence.
+Exercise authenticated identity, cancellation, retained lookup, CLI/MCP parity
+and advertised schemas. Run focused existing WASM/Evidence tests, repository
+checks and docs validation; independent subagent review before handoff.
+
+Format reference: https://webassembly.github.io/spec/core/binary/modules.html
diff --git a/docs/specs/2356-wasm-interface-comparison/tasks.md b/docs/specs/2356-wasm-interface-comparison/tasks.md
new file mode 100644
index 000000000..722ace2c9
--- /dev/null
+++ b/docs/specs/2356-wasm-interface-comparison/tasks.md
@@ -0,0 +1,8 @@
+# Tasks
+
+- [x] Confirm scope and overlap boundaries against #2406.
+- [x] Record spec and implementation plan.
+- [x] Implement typed producer interface and shared comparison.
+- [x] Add CLI/MCP contracts and public workflow tests.
+- [x] Run focused, static and documentation checks.
+- [x] Resolve independent review findings and rerun relevant checks.
diff --git a/docs/specs/2356-wasm-interface-comparison/verification.md b/docs/specs/2356-wasm-interface-comparison/verification.md
new file mode 100644
index 000000000..ca531e8e4
--- /dev/null
+++ b/docs/specs/2356-wasm-interface-comparison/verification.md
@@ -0,0 +1,47 @@
+# Verification
+
+Verified base: a2d8b94dd50dab8d966239dd63ab84718aa8b4b4 (fetched main).
+The change was reapplied without conflicts and relevant checks rerun.
+
+- Focused existing/new parser, provider, service, contract and CLI/MCP tests:
+ 8 files / 56 tests passed.
+- After the final partial-uniqueness correction, reran service and compiled
+ CLI/MCP boundary tests: 2 files / 5 tests passed.
+- `npm run check`: typecheck, lint, formatting, knip, module boundaries and
+ architecture guards passed. Lint reports warnings; no errors.
+- `npm run docs:generate`, `npm run docs:check` and
+ `npm run verify:test-discovery` passed; every test file was discovered exactly once.
+- Real WABT 1.0.42 comparison through compiled CLI and stdio MCP passed:
+ equivalent padded LEB128 original encodings, function signatures, all admitted
+ kinds, custom memory page size, escaped/BOM names, empty interfaces, explicit
+ scope, duplicate imports and unadmitted exception reference signatures.
+- Existing real WABT artifact verifier passed after correcting its stale inline
+ partial-observation assumption to resolve the MCP retained reference from the
+ Evidence bundle and authenticate the original record.
+
+## Independent review
+
+Three findings repaired at their owning boundary:
+
+1. Memory page size omitted: now preserved, default 65536 canonicalized.
+2. Shared signature expansion undercounted: charge every retained occurrence.
+ Reviewer independently measured the 1000-params/10000-exports case: original
+ 262956-byte WAT produced 61824014-byte JSON before correction. It now retains
+ 1949 observations in a partial facet, with 12046676-byte JSON and an explicit
+ capacity issue. Original WAT stays available; incomplete coverage prevents
+ absence and uniqueness claims.
+3. Partial singleton uniqueness: comparison now returns unknown whenever either
+ selected inventory is incomplete, including matched names.
+
+Final independent source review reported no actionable findings. Review does not
+replace the executed tests or real-tool evidence.
+
+## Scope and measurement limits
+
+On the original implementation base f254f5a, the complete comparison verifier reported 7.98 seconds elapsed, 6.56 user and
+3.13 system seconds, and 429441024 bytes maximum RSS via macOS `/usr/bin/time -l`.
+These are command-accounting measurements, not aggregate concurrent process-family
+memory or model tokens. Token usage and full process-family peak are unmeasured.
+Real-tool verification covers macOS arm64 only; Linux/Windows remain unverified.
+No module was instantiated/executed and no source was fetched. Static equality
+is not runtime compatibility. Full CI is separate from this local verification record.
diff --git a/docs/specs/solidity/cbor-metadata/plan.md b/docs/specs/solidity/cbor-metadata/plan.md
new file mode 100644
index 000000000..04b601b66
--- /dev/null
+++ b/docs/specs/solidity/cbor-metadata/plan.md
@@ -0,0 +1,33 @@
+# Implementation plan
+
+1. Use cborg 6.1.3 (Apache-2.0) as a pinned format decoder, with a bounded
+ tokenizer at its owning adapter boundary. Decode maps as Map, preserve integer
+ precision and strictly validate emitted text bytes before conversion.
+2. Add the portable metadata-result schema in src/domain/evm/. Use exact byte
+ ranges into the already retained bytecode rather than copying an opaque CBOR
+ hex payload into every selected summary.
+3. Add src/evm/SolidityMetadata.ts to interpret the terminal length/root-map
+ candidate. Return unavailable, decoded, partial or not-decoded facts; keep
+ unsupported nested values explicit instead of recursively expanding them.
+4. Compute the facts in EvmoleInterfaceWorker from its selected bytes. The host
+ provider validates ranges/identity and projects the additive metadata field.
+ Preserve raw engine output without rewriting its unknown-precision metadata.
+5. Ensure the canonical EVM bytecode shape carries the optional field into complete
+ and summary Evidence. Include decoding profile, coverage and limitations.
+6. Add producer, worker, historical contract and public MCP boundary coverage.
+ Compile an owned Solidity fixture with a pinned caller-supplied solc module,
+ and verify its CBOR compiler version. Run relevant static/architecture checks.
+7. Extend the existing real Linux CLI/MCP verifier with independent metadata
+ assertions. Run the compiler-setting matrix in its CI lane and classify its
+ verifier as EVM-owned, with a production CI-planner regression.
+
+No new execution provider or target authority is introduced: a format parser is
+added inside the existing EVM worker. Linux x64 provider admission, target-free
+semantics, cleanup, limits and complete original Evidence remain unchanged.
+No metadata JSON or source URL is automatically acquired.
+
+The parser bounds nesting to 64 and token/collection growth to the selected
+terminal payload's byte count (the format's terminal length is at most 65535).
+Every data item consumes encoded bytes; broad declared collection lengths cannot
+justify allocation without that lower bound. Expanded results still pass the
+worker's existing 16 MiB output check.
diff --git a/docs/specs/solidity/cbor-metadata/spec.md b/docs/specs/solidity/cbor-metadata/spec.md
new file mode 100644
index 000000000..ea08dcc90
--- /dev/null
+++ b/docs/specs/solidity/cbor-metadata/spec.md
@@ -0,0 +1,43 @@
+# Solidity terminal metadata inspection
+
+Issue: https://github.com/morluto/rea/issues/2388
+
+## Outcome
+
+The existing EVM interface operation should retain independently decoded terminal
+CBOR candidate facts from the exact selected bytes instead of relying on precision
+unknowns in the engine's reported metadata. This increment covers trailer ranges,
+bounded decoding, exact integer/non-text scalar key evidence and compiler-version
+representations.
+
+## Requirements
+
+- Preserve the existing carrier/decoded identities and original full bytecode.
+- Report a plausible terminal root-map candidate separately from proof of Solidity
+ origin. Missing or non-map trailers remain unavailable, not proven absent.
+- Interpret scalar root-map keys/values losslessly: integers as decimal strings,
+ bytes as hex, strings, booleans and null. Preserve undecoded/unsupported values
+ through the original bytecode plus exact candidate offset/length.
+- Report partial and not-decoded coverage, parser/profile failures and unsupported
+ CBOR forms explicitly. Do not silently coerce unknown keys into object properties.
+- Interpret admitted solc release/prerelease representations only from observed
+ entries; preserve ambiguity if duplicate compiler-version keys occur.
+- Keep the additive field compatible with historical Evidence and retain
+ it through selected summary delivery and public CLI/MCP.
+ Historical omission parses as a null unknown facet without mutating the source.
+- Bound nesting before decoder recursion and collection lengths before allocation.
+ Keep parsing inside the existing owned worker and its resource/output budgets.
+- Do not fetch metadata or source content, contact RPC or execute contract code.
+
+## Acceptance and limits
+
+Owned compiler fixtures and malformed/synthetic trailers cover integers beyond
+Number precision, non-text keys, nested/uninterpreted values, invalid UTF-8,
+invalid lengths, unsupported tags and nesting bounds. Public boundary tests
+preserve the field and older omission. A real pinned solc artifact verifies actual
+metadata facts; the existing Linux-only EVM host profile remains unchanged.
+
+## Deferred issue criteria
+
+Binding a supplied metadata JSON to IPFS/Swarm commitments is a later independent
+increment of #2388. This PR must reference rather than close the whole issue.
diff --git a/docs/specs/solidity/cbor-metadata/tasks.md b/docs/specs/solidity/cbor-metadata/tasks.md
new file mode 100644
index 000000000..e80aefa01
--- /dev/null
+++ b/docs/specs/solidity/cbor-metadata/tasks.md
@@ -0,0 +1,8 @@
+# Tasks
+
+- [x] Define single-feature requirements and implementation plan before code.
+- [x] Add the pinned decoder and bounded owner boundary.
+- [x] Implement lossless scalar metadata facts and compiler-version evidence.
+- [x] Integrate worker/provider contracts and summary/historical compatibility.
+- [x] Verify producer, worker and public boundary behavior with owned fixtures.
+- [x] Run relevant repository checks and prepare a separately scoped PR.
diff --git a/docs/specs/solidity/cbor-metadata/verification.md b/docs/specs/solidity/cbor-metadata/verification.md
new file mode 100644
index 000000000..dae558d94
--- /dev/null
+++ b/docs/specs/solidity/cbor-metadata/verification.md
@@ -0,0 +1,43 @@
+# Verification
+
+After integrating upstream c462a4ebd, 47 tests passed
+across the parser, domain contract, application service, compiled worker and MCP
+boundary (five files). Compiled-worker goldens include BOM text, partial nested
+values, unavailable trailers, malformed UTF-8 and unsupported floats. Five
+upstream adapter tests also passed after its stricter integer validation landed.
+All six static tasks and architecture guards
+passed. After integration, test discovery found all 1004 files exactly once.
+The CI-planner selection passed all 85 tests, including the new EVM-verifier case.
+
+The parser regressions cover exact large integers and typed keys, malformed UTF-8,
+duplicate keys, excessive nesting, unsupported tags/floats, length bounds and
+historical omission. A real defect in the decoder's default text handling removed
+an initial U+FEFF. The owning tokenizer now validates and decodes the retained
+text bytes with BOM preservation; independent fixtures check both values and
+compiler-key identity.
+
+The owned fixture was compiled with caller-selected solc 0.8.30, using unoptimized,
+optimized via-IR, content-hash-free and CBOR-free settings. All four metadata
+checks passed again after integration. No contract was executed. Local first decode
+took approximately 0.81 ms or less. The originally reported approximately 2.92 ms
+for 100 warm iterations included equality assertions and is not a decode-only
+measurement. The verifier now measures parsing separately from those assertions;
+updated warm timings have not been collected. These historical figures describe
+these fixtures and this host, not a general workload.
+Peak process RSS
+included the compiler (299536 KiB); complete process-family CPU/memory and agent
+token costs were not measured.
+
+Compiled worker tests exercise unchanged EVMole WASM on this macOS host. MCP tests
+use the production server and contracts with an injected provider seam. The full
+Linux-only provider CLI/MCP, resource-limit and cleanup lane remains unverified
+here. Metadata JSON commitment binding remains deferred under issue #2388.
+
+The Linux CLI/MCP verifier asserts exact compiler bytes, declared version and
+candidate ranges for its existing owned raw/hex artifacts. Its CI lane additionally
+runs the new compiler-setting matrix. A regression using the actual CI planner
+checks that edits to the new verifier select the EVM lane.
+
+The later merge/review correction adds decoded-summary and omitted historical
+metadata coverage. Tests and provider reruns for that correction were skipped at
+the maintainer's request; the earlier runs above do not validate the changed head.
diff --git a/docs/testing.md b/docs/testing.md
index 7aa80d22a..213c48e44 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -646,6 +646,12 @@ records independent symbol/directory/SHA oracles; it never executes the target.
Linux also builds stripped, ordinary-native and small malformed/ambiguous/layout
negative inputs. The optional Windows fixture workflow builds a PE on a Windows
runner; analyzing that PE on Linux does not verify a Windows Ghidra host.
+On a local Windows host, the .NET 8 ILCompiler finds the linker only through
+Visual Studio 2019 or 2022 (`vswhere -version [16,18)`). With only Visual
+Studio Build Tools 2026 installed, open its x64 Developer Command Prompt.
+In that same `cmd.exe` session, run `set IlcUseEnvironmentalTools=true` and
+then `npm run build:fixtures:nativeaot`. Running `vcvars64.bat` as a child of
+PowerShell does not preserve its environment in the parent PowerShell session.
Build the clean pinned upstream adapter with `build:ghidra:nativeaot`, then set
`REA_GHIDRA_NATIVEAOT_JAR`. Run `verify:ghidra:nativeaot -- symbols`, `-- stripped`,
diff --git a/docs/user-visible-errors.csv b/docs/user-visible-errors.csv
index a9dd0d05d..02237dcdc 100644
--- a/docs/user-visible-errors.csv
+++ b/docs/user-visible-errors.csv
@@ -1,4 +1,7 @@
harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implementation_status,verification_result
+5,src/domain/native/bytePattern.ts,Unconstrained byte signature,Pattern must constrain at least one bit,Low risk; identifies input correction,Keep current copy,implemented,parser and MCP validation tests
+5,src/domain/native/bytePattern.ts,Unequal mask length,Mask length must equal pattern length,Low risk; identifies input correction,Keep current copy,implemented,parser and MCP validation tests
+5,bridge/ghidra/ReaGhidraBridge.java:searchMemoryBytes,Cursor reused with another query,Byte search cursor does not match the target and query,Low risk; identifies continuation mismatch,Keep current copy,implemented,real-provider verification required
1,src/domain/errors.ts:AnalysisCapabilityUnavailableError,Requested analysis unavailable,"Provider {providerId} cannot execute {operation}: {reason}",Exposes provider and internal operation/reason,"This analysis is unavailable for the current target. Choose another analysis or target.",implemented,focused assertions passed; full `npm run check` passed
2,src/domain/errors.ts:ProviderAdapterError,Provider adapter fails,"Provider {providerId} adapter failed during {operation}",Exposes provider and implementation boundary,"Analysis could not complete. Run `rea doctor` and review the reported details; repeating the unchanged request is not expected to succeed.",implemented,focused assertions passed; full `npm run check` passed
3,src/domain/errors.ts:HopperRemoteError,Remote analysis request fails,"Hopper request failed ({code}): {safeMessage}",Exposes provider name code and upstream text,"Analysis could not complete. Run `rea doctor` and review the reported details; repeating the unchanged request is not expected to succeed.",implemented,focused assertions passed; full `npm run check` passed
diff --git a/docs/wasm-artifacts.md b/docs/wasm-artifacts.md
index 6bc7c23ce..17b6c77f9 100644
--- a/docs/wasm-artifacts.md
+++ b/docs/wasm-artifacts.md
@@ -13,6 +13,11 @@ Check `binary_session` for configured capability availability. A recognized
header is not validation of the complete module; missing WABT is reported
separately as an unavailable capability, not as missing native engine support.
+The recognized WASM header is checked before automatic database routing from a
+.hop filename. Renaming a module to .hop therefore keeps the static-inspection
+diagnostic. An explicit database selection or raw/COM format selection retains
+the caller's requested interpretation; ordinary .hop database routing is unchanged.
+
Set `REA_WABT_BIN_DIRECTORY` to an absolute directory containing executable
`wasm-validate`, `wasm-objdump` and `wasm2wat` (with `.exe` on Windows). Missing
executables, unsupported banners and inaccessible tools produce actionable
@@ -79,3 +84,46 @@ Both CLI and MCP use the same typed port and application service. MCP publishes
SDK-compatible schemas and records Evidence for `get_evidence_bundle` readback.
See [WABT provenance](https://github.com/morluto/rea/blob/main/third_party/wabt/README.md) and
[testing](testing.md#offline-wasm-artifacts) for real-tool verification.
+
+## Compare core-module interfaces
+
+Inspection adds producer-owned typed `interface` facts while keeping exact WAT
+import/export forms. Function/tag signatures resolve type indices; table/memory
+limits, memory page size and global value type/mutability remain explicit.
+Unadmitted types (including GC reference signatures) retain null signatures and
+issues. Exact decoded names retain escapes, empty names, BOM and punctuation.
+This facet is absent in historical Evidence that did not examine typed interfaces.
+
+`compare_wasm_interfaces` accepts two complete `inspect_wasm_artifact` Evidence
+records, or exact retained references in the current MCP session, and required
+`scope: "imports" | "exports" | "both"`. It reads no files and needs no active
+WABT toolchain. CLI uses portable inline Evidence in one JSON document:
+
+```bash
+rea compare-wasm-interfaces comparison-input.json --json
+```
+
+```json
+{
+ "left": { "kind": "retained-evidence", "evidence_id": "ev_" },
+ "right": { "kind": "retained-evidence", "evidence_id": "ev_" },
+ "scope": "both"
+}
+```
+
+The references above are MCP-only; put complete Evidence objects in the CLI
+input. Resolve summary delivery to its retained complete parent before comparing.
+Results retain both input Evidence IDs, artifact paths/digests/lengths and every
+selected occurrence. Unique names are added, removed, changed or unchanged;
+duplicate names and unsupported signatures are unknown. Incomplete or historical
+inventories never establish absence. Imports group by module/name, exports by
+name, so a change of kind is visible. Output ordering is deterministic.
+
+Static type equality does not establish runtime compatibility, ABI compatibility
+or linking success. Comparison covers core modules; WebAssembly components and
+canonical ABI relationships belong to their separate inspection workflow.
+
+After building, run `REA_WABT_BIN_DIRECTORY=/absolute/wabt/bin node
+scripts/verify-wasm-interface-comparison.mjs` for original-module comparison
+through the compiled CLI and stdio MCP. It uses owned fixtures and WABT 1.0.42;
+no WASM module is instantiated or executed.
diff --git a/docs/windows-ghidra-p0.md b/docs/windows-ghidra-p0.md
index 5def0dae4..78e11ee79 100644
--- a/docs/windows-ghidra-p0.md
+++ b/docs/windows-ghidra-p0.md
@@ -29,8 +29,10 @@ admitted raw NMOS 6502 mapping on Windows. Windows P0 admission remains limited
to native PE and raw NMOS 6502 images. For x64 PE
NativeAOT RTR 9.1, the read-only snapshot parser can report metadata through
`inspect_native_load_image` and `inspect_native_data_type` without a JAR or
-database mutation. A real Windows-host NativeAOT Ghidra run is still required
-to establish the complete host workflow.
+database mutation: MethodTable relationships, virtual slots and frozen strings.
+`analyze_function` decompiles slot targets. Database reads of the rehydrated
+bytes (`read_bytes`, `search_strings`) return nothing on this host. See
+[NativeAOT](ghidra-nativeaot.md).
This boundary does not establish general Windows feature parity. Ghidra GUI
state, annotations, other target architectures and formats, and Hopper-only
diff --git a/licenses/npm-dependencies.txt b/licenses/npm-dependencies.txt
index ce45f7459..05fc2f173 100644
--- a/licenses/npm-dependencies.txt
+++ b/licenses/npm-dependencies.txt
@@ -2,7 +2,7 @@ REA npm dependency inventory
Prepared from the root package-lock.json; refresh this inventory when locked dependencies change.
License identifiers are upstream lockfile declarations, not a legal assessment.
Runtime includes transitive and optional platform packages; development includes build, test and website tooling.
-Locked package entries: 586; runtime: 174.
+Locked package entries: 587; runtime: 175.
Full runtime copyright/license/NOTICE texts: npm-runtime-notices.txt.
@algolia/abtesting@1.25.0 | development | MIT
@@ -1430,6 +1430,11 @@ canonicalize@3.0.0 | runtime | Apache-2.0
Package: https://www.npmjs.com/package/canonicalize/v/3.0.0
Archive: https://registry.npmjs.org/canonicalize/-/canonicalize-3.0.0.tgz
Integrity: sha512-yYLfHyDMIXRyRqsKBRLX023riFLpXY2YOfdtqKXZRZy9qsfOJ9U+4F9YZL7MEzL5+ziN2x2nlBvY/Voi3EBljA==
+cborg@6.1.3 | runtime | Apache-2.0
+ Lockfile path: node_modules/cborg
+ Package: https://www.npmjs.com/package/cborg/v/6.1.3
+ Archive: https://registry.npmjs.org/cborg/-/cborg-6.1.3.tgz
+ Integrity: sha512-Kb/wD/AFuVcqvVjR+HYNOjNiuhVJ2C+RWxQE2Yqq2WVlVkRjdvVLkkM6QsL3NMsBRh+rgNc+aXmcraA+X0HObQ==
ccount@2.0.1 | development | MIT
Lockfile path: node_modules/ccount
Package: https://www.npmjs.com/package/ccount/v/2.0.1
diff --git a/licenses/npm-runtime-notices.txt b/licenses/npm-runtime-notices.txt
index d981aa68a..2a0c45d24 100644
--- a/licenses/npm-runtime-notices.txt
+++ b/licenses/npm-runtime-notices.txt
@@ -1,7 +1,7 @@
REA third-party npm runtime copyright, license and NOTICE texts
Collected from integrity-verified locked npm archives. Release-specific supplements and upstream notice gaps are identified in their entries.
External engines are separately obtained; see THIRD_PARTY_NOTICES.md.
-Runtime dependency fingerprint (SHA-256): 5de9cf97e7c327da1892efeff30b40507e4c7f8e496b5d305b8016c162bc15e3
+Runtime dependency fingerprint (SHA-256): 63d5b55e027a0cfb3e77989ca337aca905f1652d10cfbfc4d4dd876d3ecd4f3a
===== @babel/helper-string-parser@8.0.0 =====
Declared license: MIT
@@ -2456,6 +2456,234 @@ Scope: npm runtime dependency, including optional platform packages.
limitations under the License.
+===== cborg@6.1.3 =====
+Declared license: Apache-2.0
+Archive: https://registry.npmjs.org/cborg/-/cborg-6.1.3.tgz
+Integrity: sha512-Kb/wD/AFuVcqvVjR+HYNOjNiuhVJ2C+RWxQE2Yqq2WVlVkRjdvVLkkM6QsL3NMsBRh+rgNc+aXmcraA+X0HObQ==
+Scope: npm runtime dependency, including optional platform packages.
+
+--- package/LICENSE ---
+
+Copyright 2020 Rod Vagg
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+--- Apache-2.0 full license (licenses/Apache-2.0.txt) ---
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
===== clean-git-ref@2.0.1 =====
Declared license: Apache-2.0
Archive: https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz
diff --git a/native/windows/README.md b/native/windows/README.md
index e2543260f..86103ce9a 100644
--- a/native/windows/README.md
+++ b/native/windows/README.md
@@ -90,8 +90,10 @@ without this lane reports Windows native controls as unavailable.
root and ancestor leases for retry; a partially removed runtime accepts only
cleanup, and its registry entry remains until removal succeeds. Normal close, cancellation,
startup failure, and timeout release provider resources. Abrupt owner death
- kills the owned job; it can leave private runtime files for later operator
- cleanup and does not claim transactional deletion after a crash.
+ kills the owned job. The Android provider additionally uses a detached helper
+ that owns its private runtime directory and cleans it when the owner pipe
+ closes. Other providers can leave private runtime files after owner death;
+ helper death and host power loss are not transactional deletion.
Windows failures retain their constraint, requested coordinate, Win32 code,
and system message. Unsupported filesystems and reparse paths are distinct
@@ -115,8 +117,10 @@ the supported device/UNC namespaces; existing admission checks still apply.
The bearer-token descriptor remains under its immutable private lease until
`runtime_close` on Windows; POSIX removes the descriptor after the bridge reads
-it. Job ownership guarantees process termination on owner death, not deletion
-of private files after a crash. SUBST drive aliases and changing DOS-device
+it. Job ownership guarantees process termination on owner death. Android's
+detached workspace helper provides a separate cleanup owner for that provider,
+but simultaneous helper death or power loss can still leave private files.
+SUBST drive aliases and changing DOS-device
namespaces are outside the verified P0 scope and are not detected as a separate
alias policy. Mounted-folder paths that report a reparse tag are rejected by
component admission; broader namespace variants remain unverified. Preserve
@@ -163,9 +167,14 @@ V8 heap flags cannot bound (limit notification, peak commit at the limit, all
members settled, with the notification present in the first native exit report
and the wrapper's exit event), and a control under the same limit that
completes normally.
-An optional independently built
-`tests/fixtures/windows/processBoundary.cc` executable adds breakaway and
-caller/child token observations and an in-place ancestor reparse attempt:
+A separately built `tests/fixtures/windows/processBoundary.cc` executable adds
+breakaway, caller/child token and environment semantics observations and an
+in-place ancestor reparse attempt. The lane skips all of them when the
+executable is absent, so both Windows workflows cross-build it in the same
+MinGW job that builds their artifact and pass it: the capability and package
+lane in `.github/workflows/ci.yml`, and the real Ghidra proof run in
+`.github/workflows/real-ghidra-windows.yml`. Build it locally with any Windows
+C++ compiler, or the same way they do, and pass its path:
```powershell
node scripts/verify-windows-native.mjs . C:\fixtures\process-boundary.exe
diff --git a/native/windows/src/filesystem.cc b/native/windows/src/filesystem.cc
index c8f66e5c1..71c8bbd44 100644
--- a/native/windows/src/filesystem.cc
+++ b/native/windows/src/filesystem.cc
@@ -1,6 +1,7 @@
#include "authority.hpp"
#include
#include
+#include
namespace rea {
@@ -165,7 +166,9 @@ static Handle createRelative(HANDLE parent, const std::wstring& name, DWORD acce
IO_STATUS_BLOCK observation{};
HANDLE raw = INVALID_HANDLE_VALUE;
const auto status = NtCreateFile(&raw, access | SYNCHRONIZE, &attributes, &observation, nullptr,
- FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_CREATE,
+ FILE_ATTRIBUTE_NORMAL,
+ directory ? FILE_SHARE_READ | FILE_SHARE_DELETE : FILE_SHARE_READ,
+ FILE_CREATE,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT |
(directory ? FILE_DIRECTORY_FILE : FILE_NON_DIRECTORY_FILE), nullptr, 0);
if (status < 0) throw Failure("Create private object relative to its admitted parent failed", path,
@@ -176,6 +179,30 @@ static Handle createRelative(HANDLE parent, const std::wstring& name, DWORD acce
return result;
}
+// Lookup relative to an already-owned directory; path is diagnostic only.
+static Handle openRelative(HANDLE parent, const std::wstring& name, DWORD access,
+ DWORD sharing, ULONG kind, const std::wstring& path) {
+ require(!name.empty() && name.find_first_of(L"\\/:\"<>|?*") == std::wstring::npos && name.size() < 32768,
+ "Private lookup requires one filename component", path, ERROR_INVALID_PARAMETER);
+ UNICODE_STRING coordinate{};
+ coordinate.Buffer = const_cast(name.data());
+ coordinate.Length = static_cast(name.size() * sizeof(wchar_t));
+ coordinate.MaximumLength = coordinate.Length;
+ OBJECT_ATTRIBUTES attributes;
+ InitializeObjectAttributes(&attributes, &coordinate, OBJ_CASE_INSENSITIVE, parent, nullptr);
+ IO_STATUS_BLOCK observation{};
+ HANDLE raw = INVALID_HANDLE_VALUE;
+ const auto status = NtCreateFile(&raw, access | SYNCHRONIZE, &attributes, &observation,
+ nullptr, FILE_ATTRIBUTE_NORMAL, sharing, FILE_OPEN,
+ FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT | kind, nullptr, 0);
+ if (status < 0)
+ throw Failure("Open private object relative to its directory handle failed", path,
+ RtlNtStatusToDosError(status));
+ Handle result(raw);
+ require(result.valid(), "Private lookup returned an invalid handle", path);
+ return result;
+}
+
static void verifyPrivate(HANDLE handle, const std::wstring& path, bool root) {
Security expected;
PSID owner = nullptr;
@@ -258,7 +285,16 @@ static std::unique_ptr runtimeDirectory(Runtime& root, const std::wstring&
size_t offset = root.path.size() + 1;
while (offset < path.size()) {
const auto end = path.find(L'\\', offset);
- result->handles.push_back(openComponent(path.substr(0, end), 0, true));
+ const auto name = path.substr(offset, end == std::wstring::npos ? end : end - offset);
+ auto child = openRelative(result->get(), name,
+ FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | READ_CONTROL,
+ FILE_SHARE_READ, FILE_DIRECTORY_FILE, path.substr(0, end));
+ FILE_ATTRIBUTE_TAG_INFO info{};
+ require(GetFileInformationByHandleEx(child.get(), FileAttributeTagInfo, &info, sizeof(info)),
+ "Read private directory tag failed", path);
+ require((info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0,
+ "Reparse private directory is unsupported", path, ERROR_NOT_SUPPORTED);
+ result->handles.push_back(std::move(child));
if (end == std::wstring::npos) break;
offset = end + 1;
}
@@ -320,7 +356,9 @@ static void disposeSnapshot(Runtime& root, Handle& file, const std::wstring& pat
require(GetFileInformationByHandleEx(file.get(), FileIdInfo, &expected, sizeof(expected)),
"Read incomplete snapshot identity failed", path);
file.reset();
- auto cleanup = openComponent(path, DELETE | GENERIC_READ | FILE_WRITE_ATTRIBUTES, false);
+ auto cleanup = openRelative(parent->get(), path.substr(path.find_last_of(L'\\') + 1),
+ DELETE | GENERIC_READ | FILE_WRITE_ATTRIBUTES,
+ FILE_SHARE_READ, FILE_NON_DIRECTORY_FILE, path);
FILE_ID_INFO observed{};
require(GetFileInformationByHandleEx(cleanup.get(), FileIdInfo, &observed, sizeof(observed)),
"Read snapshot cleanup identity failed", path);
@@ -339,21 +377,51 @@ void closeRuntime(Runtime& root) {
// cleanup. POSIX removes its consumed descriptor earlier. Abrupt owner death
// closes process jobs but cannot run this walk, so private files can remain.
root.immutableFiles.clear();
- // Each traversed directory stays locked against replacement. Reparse entries
- // are unlinked as objects; their targets are never enumerated or deleted.
+ // Enumerate and open children through the retained directory handles. Paths
+ // below are diagnostic labels only: renames and ancestor junction replacement
+ // cannot redirect cleanup into an unrelated tree.
struct Frame {
Handle directory;
- HANDLE search = INVALID_HANDLE_VALUE;
std::wstring path;
- WIN32_FIND_DATAW next{};
- bool pending = false;
- Frame(Handle handle, std::wstring path) : directory(std::move(handle)), path(std::move(path)) {
- search = FindFirstFileExW((L"\\\\?\\" + this->path + L"\\*").c_str(), FindExInfoBasic, &next,
- FindExSearchNameMatch, nullptr, 0);
- pending = search != INVALID_HANDLE_VALUE;
- require(pending || GetLastError() == ERROR_FILE_NOT_FOUND, "Enumerate runtime cleanup failed", this->path);
+ // One NTFS component is at most 255 UTF-16 units; 1 KiB fits a record
+ // without retaining a large batch at every directory depth.
+ alignas(FILE_ID_BOTH_DIR_INFO) std::array entries{};
+ size_t offset = 0;
+ bool buffered = false;
+ bool restart = true;
+ Frame(Handle handle, std::wstring path) : directory(std::move(handle)), path(std::move(path)) {}
+ bool next(std::wstring& name) {
+ for (;;) {
+ if (!buffered) {
+ const auto kind = restart ? FileIdBothDirectoryRestartInfo : FileIdBothDirectoryInfo;
+ restart = false;
+ if (!GetFileInformationByHandleEx(directory.get(), kind, entries.data(),
+ static_cast(entries.size()))) {
+ const auto code = GetLastError();
+ if (code == ERROR_NO_MORE_FILES) return false;
+ throw Failure("Enumerate runtime cleanup by handle failed", path, code);
+ }
+ offset = 0;
+ buffered = true;
+ }
+ require(offset <= entries.size() - offsetof(FILE_ID_BOTH_DIR_INFO, FileName),
+ "Cleanup directory record is truncated", path, ERROR_INVALID_DATA);
+ const auto* entry = reinterpret_cast(entries.data() + offset);
+ require(entry->FileNameLength % sizeof(wchar_t) == 0 &&
+ entry->FileNameLength <= entries.size() - offset - offsetof(FILE_ID_BOTH_DIR_INFO, FileName),
+ "Cleanup directory filename is truncated", path, ERROR_INVALID_DATA);
+ name.assign(entry->FileName, entry->FileNameLength / sizeof(wchar_t));
+ if (entry->NextEntryOffset == 0) buffered = false;
+ else {
+ require(entry->NextEntryOffset % alignof(FILE_ID_BOTH_DIR_INFO) == 0 &&
+ entry->NextEntryOffset >= offsetof(FILE_ID_BOTH_DIR_INFO, FileName) + entry->FileNameLength &&
+ entry->NextEntryOffset <= entries.size() - offset - offsetof(FILE_ID_BOTH_DIR_INFO, FileName),
+ "Cleanup directory record offset is invalid", path, ERROR_INVALID_DATA);
+ offset += entry->NextEntryOffset;
+ }
+ if (name != L"." && name != L"..") return true;
+ }
}
- ~Frame() { if (search != INVALID_HANDLE_VALUE) FindClose(search); }
};
std::vector> frames;
HANDLE duplicate;
@@ -362,21 +430,16 @@ void closeRuntime(Runtime& root) {
frames.push_back(std::make_unique (Handle(duplicate), root.path));
while (!frames.empty()) {
auto& frame = *frames.back();
- if (!frame.pending) {
- if (frame.search != INVALID_HANDLE_VALUE) { FindClose(frame.search); frame.search = INVALID_HANDLE_VALUE; }
+ std::wstring name;
+ if (!frame.next(name)) {
dispose(frame.directory.get(), frame.path);
frames.pop_back();
continue;
}
- const auto name = std::wstring(frame.next.cFileName);
- frame.pending = FindNextFileW(frame.search, &frame.next) != FALSE;
- require(frame.pending || GetLastError() == ERROR_NO_MORE_FILES, "Continue runtime cleanup enumeration failed", frame.path);
- if (name == L"." || name == L"..") continue;
const auto path = frame.path + L"\\" + name;
- Handle child(CreateFileW((L"\\\\?\\" + path).c_str(), DELETE | GENERIC_READ | FILE_WRITE_ATTRIBUTES,
- FILE_SHARE_READ, nullptr, OPEN_EXISTING,
- FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr));
- require(child.valid(), "Open owned cleanup object failed", path);
+ auto child = openRelative(frame.directory.get(), name,
+ DELETE | FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES,
+ FILE_SHARE_READ | FILE_SHARE_DELETE, 0, path);
FILE_ATTRIBUTE_TAG_INFO info{};
require(GetFileInformationByHandleEx(child.get(), FileAttributeTagInfo, &info, sizeof(info)), "Read cleanup object tag failed", path);
if ((info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 && (info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0)
@@ -551,9 +614,16 @@ napi_value filesystemCall(napi_env env, const std::wstring& operation, const std
// must share both to pass Windows' symmetric share check. Those retained
// handles still omit FILE_SHARE_WRITE and FILE_SHARE_DELETE, blocking
// competing writes and delete opens.
- file->handles.push_back(openComponent(path, GENERIC_READ, false,
- FILE_SHARE_READ | FILE_SHARE_WRITE |
- FILE_SHARE_DELETE));
+ auto child = openRelative(file->get(), path.substr(path.find_last_of(L'\\') + 1),
+ GENERIC_READ | FILE_READ_ATTRIBUTES | READ_CONTROL,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ FILE_NON_DIRECTORY_FILE, path);
+ FILE_ATTRIBUTE_TAG_INFO info{};
+ require(GetFileInformationByHandleEx(child.get(), FileAttributeTagInfo, &info, sizeof(info)),
+ "Read private file tag failed", path);
+ require((info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0,
+ "Reparse private file is unsupported", path, ERROR_NOT_SUPPORTED);
+ file->handles.push_back(std::move(child));
verifyPrivate(file->get(), path, false);
auto result = identity(env, file->get(), path);
set(env, result, "handle", wrap(env, std::move(file)));
diff --git a/package-lock.json b/package-lock.json
index 5af71e98e..7eae3799e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,6 +24,7 @@
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"canonicalize": "3.0.0",
+ "cborg": "6.1.3",
"cross-spawn": "7.0.6",
"diff": "9.0.0",
"evmole": "0.9.3",
@@ -4181,6 +4182,15 @@
"node": ">=18"
}
},
+ "node_modules/cborg": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/cborg/-/cborg-6.1.3.tgz",
+ "integrity": "sha512-Kb/wD/AFuVcqvVjR+HYNOjNiuhVJ2C+RWxQE2Yqq2WVlVkRjdvVLkkM6QsL3NMsBRh+rgNc+aXmcraA+X0HObQ==",
+ "license": "Apache-2.0",
+ "bin": {
+ "cborg": "lib/bin.js"
+ }
+ },
"node_modules/ccount": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
diff --git a/package.json b/package.json
index 7ee2b42ab..19e978205 100644
--- a/package.json
+++ b/package.json
@@ -62,6 +62,7 @@
"bridge/android/ReaJadxBridge.java",
"bridge/mitmproxy/capture.py",
"scripts/rea.mjs",
+ "scripts/rea-android-workspace-guardian.mjs",
"scripts/electron-active-hook.cjs",
"scripts/electron-active-hook-boundaries.cjs",
"scripts/hopper-demo-x11.py",
@@ -265,11 +266,14 @@
"verify:browser:source-maps": "npm run build:cached && node scripts/verify-browser-source-maps.mjs",
"verify:browser:runtime": "npm run build:cached && node scripts/verify-browser-runtime.mjs",
"verify:web:network-captures": "npm run build:cached && node scripts/verify-web-network-captures.mjs",
+ "verify:web:http3": "npm run build:cached && node scripts/verify-http3-network-captures.mjs",
"verify:binary:layout": "npm run build:cached && node scripts/verify-binary-layout.mjs",
"verify:go:binary": "npm run build:cached && node scripts/verify-go-binary.mjs",
"verify:wasm:package": "npm run build:cached && node scripts/verify-packaged-wasm-artifact.mjs",
+ "verify:wasm-interface-comparison": "npm run build:cached && node scripts/verify-wasm-interface-comparison.mjs",
"verify:wasm:artifact": "npm run build:cached && node scripts/verify-wasm-artifact.mjs",
"verify:evm:interface": "npm run build:cached && node scripts/verify-evm-interface.mjs",
+ "verify:solidity:metadata": "npm run build:cached && node scripts/verify-solidity-metadata.mjs",
"verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs",
"mcp-catalog:check:built": "node scripts/generate-mcp-tool-catalog.mjs --check",
"compile": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json && node scripts/bundle-runtime.mjs",
@@ -309,6 +313,7 @@
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"canonicalize": "3.0.0",
+ "cborg": "6.1.3",
"cross-spawn": "7.0.6",
"diff": "9.0.0",
"evmole": "0.9.3",
diff --git a/scripts/ci/scopes.mjs b/scripts/ci/scopes.mjs
index ffe15cb3d..a4f0f0fac 100644
--- a/scripts/ci/scopes.mjs
+++ b/scripts/ci/scopes.mjs
@@ -55,6 +55,9 @@ export const SCOPES = {
"tests/boundary/providers/native/artifactProvider.extractionCompleteness.test.ts",
"tests/boundary/filesystem/setupUnavailableProvider.test.ts",
"tests/boundary/filesystem/windowsNpxRegistration.test.ts",
+ "tests/boundary/android/workspaceGuardianRetry.test.ts",
+ "src/android/JadxWorkspaceRoot.ts",
+ "scripts/rea-android-workspace-guardian.mjs",
"tests/acceptance/setup/homeDirectoryFallback.test.ts",
"tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts",
"src/domain/javascript/javascriptRuntimeStaticCandidates.ts",
@@ -218,6 +221,7 @@ export const SCOPES = {
"src/artifacts/readStableArtifact*.ts",
"src/artifacts/ArtifactReader.ts",
"scripts/verify-evm-interface.mjs",
+ "scripts/verify-solidity-metadata.mjs",
"scripts/fixtures/evm-interface.sol",
"third_party/evmole/**",
],
@@ -359,6 +363,7 @@ export const SCOPES = {
"website/**",
"scripts/*website*.py",
"README*.md",
+ "docs/readme/README*.md",
".github/workflows/website-check.yml",
".github/workflows/website-pages.yml",
],
diff --git a/scripts/fixtures/http3-lab-requirements.txt b/scripts/fixtures/http3-lab-requirements.txt
new file mode 100644
index 000000000..57e2af74a
--- /dev/null
+++ b/scripts/fixtures/http3-lab-requirements.txt
@@ -0,0 +1,10 @@
+# Exercised runtime graph: CPython 3.14.7, Linux x86_64 guest.
+aioquic==1.2.0
+attrs==26.1.0
+certifi==2026.7.22
+cffi==2.1.1
+cryptography==50.0.2
+pycparser==3.11
+pylsqpack==0.3.24
+pyOpenSSL==26.4.0
+service-identity==26.1.0
diff --git a/scripts/fixtures/http3-lab.py b/scripts/fixtures/http3-lab.py
new file mode 100644
index 000000000..0f1ed7480
--- /dev/null
+++ b/scripts/fixtures/http3-lab.py
@@ -0,0 +1,416 @@
+"""Real loopback HTTP/3 laboratory using aioquic and caller-supplied mitmdump.
+
+Run inside a disposable Linux guest. All native records are written by
+mitmproxy's real save addon, not constructed fixtures. Endpoint stream IDs are
+observations scoped to that endpoint; no MITM stream mapping is claimed.
+"""
+
+import argparse
+import asyncio
+import base64
+import hashlib
+import json
+import os
+import signal
+import ssl
+import subprocess
+import time
+from pathlib import Path
+
+import aioquic
+from aioquic.asyncio import QuicConnectionProtocol, connect, serve
+from aioquic.h3.connection import H3Connection
+from aioquic.h3.events import DataReceived, HeadersReceived
+from aioquic.quic.configuration import QuicConfiguration
+from aioquic.quic.events import ConnectionTerminated, HandshakeCompleted, StreamReset
+from aioquic.quic.packet import QuicProtocolVersion
+
+
+def binary(value):
+ return {"base64": base64.b64encode(value).decode("ascii"), "bytes": len(value),
+ "sha256": hashlib.sha256(value).hexdigest()}
+
+
+def pairs(headers):
+ return [[binary(name), binary(value)] for name, value in headers]
+
+
+class Journal:
+ def __init__(self, root):
+ self.root = root
+ self.events = []
+ self.requests = {}
+ self.responses = {}
+ self.tasks = set()
+ self.children = []
+ self.writers = []
+
+ def emit(self, event, **data):
+ value = {"event": event, "monotonic": time.monotonic(), **data}
+ self.events.append(value)
+ with (self.root / "endpoints.jsonl").open("a") as handle:
+ handle.write(json.dumps(value) + "\n")
+
+ def task(self, coroutine):
+ task = asyncio.create_task(coroutine)
+ self.tasks.add(task)
+ return task
+
+
+
+class Endpoint(QuicConnectionProtocol):
+ def __init__(self, *args, journal, endpoint, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.journal, self.endpoint = journal, endpoint
+ self.http = H3Connection(self._quic)
+ self.messages = {}
+ self.waiters = {}
+ self.concurrent = []
+ self.identity = f"{endpoint}-{len(journal.events)}"
+
+ def quic_event_received(self, event):
+ if isinstance(event, HandshakeCompleted):
+ # This is the negotiated runtime value in pinned aioquic 1.2.0,
+ # not configuration.supported_versions or mitmproxy's QUICv1 label.
+ self.journal.emit("handshake", endpoint=self.endpoint, connection=self.identity,
+ alpn=event.alpn_protocol, quic_version=int(self._quic._version))
+ if isinstance(event, (StreamReset, ConnectionTerminated)):
+ self.journal.emit(type(event).__name__, endpoint=self.endpoint,
+ connection=self.identity, error_code=event.error_code,
+ stream_id=getattr(event, "stream_id", None),
+ reason=getattr(event, "reason_phrase", None))
+ targets = self.waiters.values() if isinstance(event, ConnectionTerminated) else [self.waiters.get(event.stream_id)]
+ for waiter in targets:
+ if waiter is not None and not waiter.done():
+ waiter.set_exception(ConnectionError(type(event).__name__))
+ for item in self.http.handle_event(event):
+ if isinstance(item, (HeadersReceived, DataReceived)):
+ message = self.messages.setdefault(item.stream_id, {"headers": [], "body": bytearray()})
+ if isinstance(item, HeadersReceived):
+ message["headers"].extend(item.headers)
+ else:
+ message["body"].extend(item.data)
+ if item.stream_ended:
+ self.completed(item.stream_id, message)
+
+ def completed(self, stream_id, message):
+ body = bytes(message["body"])
+ if self.endpoint == "client":
+ self.journal.emit("response-received", endpoint="client", connection=self.identity,
+ stream_id=stream_id, headers=pairs(message["headers"]), body=binary(body))
+ self.waiters[stream_id].set_result(message)
+ return
+ headers = dict(message["headers"])
+ case = headers.get(b"x-lab-case", b"curl").decode("ascii")
+ self.journal.requests[case] = {"headers": pairs(message["headers"]), "body": binary(body)}
+ self.journal.emit("request-received", endpoint="origin", connection=self.identity,
+ stream_id=stream_id, case=case, **self.journal.requests[case])
+ if case.startswith("parallel-"):
+ self.concurrent.append((case, stream_id, body))
+ if len(self.concurrent) == 3:
+ self.journal.task(self.answer_parallel())
+ else:
+ self.answer(case, stream_id, b"\x00\xfeanswer:" + case.encode() + b":" + body[::-1])
+
+ async def answer_parallel(self):
+ for case, stream_id, body in reversed(self.concurrent):
+ self.answer(case, stream_id, b"\x00\xfeanswer:" + case.encode() + b":" + body[::-1])
+ await asyncio.sleep(0.15)
+
+ def answer(self, case, stream_id, body):
+ headers = [(b":status", b"200"), (b"content-type", b"application/octet-stream"),
+ (b"content-length", str(len(body)).encode()),
+ (b"x-lab-case", case.encode()), (b"x-duplicate", b"one"), (b"x-duplicate", b"two")]
+ self.journal.responses[case] = {"headers": pairs(headers), "body": binary(body), "finished": True}
+ self.journal.emit("response-sent", endpoint="origin", connection=self.identity,
+ stream_id=stream_id, case=case, **self.journal.responses[case])
+ self.http.send_headers(stream_id, headers)
+ self.http.send_data(stream_id, body, end_stream=True)
+ self.transmit()
+
+ def request(self, case, body):
+ stream_id = self._quic.get_next_available_stream_id()
+ headers = [(b":method", b"POST"), (b":scheme", b"https"),
+ (b":authority", b"lab.test:18443"), (b":path", b"/same?token=ordinary&x=%2F"),
+ (b"x-lab-case", case.encode()), (b"content-type", b"application/octet-stream"),
+ (b"content-length", str(len(body)).encode()),
+ (b"x-duplicate", b"first"), (b"x-duplicate", b"second")]
+ self.waiters[stream_id] = asyncio.get_running_loop().create_future()
+ self.journal.emit("request-sent", endpoint="client", connection=self.identity,
+ stream_id=stream_id, case=case, headers=pairs(headers), body=binary(body), finished=True)
+ self.http.send_headers(stream_id, headers)
+ self.http.send_data(stream_id, body, end_stream=True)
+ self.transmit()
+ return stream_id, self.waiters[stream_id]
+
+
+
+class Proxy:
+ def __init__(self, args, root, name, http3=True):
+ self.args, self.root, self.name, self.http3 = args, root, name, http3
+ self.process = None
+ self.log = None
+
+ async def start(self):
+ self.log = (self.root / f"{self.name}.proxy.log").open("wb")
+ command = [self.args.mitmdump, "--mode", "reverse:https://lab.test:19443@127.0.0.1:18443",
+ "--set", f"confdir={self.root / 'proxy-config'}", "--set", "connection_strategy=lazy",
+ "--set", "keep_host_header=true", "--set", "store_streamed_bodies=false",
+ "--set", f"rea_lab_log={self.root / (self.name + '.producer.jsonl')}",
+ "-w", str(self.root / (self.name + '.mitm')),
+ "-s", str(Path(__file__).with_name("observe-mitmproxy-http3.py"))]
+ command.extend(["--set", f"ssl_verify_upstream_trusted_ca={self.root / 'origin-ca.pem'}"])
+ if not self.http3:
+ command.extend(["--set", "http3=false"])
+ self.process = subprocess.Popen(command, stdout=self.log, stderr=subprocess.STDOUT,
+ start_new_session=True)
+ lease = {"runId": os.environ["REA_PROCESS_RUN_ID"], "leaderPid": self.process.pid,
+ "processGroupId": self.process.pid}
+ with (self.root / "process-groups.jsonl").open("a") as handle:
+ handle.write(json.dumps(lease) + "\n")
+ deadline = time.monotonic() + 15
+ journal = self.root / f"{self.name}.producer.jsonl"
+ while time.monotonic() < deadline:
+ if self.process.poll() is not None:
+ raise RuntimeError(f"Proxy exited: {self.log.name}")
+ if journal.exists() and '"event": "ready"' in journal.read_text():
+ return self
+ await asyncio.sleep(0.05)
+ raise TimeoutError(f"Proxy readiness: {self.log.name}")
+
+ async def stop(self):
+ if self.process is not None and self.process.poll() is None:
+ # The standalone mitmdump distribution has a bootloader parent.
+ # Signal its owned session together, including the interpreter child.
+ os.killpg(self.process.pid, signal.SIGINT)
+ try:
+ await asyncio.wait_for(asyncio.to_thread(self.process.wait), 10)
+ except TimeoutError:
+ os.killpg(self.process.pid, signal.SIGKILL)
+ await asyncio.to_thread(self.process.wait)
+ raise
+ if self.process is not None:
+ for _ in range(40):
+ if group_is_empty(self.process.pid):
+ return
+ await asyncio.sleep(0.05)
+ raise RuntimeError(f"Owned proxy group still live: {self.process.pid}")
+
+ async def close_log(self):
+ if self.log is not None:
+ self.log.close()
+
+
+def certificates(root):
+ for name in ["origin"]:
+ subprocess.run(["openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes",
+ "-keyout", str(root / f"{name}-key.pem"), "-out", str(root / f"{name}-ca.pem"),
+ "-days", "2", "-subj", "/CN=lab.test", "-addext", "subjectAltName=DNS:lab.test",
+ "-addext", "basicConstraints=critical,CA:TRUE"], check=True,
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+
+
+def client_config(root):
+ config = QuicConfiguration(is_client=True, alpn_protocols=["h3"], supported_versions=[QuicProtocolVersion.VERSION_1],
+ server_name="lab.test", idle_timeout=3)
+ config.load_verify_locations(cafile=str(root / "proxy-config/mitmproxy-ca-cert.pem"))
+ return config
+
+
+def connection(root, journal):
+ return connect("127.0.0.1", 18443, configuration=client_config(root),
+ create_protocol=lambda *a, **kw: Endpoint(*a, journal=journal, endpoint="client", **kw))
+
+
+async def curl_request(root, journal, name, version, path="/curl", maximum=8):
+ child = await asyncio.create_subprocess_exec("curl", version, "--silent", "--show-error",
+ "--max-time", str(maximum), "--cacert", str(root / "proxy-config/mitmproxy-ca-cert.pem"),
+ "--resolve", "lab.test:18443:127.0.0.1", "--noproxy", "*", "-H", f"x-lab-case: {name}",
+ "--output", str(root / (name + ".body")), "--dump-header", str(root / (name + ".headers")),
+ "--write-out", "%{json}", "https://lab.test:18443" + path,
+ stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
+ journal.children.append(child)
+ out, err = await child.communicate()
+ (root / (name + ".json")).write_bytes(out)
+ (root / (name + ".stderr")).write_bytes(err)
+ return child.returncode, json.loads(out)
+
+
+async def tcp_origin(reader, writer, journal):
+ journal.writers.append(writer)
+ try:
+ head = await asyncio.wait_for(reader.readuntil(b"\r\n\r\n"), 5)
+ case = "tcp"
+ for line in head.split(b"\r\n")[1:]:
+ if line.lower().startswith(b"x-lab-case:"):
+ case = line.split(b":", 1)[1].strip().decode("ascii")
+ body = b"\x00\xfftcp-origin"
+ journal.emit("tcp-request", case=case, alpn=writer.get_extra_info("ssl_object").selected_alpn_protocol(),
+ headers=binary(head), body=binary(body))
+ writer.write(b"HTTP/1.1 200 OK\r\ncontent-type: application/octet-stream\r\ncontent-length: " +
+ str(len(body)).encode() + b"\r\nconnection: close\r\n\r\n" + body)
+ await writer.drain()
+ finally:
+ writer.close()
+ await writer.wait_closed()
+
+
+def group_is_empty(group):
+ for entry in Path("/proc").iterdir():
+ if not entry.name.isdigit():
+ continue
+ try:
+ fields = (entry / "stat").read_text().rsplit(")", 1)[1].split()
+ except FileNotFoundError:
+ continue
+ if fields[0] != "Z" and int(fields[2]) == group:
+ return False
+ return True
+
+
+async def attempt_all(actions):
+ failures = []
+ for name, action in actions:
+ try:
+ await asyncio.wait_for(action(), 12)
+ except BaseException as error:
+ failures.append((name, error))
+ return failures
+
+
+async def close_resource(resource):
+ resource.close()
+
+
+async def finish_child(child):
+ if child.returncode is None:
+ child.terminate()
+ try:
+ await asyncio.wait_for(child.wait(), 3)
+ except TimeoutError:
+ child.kill()
+ await child.wait()
+
+
+async def finish_tasks(journal):
+ for task in journal.tasks:
+ if not task.done():
+ task.cancel()
+ results = await asyncio.gather(*journal.tasks, return_exceptions=True)
+ failures = [error for error in results if isinstance(error, BaseException)
+ and not isinstance(error, asyncio.CancelledError)]
+ if failures:
+ raise BaseExceptionGroup("Endpoint tasks failed", failures)
+
+
+async def run(args):
+ if aioquic.__version__ != "1.2.0":
+ raise RuntimeError("This laboratory requires aioquic 1.2.0")
+ root = Path(args.output).resolve()
+ root.mkdir(mode=0o700)
+ os.umask(0o077)
+ journal = Journal(root)
+ loop = asyncio.get_running_loop()
+ asynchronous_errors = []
+ def unexpected_exception(active_loop, context):
+ asynchronous_errors.append(context.get("message", "Unexpected asynchronous error"))
+ active_loop.default_exception_handler(context)
+ loop.set_exception_handler(unexpected_exception)
+ certificates(root)
+ server = tcp_server = None
+ proxies = []
+ scenarios = []
+ primary_error = None
+ cleanup_errors = []
+ try:
+ config = QuicConfiguration(is_client=False, alpn_protocols=["h3"],
+ supported_versions=[QuicProtocolVersion.VERSION_1], idle_timeout=3)
+ config.load_cert_chain(str(root / "origin-ca.pem"), str(root / "origin-key.pem"))
+ server = await serve("127.0.0.1", 19443, configuration=config,
+ create_protocol=lambda *a, **kw: Endpoint(*a, journal=journal, endpoint="origin", **kw))
+ tls_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+ tls_context.load_cert_chain(str(root / "origin-ca.pem"), str(root / "origin-key.pem"))
+ tls_context.set_alpn_protocols(["http/1.1"])
+ tcp_server = await asyncio.start_server(lambda r, w: journal.task(tcp_origin(r, w, journal)),
+ "127.0.0.1", 19443, ssl=tls_context)
+ proxy = Proxy(args, root, "normal")
+ proxies.append(proxy)
+ await proxy.start()
+ async with connection(root, journal) as client:
+ requests = [client.request(f"parallel-{i}", bytes([0, 255, i]) + bytes(range(256))) for i in range(3)]
+ await asyncio.wait_for(asyncio.gather(*(future for _, future in requests)), 10)
+ # cURL supplies an independently implemented HTTP/3 smoke case.
+ code, result = await curl_request(root, journal, "curl", "--http3-only")
+ if code != 0:
+ raise RuntimeError("HTTP/3 cURL failed; inspect curl.stderr")
+ if result["http_version"] != "3":
+ raise AssertionError("cURL silently downgraded")
+ await proxy.stop()
+ scenarios.append({"name": "normal", "capture": "normal.mitm"})
+
+ proxy = Proxy(args, root, "fallback", http3=False)
+ proxies.append(proxy)
+ await proxy.start()
+ code, result = await curl_request(root, journal, "fallback", "--http3", path="/fallback")
+ if code != 0 or result["http_version"] != "2":
+ raise AssertionError(f"Fallback control: {result}")
+ await proxy.stop()
+ scenarios.append({"name": "fallback", "capture": "fallback.mitm"})
+
+ except BaseException as error:
+ primary_error = error
+ finally:
+ actions = []
+ for proxy in proxies:
+ actions.extend([(f"proxy-{proxy.name}", proxy.stop),
+ (f"proxy-log-{proxy.name}", proxy.close_log)])
+ if server is not None:
+ actions.append(("quic-server", lambda: close_resource(server)))
+ if tcp_server is not None:
+ actions.extend([("tcp-listener-close", lambda: close_resource(tcp_server)),
+ ("tcp-listener-wait", tcp_server.wait_closed)])
+ for index, writer in enumerate(journal.writers):
+ actions.extend([(f"tcp-writer-close-{index}", lambda w=writer: close_resource(w)),
+ (f"tcp-writer-wait-{index}", writer.wait_closed)])
+ actions.append(("endpoint-tasks", lambda: finish_tasks(journal)))
+ for index, child in enumerate(journal.children):
+ actions.append((f"curl-child-{index}", lambda c=child: finish_child(c)))
+ cleanup_errors = await attempt_all(actions)
+ # This runs after every close attempt, including failed setup.
+ groups = []
+ async def audit_groups():
+ for proxy in proxies:
+ if proxy.process is not None:
+ groups.append({"group": proxy.process.pid, "empty": group_is_empty(proxy.process.pid)})
+ if any(not item["empty"] for item in groups):
+ raise RuntimeError("Proxy groups remain live")
+ cleanup_errors.extend(await attempt_all([("process-groups", audit_groups)]))
+ try:
+ (root / "producer-cleanup.json").write_text(json.dumps({
+ "primary_error": None if primary_error is None else str(primary_error),
+ "cleanup_errors": [{"action": name, "type": type(error).__name__, "message": str(error)}
+ for name, error in cleanup_errors], "groups": groups,
+ "asynchronous_errors": asynchronous_errors}, indent=2))
+ except BaseException as error:
+ cleanup_errors.append(("cleanup-report", error))
+ if primary_error is not None:
+ for name, error in cleanup_errors:
+ primary_error.add_note(f"Cleanup {name}: {error}")
+ raise primary_error
+ if cleanup_errors:
+ raise BaseExceptionGroup("HTTP/3 producer cleanup failed", [error for _, error in cleanup_errors])
+ if asynchronous_errors:
+ raise RuntimeError(f"Unexpected asynchronous errors: {asynchronous_errors}")
+ (root / "manifest.json").write_text(json.dumps({"profile": "http3-minimal-v1", "aioquic": aioquic.__version__,
+ "scenarios": scenarios, "requests": journal.requests, "responses": journal.responses,
+ "events": journal.events, "limitations": [
+ "Endpoint stream IDs are scoped observations, not mitmproxy serialized IDs or mappings.",
+ "The fixture reads negotiated _version from pinned aioquic 1.2.0.",
+ "Only QUIC v1, Linux reverse proxy and the selected endpoints are exercised."]}, indent=2))
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--mitmdump", required=True)
+ parser.add_argument("--output", required=True)
+ asyncio.run(run(parser.parse_args()))
diff --git a/scripts/fixtures/http3-lab.test.py b/scripts/fixtures/http3-lab.test.py
new file mode 100644
index 000000000..3bc8c1b7e
--- /dev/null
+++ b/scripts/fixtures/http3-lab.test.py
@@ -0,0 +1,56 @@
+"""Lifecycle regressions; these tests do not establish HTTP/3 provider support."""
+import importlib.util
+import json
+import tempfile
+import unittest
+from pathlib import Path
+from types import SimpleNamespace
+from unittest.mock import patch
+
+spec = importlib.util.spec_from_file_location("http3_lab", Path(__file__).with_name("http3-lab.py"))
+lab = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(lab)
+
+
+class CleanupTests(unittest.IsolatedAsyncioTestCase):
+ async def test_all_actions_run_after_multiple_close_failures(self):
+ attempted = []
+ async def fail():
+ attempted.append("failed-close")
+ raise RuntimeError("close failed")
+ async def audit():
+ attempted.append("audit")
+ failures = await lab.attempt_all([("one", fail), ("two", fail), ("audit", audit)])
+ self.assertEqual(attempted, ["failed-close", "failed-close", "audit"])
+ self.assertEqual([name for name, _ in failures], ["one", "two"])
+
+ async def test_partial_setup_keeps_primary_error_and_audits_after_close_failure(self):
+ original_serve = lab.serve
+ closed = []
+ class ClosingServer:
+ def __init__(self, server):
+ self.server = server
+ def close(self):
+ self.server.close()
+ closed.append(True)
+ raise RuntimeError("injected close failure")
+ async def serve(*args, **kwargs):
+ return ClosingServer(await original_serve(*args, **kwargs))
+ async def fail_tcp(*args, **kwargs):
+ raise RuntimeError("injected primary setup failure")
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory) / "capture"
+ with patch.object(lab, "serve", serve), patch.object(lab.asyncio, "start_server", fail_tcp):
+ with self.assertRaisesRegex(RuntimeError, "injected primary setup failure") as caught:
+ await lab.run(SimpleNamespace(output=str(root), mitmdump="unused"))
+ self.assertEqual(closed, [True])
+ self.assertIn("injected close failure", " ".join(caught.exception.__notes__))
+ report = json.loads((root / "producer-cleanup.json").read_text())
+ self.assertEqual(report["primary_error"], "injected primary setup failure")
+ self.assertEqual(report["cleanup_errors"][0]["action"], "quic-server")
+ self.assertEqual(report["groups"], [])
+ self.assertFalse((root / "manifest.json").exists())
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/scripts/fixtures/observe-mitmproxy-http3.py b/scripts/fixtures/observe-mitmproxy-http3.py
new file mode 100644
index 000000000..003025a75
--- /dev/null
+++ b/scripts/fixtures/observe-mitmproxy-http3.py
@@ -0,0 +1,64 @@
+"""Test-only observation of real flows; never synthesizes flow or stream IDs."""
+
+import base64
+import hashlib
+import json
+from pathlib import Path
+
+from mitmproxy import ctx
+
+
+def bytes_view(value):
+ if value is None:
+ return None
+ return {"base64": base64.b64encode(value).decode("ascii"),
+ "bytes": len(value), "sha256": hashlib.sha256(value).hexdigest()}
+
+
+class Observer:
+ def load(self, loader):
+ loader.add_option("rea_lab_log", str, "", "HTTP/3 verifier observations")
+
+ def emit(self, event, **data):
+ with Path(ctx.options.rea_lab_log).open("a", encoding="utf-8") as handle:
+ handle.write(json.dumps({"event": event, **data}) + "\n")
+
+ def running(self):
+ self.emit("ready")
+
+ def connection(self, conn):
+ return {"id": conn.id, "transport_protocol": conn.transport_protocol,
+ "alpn": bytes_view(conn.alpn), "tls_version": conn.tls_version,
+ "timestamp_tls_setup": conn.timestamp_tls_setup,
+ "error": conn.error}
+
+ def tls_established_client(self, data):
+ self.emit("tls-established-client", connection=self.connection(data.conn))
+
+ def tls_established_server(self, data):
+ self.emit("tls-established-server", connection=self.connection(data.conn))
+
+ def flow(self, event, flow):
+ def message(message):
+ if message is None:
+ return None
+ return {"http_version": message.http_version,
+ "content": bytes_view(message.raw_content),
+ "timestamp_end": message.timestamp_end}
+ self.emit(event, id=flow.id, case=flow.request.headers.get("x-lab-case", "curl"),
+ request=message(flow.request), response=message(flow.response),
+ error=None if flow.error is None else flow.error.msg,
+ client_conn=self.connection(flow.client_conn),
+ server_conn=self.connection(flow.server_conn))
+
+ def request(self, flow):
+ self.flow("request", flow)
+
+ def response(self, flow):
+ self.flow("response", flow)
+
+ def error(self, flow):
+ self.flow("error", flow)
+
+
+addons = [Observer()]
diff --git a/scripts/lib/browser-runtime-fixture-lifecycle.mjs b/scripts/lib/browser-runtime-fixture-lifecycle.mjs
index d39617f58..88a76e573 100644
--- a/scripts/lib/browser-runtime-fixture-lifecycle.mjs
+++ b/scripts/lib/browser-runtime-fixture-lifecycle.mjs
@@ -8,7 +8,10 @@ import { spawnOwnedProviderProcess } from "../../dist/process/ProviderProcess.js
import { createSystemProcessOwnershipHost } from "../../dist/process/ProcessOwnershipObservation.js";
import { liveProcesses } from "../../dist/process/ProcessOwnershipProcessTree.js";
import { CdpConnection } from "../../dist/browser/CdpConnection.js";
-import { waitForBrowserDevtoolsPort } from "../../dist/browser/BrowserProcessStartup.js";
+import {
+ BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS,
+ waitForBrowserDevtoolsPort,
+} from "../../dist/browser/BrowserProcessStartup.js";
/** Launch the synthetic fixture browser and await CDP in its own verified process group. */
export async function startRuntimeFixtureBrowser(executable, origin) {
@@ -50,7 +53,7 @@ export async function startRuntimeFixtureBrowser(executable, origin) {
stderr: () => stderr,
// Match the broader real-browser verifier's allowance for cold Chrome
// startup on shared runners; early process failures still reject at once.
- timeoutMs: 60_000,
+ timeoutMs: BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS,
});
return {
port,
diff --git a/scripts/lib/browser-socket-e2e.mjs b/scripts/lib/browser-socket-e2e.mjs
new file mode 100644
index 000000000..6e54e3d18
--- /dev/null
+++ b/scripts/lib/browser-socket-e2e.mjs
@@ -0,0 +1,100 @@
+import assert from "node:assert/strict";
+import { readFile } from "node:fs/promises";
+import { join } from "node:path";
+
+import {
+ inspectWebPageInputSchema,
+ listBrowserTargetsInputSchema,
+} from "../../dist/domain/browserObservation.js";
+import { compareWebCapturesInputSchema } from "../../dist/domain/webCaptureDiffSchemas.js";
+import { artifactCliEvidence } from "./artifact-e2e.mjs";
+
+/** Verify browser-socket discovery and inspection against HTTP discovery. */
+export async function verifyBrowserSocketTransport({
+ provider,
+ profile,
+ endpoint,
+ target,
+ origin,
+}) {
+ const [port, path] = (
+ await readFile(join(profile, "DevToolsActivePort"), "utf8")
+ ).split("\n");
+ const socket = `ws://127.0.0.1:${port}${path}`;
+ const version = await (await fetch(`${endpoint}/json/version`)).json();
+ assert.equal(
+ new URL(version.webSocketDebuggerUrl).pathname,
+ path,
+ "DevToolsActivePort and /json/version named different browser sockets",
+ );
+ const list = async (cdpEndpoint) => {
+ const listed = await provider.listTargets(
+ listBrowserTargetsInputSchema.parse({
+ cdp_endpoint: cdpEndpoint,
+ allowed_origins: [origin],
+ }),
+ );
+ if (!listed.ok) throw listed.error;
+ return listed.value;
+ };
+ const throughHttp = await list(endpoint);
+ const throughSocket = await list(socket);
+ // Target.getTargets reports attachment state that /json/list may omit.
+ const withoutAttachment = (targets) =>
+ targets.map(({ attached: _attached, ...rest }) => rest);
+ assert.ok(throughSocket.targets.length > 0, "Browser socket listed no page");
+ assert.deepEqual(
+ withoutAttachment(throughSocket.targets),
+ withoutAttachment(throughHttp.targets),
+ );
+ assert.ok(
+ throughSocket.targets.every(
+ ({ attached }) => typeof attached === "boolean",
+ ),
+ "Browser socket omitted target attachment state",
+ );
+ assert.equal(throughSocket.browser.product, throughHttp.browser.product);
+
+ const evidence = await artifactCliEvidence("inspect-web-page", socket, [
+ target,
+ "--allowed-origins",
+ origin,
+ "--observation-ms",
+ "100",
+ ]);
+ assert.equal(evidence.normalized_result.target.target_id, target);
+ assert.ok(
+ evidence.normalized_result.dom.nodes.length > 0,
+ "Browser-socket inspection returned no DOM nodes",
+ );
+ assert.equal(
+ evidence.parameters.cdp_endpoint,
+ `ws://127.0.0.1:${port}/devtools/browser/[REDACTED]`,
+ );
+ assert.ok(
+ !JSON.stringify(evidence).includes(path),
+ "Browser-socket Evidence retained the socket identifier",
+ );
+ const throughHttpInspection = await provider.inspectPage(
+ inspectWebPageInputSchema.parse({
+ cdp_endpoint: endpoint,
+ allowed_origins: [origin],
+ target_id: target,
+ observation_ms: 100,
+ }),
+ );
+ if (!throughHttpInspection.ok) throw throughHttpInspection.error;
+ const compared = await provider.compareCaptures(
+ compareWebCapturesInputSchema.parse({
+ before: { inspection: throughHttpInspection.value },
+ after: { inspection: evidence.normalized_result },
+ }),
+ );
+ if (!compared.ok) throw compared.error;
+ assert.equal(compared.value.dimensions.dom_structure.status, "unchanged");
+ assert.equal(compared.value.dimensions.scripts.status, "unchanged");
+ return {
+ targets: throughSocket.targets.length,
+ revision: throughSocket.browser.revision,
+ };
+}
diff --git a/scripts/lib/catalog-builders.mjs b/scripts/lib/catalog-builders.mjs
index 32ec51ee3..d924ef49c 100644
--- a/scripts/lib/catalog-builders.mjs
+++ b/scripts/lib/catalog-builders.mjs
@@ -213,7 +213,9 @@ export const providerCatalog = (sources) => {
},
{
identity: sources.wasmProvider.WABT_PROVIDER_IDENTITY,
- contracts: sources.wasmContracts.WASM_TOOL_CONTRACTS,
+ contracts: sources.wasmContracts.WASM_TOOL_CONTRACTS.filter(
+ ({ kind }) => kind === "artifact-provider",
+ ),
},
{
identity: sources.evmProvider.EVMOLE_PROVIDER_IDENTITY,
diff --git a/scripts/lib/clean-linux-hopper-runtime.mjs b/scripts/lib/clean-linux-hopper-runtime.mjs
index b43eca7fc..41ccfed6c 100644
--- a/scripts/lib/clean-linux-hopper-runtime.mjs
+++ b/scripts/lib/clean-linux-hopper-runtime.mjs
@@ -68,18 +68,19 @@ export const assertCleanLinuxHopperRuntime = async (
"utf8",
);
const arguments_ = commandLine.split("\0");
- let kernelCommand;
- if (commandLine.length === 0) {
- // Zombies have neither arguments nor an executable link, but keep
- // their kernel command name until their parent reaps them.
- kernelCommand = (
- await readFile(`${procRoot}/${entry.name}/comm`, "utf8")
- ).replace(/\n$/u, "");
- if (/^python3(?:\.\d+)?$/u.test(kernelCommand))
- throw new Error(
- "Cannot identify a command-less Python process on the clean Linux runner",
- );
- }
+ // The kernel command field identifies renamed runtime processes such
+ // as Xvfb. Keep it separate from arguments so ordinary script text does
+ // not count as a retained runtime.
+ const kernelCommand = (
+ await readFile(`${procRoot}/${entry.name}/comm`, "utf8")
+ ).replace(/\n$/u, "");
+ if (
+ commandLine.length === 0 &&
+ /^python3(?:\.\d+)?$/u.test(kernelCommand)
+ )
+ throw new Error(
+ "Cannot identify a command-less Python process on the clean Linux runner",
+ );
retainedRuntime =
kernelCommand === "Hopper" ||
kernelCommand === "Xvfb" ||
diff --git a/scripts/lib/docs-facts.mjs b/scripts/lib/docs-facts.mjs
index 26d9a4ac3..dda249aaa 100644
--- a/scripts/lib/docs-facts.mjs
+++ b/scripts/lib/docs-facts.mjs
@@ -1,13 +1,41 @@
import { readFile, readdir, stat } from "node:fs/promises";
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
-const readmeLinkTargets = (content, language = "en") =>
- [...content.matchAll(/\]\(([^)\s]+)\)/gu)]
- .map((match) => match[1] ?? "")
+const repoRelative = (root, from, target) => {
+ const index = target.search(/[#?]/u);
+ const pathPart = index === -1 ? target : target.slice(0, index);
+ const suffix = index === -1 ? "" : target.slice(index);
+ if (pathPart.length === 0 || /^[a-z][a-z0-9+.-]*:/iu.test(pathPart))
+ return target;
+ const absolute = resolve(root, dirname(from), pathPart);
+ const withinRepo = relative(root, absolute);
+ if (
+ withinRepo === ".." ||
+ withinRepo.startsWith(`..${sep}`) ||
+ isAbsolute(withinRepo)
+ )
+ return target;
+ return `${withinRepo.split(sep).join("/")}${suffix}`;
+};
+
+const isLanguageNavigation = (target) => {
+ const path = target.split(/[#?]/u)[0];
+ return (
+ path === "README.md" ||
+ /^docs\/readme\/README_[A-Za-z0-9-]+\.md$/u.test(path)
+ );
+};
+
+const readmeDestinations = (content) => [
+ ...content.matchAll(/\]\(([^)\s]+)\)/gu),
+ ...content.matchAll(/\b(?:src|href)="([^"]+)"/gu),
+];
+
+const readmeLinkTargets = (root, from, content, language = "en") =>
+ readmeDestinations(content)
+ .map((match) => repoRelative(root, from, match[1] ?? ""))
.filter(
- (target) =>
- !target.startsWith("#") &&
- !/^README(?:_[A-Za-z0-9-]+)?\.md$/u.test(target),
+ (target) => !target.startsWith("#") && !isLanguageNavigation(target),
)
.map((target) => {
if (language === "en" || !URL.canParse(target)) return target;
@@ -70,9 +98,19 @@ export const documentationFactIssues = async (root, catalog) => {
const issues = await skillReferenceIssues(
join(root, "skills/reverse-engineer-anything"),
);
- const readmePaths = (await readdir(root))
- .filter((path) => /^README(?:_[A-Za-z0-9-]+)?\.md$/u.test(path))
- .sort();
+ const translatedFolder = join(root, "docs/readme");
+ let translatedNames = [];
+ try {
+ translatedNames = await readdir(translatedFolder);
+ } catch (cause) {
+ if (cause?.code !== "ENOENT") throw cause;
+ }
+ const readmePaths = [
+ "README.md",
+ ...translatedNames
+ .filter((name) => /^README_[A-Za-z0-9-]+\.md$/u.test(name))
+ .map((name) => `docs/readme/${name}`),
+ ].sort();
const readmes = new Map();
for (const path of readmePaths) {
const content = await readFile(join(root, path), "utf8");
@@ -94,7 +132,11 @@ export const documentationFactIssues = async (root, catalog) => {
);
}
}
- const canonicalLinks = readmeLinkTargets(readmes.get("README.md") ?? "");
+ const canonicalLinks = readmeLinkTargets(
+ root,
+ "README.md",
+ readmes.get("README.md") ?? "",
+ );
const languageInventory = JSON.parse(
await readFile(join(root, "website/locales/languages.json"), "utf8"),
);
@@ -104,7 +146,12 @@ export const documentationFactIssues = async (root, catalog) => {
for (const path of readmePaths.filter((path) => path !== "README.md"))
if (
JSON.stringify(
- readmeLinkTargets(readmes.get(path) ?? "", readmeLanguages.get(path)),
+ readmeLinkTargets(
+ root,
+ path,
+ readmes.get(path) ?? "",
+ readmeLanguages.get(path),
+ ),
) !== JSON.stringify(canonicalLinks)
)
issues.push(`${path}: documentation links differ from README.md`);
diff --git a/scripts/lib/http3-capture-assertions.mjs b/scripts/lib/http3-capture-assertions.mjs
new file mode 100644
index 000000000..ca08d32cc
--- /dev/null
+++ b/scripts/lib/http3-capture-assertions.mjs
@@ -0,0 +1,243 @@
+import assert from "node:assert/strict";
+import { createHash } from "node:crypto";
+import { isUtf8 } from "node:buffer";
+const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
+// The native decoder displays strict UTF-8 and retains invalid bytes as null text.
+const textOf = (expected) => {
+ const value = Buffer.from(expected.base64, "base64");
+ return isUtf8(value) ? value.toString("utf8") : null;
+};
+const field = (record, pointer) =>
+ record.binary_fields.find((item) => item.pointer === pointer);
+const bytes = (record, pointer) => {
+ const value = field(record, pointer);
+ assert.equal(
+ value?.state,
+ "retained",
+ `Missing retained bytes at ${pointer}`,
+ );
+ const decoded = Buffer.from(value.content_base64, "base64");
+ assert.equal(value.bytes, decoded.length);
+ assert.equal(value.sha256, digest(decoded));
+ return decoded;
+};
+const binaryEqual = (record, pointer, expected) => {
+ const decoded = bytes(record, pointer);
+ assert.deepEqual(decoded, Buffer.from(expected.base64, "base64"), pointer);
+ assert.equal(field(record, pointer).sha256, expected.sha256);
+ assert.equal(field(record, pointer).bytes, expected.bytes);
+};
+const caseOf = (record) =>
+ record.reported.request.headers.find(
+ ([name]) => name === "x-lab-case",
+ )?.[1] ?? "curl";
+const byCase = (value, name) => {
+ const selected = value.records.filter((record) => caseOf(record) === name);
+ assert.equal(
+ selected.length,
+ 1,
+ `Expected exactly one producer record for ${name}`,
+ );
+ return selected[0];
+};
+const connectionH3 = (record, side) => {
+ const conn = record.reported[side];
+ assert.equal(conn.transport_protocol, "udp");
+ assert.equal(bytes(record, `/${side}/alpn`).toString(), "h3");
+ assert.equal(conn.alpn, "h3", `${side}.alpn readable value`);
+ assert.equal(typeof conn.timestamp_tls_setup, "number");
+};
+
+function assertHeaders(record, side, headers) {
+ const regular = headers.filter(
+ ([name]) => !Buffer.from(name.base64, "base64").toString().startsWith(":"),
+ );
+ assert.equal(record.reported[side].headers.length, regular.length);
+ for (const [index, pair] of regular.entries()) {
+ binaryEqual(record, `/${side}/headers/${index}/0`, pair[0]);
+ binaryEqual(record, `/${side}/headers/${index}/1`, pair[1]);
+ assert.deepEqual(
+ record.reported[side].headers[index],
+ pair.map(textOf),
+ `${side}.headers[${index}] readable values`,
+ );
+ }
+}
+
+export function assertEndpointHeaders(actual, expected) {
+ // The proxy rebuilds pseudo fields from its HTTP state. Compare their values
+ // by name while retaining the exact order and duplicates of regular headers.
+ const partition = (headers) => {
+ const pseudo = new Map();
+ const regular = [];
+ for (const pair of headers) {
+ const name = Buffer.from(pair[0].base64, "base64").toString();
+ if (name.startsWith(":")) {
+ assert.equal(pseudo.has(name), false);
+ pseudo.set(name, pair);
+ } else regular.push(pair);
+ }
+ return { pseudo, regular };
+ };
+ assert.deepEqual(partition(actual), partition(expected));
+}
+
+export function requireHttp3(record) {
+ for (const side of ["request", "response"])
+ assert.equal(record.reported[side].http_version, "HTTP/3");
+ connectionH3(record, "client_conn");
+ connectionH3(record, "server_conn");
+}
+
+export function verifyNormalRecords(normal, manifest, finalProducer) {
+ for (const record of normal.records) {
+ const name = caseOf(record);
+ const producer = finalProducer.find(
+ (event) => event.id === record.reported.id,
+ );
+ assert.ok(producer, `No actual producer observation for ${name}`);
+ assert.equal(record.reported.error, null);
+ assert.equal(producer.error, null);
+ assert.equal(record.reported.response.status_code, 200);
+ requireHttp3(record);
+ for (const side of ["request", "response"]) {
+ assert.equal(
+ record.reported[side].http_version,
+ producer[side].http_version,
+ );
+ binaryEqual(record, `/${side}/content`, producer[side].content);
+ assertHeaders(
+ record,
+ side,
+ manifest[side === "request" ? "requests" : "responses"][name].headers,
+ );
+ }
+ // Native UUIDs must be retained as UUIDs; the producer exports no stream mapping.
+ for (const side of ["client_conn", "server_conn"])
+ for (const key of ["id", "tls_version", "timestamp_tls_setup", "error"])
+ assert.equal(
+ record.reported[side][key],
+ producer[side][key],
+ `${side}.${key} differs from producer observation ${producer.id}`,
+ );
+ binaryEqual(record, "/request/content", manifest.requests[name].body);
+ binaryEqual(record, "/response/content", manifest.responses[name].body);
+ const pseudo = {
+ ":method": "method",
+ ":scheme": "scheme",
+ ":authority": "authority",
+ ":path": "path",
+ };
+ for (const [headerName, headerValue] of manifest.requests[name].headers) {
+ const decodedName = Buffer.from(headerName.base64, "base64").toString();
+ if (pseudo[decodedName]) {
+ binaryEqual(record, `/request/${pseudo[decodedName]}`, headerValue);
+ assert.equal(
+ record.reported.request[pseudo[decodedName]],
+ textOf(headerValue),
+ `request.${pseudo[decodedName]} readable value`,
+ );
+ }
+ }
+ }
+}
+
+export { bytes, caseOf, byCase };
+
+export function verifyConcurrency(normal, manifest) {
+ const parallelNames = ["parallel-0", "parallel-1", "parallel-2"];
+ const sent = manifest.events.filter(
+ (event) =>
+ event.event === "request-sent" && parallelNames.includes(event.case),
+ );
+ const received = manifest.events.filter(
+ (event) =>
+ event.event === "request-received" && parallelNames.includes(event.case),
+ );
+ const responses = manifest.events.filter(
+ (event) =>
+ event.event === "response-sent" && parallelNames.includes(event.case),
+ );
+ assert.equal(new Set(sent.map((event) => event.connection)).size, 1);
+ assert.equal(new Set(received.map((event) => event.connection)).size, 1);
+ assert.equal(new Set(sent.map((event) => event.stream_id)).size, 3);
+ assert.equal(new Set(received.map((event) => event.stream_id)).size, 3);
+ assert.ok(
+ Math.max(...received.map((event) => event.monotonic)) <
+ responses[0].monotonic,
+ );
+ assert.deepEqual(
+ responses.map((event) => event.case),
+ received.map((event) => event.case).reverse(),
+ );
+ for (const side of ["client_conn", "server_conn"])
+ assert.equal(
+ new Set(
+ parallelNames.map((name) => byCase(normal, name).reported[side].id),
+ ).size,
+ 1,
+ );
+ const normalConnections = new Set(
+ [...sent, ...received].map((event) => event.connection),
+ );
+ for (const connection of normalConnections) {
+ const handshakes = manifest.events.filter(
+ (event) => event.event === "handshake" && event.connection === connection,
+ );
+ assert.equal(handshakes.length, 1);
+ assert.equal(handshakes[0].alpn, "h3");
+ assert.equal(handshakes[0].quic_version, 1);
+ }
+ assert.equal(
+ new Set(normal.records.map((record) => record.reported.id)).size,
+ 4,
+ );
+ assert.equal(sent.length, 3);
+ const receivedByClient = manifest.events.filter(
+ (event) =>
+ event.event === "response-received" &&
+ sent.some(
+ (request) =>
+ request.connection === event.connection &&
+ request.stream_id === event.stream_id,
+ ),
+ );
+ assert.equal(receivedByClient.length, 3);
+ for (const request of sent) {
+ const response = receivedByClient.find(
+ (event) =>
+ event.connection === request.connection &&
+ event.stream_id === request.stream_id,
+ );
+ assert.ok(
+ response,
+ "Each sent request needs an independently observed client response",
+ );
+ assert.deepEqual(response.body, manifest.responses[request.case].body);
+ assert.deepEqual(request.body, manifest.requests[request.case].body);
+ assertEndpointHeaders(
+ request.headers,
+ manifest.requests[request.case].headers,
+ );
+ assertEndpointHeaders(
+ response.headers,
+ manifest.responses[request.case].headers,
+ );
+ }
+ return {
+ sent: sent.map((event) => event.case),
+ completed_at_origin: received.map((event) => event.case),
+ dispatched_at_origin: responses.map((event) => event.case),
+ completed_at_client: receivedByClient.map(
+ (event) =>
+ sent.find(
+ (request) =>
+ request.connection === event.connection &&
+ request.stream_id === event.stream_id,
+ ).case,
+ ),
+ saved_in_capture: normal.records
+ .filter((record) => parallelNames.includes(caseOf(record)))
+ .map(caseOf),
+ };
+}
diff --git a/scripts/lib/http3-capture-assertions.test.mjs b/scripts/lib/http3-capture-assertions.test.mjs
new file mode 100644
index 000000000..bc7e3c602
--- /dev/null
+++ b/scripts/lib/http3-capture-assertions.test.mjs
@@ -0,0 +1,163 @@
+import assert from "node:assert/strict";
+import { createHash } from "node:crypto";
+import { test } from "node:test";
+import { verifyNormalRecords } from "./http3-capture-assertions.mjs";
+
+function connectionFactsFixture() {
+ const body = Buffer.from([0, 255, 128, 1]);
+ const view = (bytes) => ({
+ base64: bytes.toString("base64"),
+ bytes: bytes.length,
+ sha256: createHash("sha256").update(bytes).digest("hex"),
+ });
+ const producer = {
+ id: "flow-id",
+ error: null,
+ request: { http_version: "HTTP/3", content: view(body) },
+ response: { http_version: "HTTP/3", content: view(body) },
+ client_conn: {
+ id: "client-id",
+ transport_protocol: "udp",
+ tls_version: "QUICv1",
+ timestamp_tls_setup: 1_700_000_000.125678,
+ error: null,
+ },
+ server_conn: {
+ id: "server-id",
+ transport_protocol: "udp",
+ tls_version: "QUICv1",
+ timestamp_tls_setup: 1_700_000_000.987654,
+ error: null,
+ },
+ };
+ const reported = structuredClone(producer);
+ const requestFields = {
+ method: "POST",
+ scheme: "https",
+ authority: "lab.test:18443",
+ path: "/echo?case=readable",
+ };
+ Object.assign(reported.request, requestFields);
+ const headers = [
+ [Buffer.from("x-repeat"), Buffer.from("first-α")],
+ [Buffer.from("x-repeat"), Buffer.from("second-β")],
+ [Buffer.from("x-binary"), Buffer.from([255])],
+ ];
+ for (const side of ["request", "response"])
+ reported[side].headers = [
+ ["x-repeat", "first-α"],
+ ["x-repeat", "second-β"],
+ ["x-binary", null],
+ ];
+ reported.client_conn.alpn = "h3";
+ reported.server_conn.alpn = "h3";
+ reported.response.status_code = 200;
+ const binary_fields = [
+ ["/request/content", body],
+ ["/response/content", body],
+ ["/client_conn/alpn", Buffer.from("h3")],
+ ["/server_conn/alpn", Buffer.from("h3")],
+ ...Object.entries(requestFields).map(([name, value]) => [
+ `/request/${name}`,
+ Buffer.from(value),
+ ]),
+ ...["request", "response"].flatMap((side) =>
+ headers.flatMap((pair, index) =>
+ pair.map((value, part) => [`/${side}/headers/${index}/${part}`, value]),
+ ),
+ ),
+ ].map(([pointer, bytes]) => {
+ const { base64, ...facts } = view(bytes);
+ return {
+ pointer,
+ representation: "producer-bytes",
+ state: "retained",
+ content_base64: base64,
+ ...facts,
+ };
+ });
+ return {
+ normal: { records: [{ reported, binary_fields }] },
+ manifest: {
+ requests: {
+ curl: {
+ headers: [
+ ...Object.entries(requestFields).map(([name, value]) => [
+ view(Buffer.from(`:${name}`)),
+ view(Buffer.from(value)),
+ ]),
+ ...headers.map((pair) => pair.map(view)),
+ ],
+ body: view(body),
+ },
+ },
+ responses: {
+ curl: {
+ headers: headers.map((pair) => pair.map(view)),
+ body: view(body),
+ },
+ },
+ },
+ producer: [producer],
+ };
+}
+
+test("positive assertions retain exact producer connection facts on both legs", () => {
+ const { normal, manifest, producer } = connectionFactsFixture();
+ verifyNormalRecords(normal, manifest, producer);
+});
+
+for (const [field, value] of [
+ ["method", "GET"],
+ ["scheme", "http"],
+ ["authority", "other.test:18443"],
+ ["path", "/different"],
+])
+ test(`positive assertions reject changed readable request ${field}`, () => {
+ const { normal, manifest, producer } = connectionFactsFixture();
+ normal.records[0].reported.request[field] = value;
+ assert.throws(() => verifyNormalRecords(normal, manifest, producer), {
+ code: "ERR_ASSERTION",
+ });
+ });
+
+for (const side of ["client_conn", "server_conn"])
+ test(`positive assertions reject null readable ${side} ALPN`, () => {
+ const { normal, manifest, producer } = connectionFactsFixture();
+ normal.records[0].reported[side].alpn = null;
+ assert.throws(() => verifyNormalRecords(normal, manifest, producer), {
+ code: "ERR_ASSERTION",
+ });
+ });
+
+for (const side of ["request", "response"])
+ for (const [index, part, value] of [
+ [0, 0, "x-changed"],
+ [0, 1, "changed-first"],
+ [1, 1, "changed-second"],
+ [2, 1, "�"],
+ ])
+ test(`positive assertions reject changed readable ${side} header ${index}/${part}`, () => {
+ const { normal, manifest, producer } = connectionFactsFixture();
+ normal.records[0].reported[side].headers[index][part] = value;
+ assert.throws(() => verifyNormalRecords(normal, manifest, producer), {
+ code: "ERR_ASSERTION",
+ });
+ });
+
+for (const side of ["client_conn", "server_conn"])
+ for (const [field, value] of [
+ ["tls_version", null],
+ ["tls_version", "changed-producer-label"],
+ ["error", "changed-connection-error"],
+ ["error", undefined],
+ ["timestamp_tls_setup", 0],
+ ["timestamp_tls_setup", 1_700_000_000],
+ ])
+ test(`positive assertions reject ${side}.${field} changed to ${String(value)}`, () => {
+ const { normal, manifest, producer } = connectionFactsFixture();
+ normal.records[0].reported[side][field] = value;
+ assert.throws(() => verifyNormalRecords(normal, manifest, producer), {
+ code: "ERR_ASSERTION",
+ });
+ });
diff --git a/scripts/lib/http3-verifier-lifecycle.mjs b/scripts/lib/http3-verifier-lifecycle.mjs
new file mode 100644
index 000000000..d083bf8d6
--- /dev/null
+++ b/scripts/lib/http3-verifier-lifecycle.mjs
@@ -0,0 +1,143 @@
+import assert from "node:assert/strict";
+import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+import { DatabaseSync } from "node:sqlite";
+import { setTimeout } from "node:timers/promises";
+import { cleanupOwnedProcessGroup } from "../../dist/process/ProcessOwnership.js";
+import { observeOwnedProcessGroups } from "../../dist/process/ProcessOwnershipObservation.js";
+
+export const describeError = (error) => ({
+ name: error?.name ?? "Error",
+ message: error?.message ?? String(error),
+ ...(error?.stack ? { stack: error.stack } : {}),
+});
+
+export async function attemptAll(actions) {
+ const failures = [];
+ for (const [name, action] of actions) {
+ try {
+ await action();
+ } catch (error) {
+ failures.push({ action: name, ...describeError(error) });
+ }
+ }
+ return failures;
+}
+
+async function latest(root, report) {
+ const temporary = join(root, `.verification-${report.run_id}.tmp`);
+ await writeFile(temporary, JSON.stringify(report, null, 2), { flag: "wx" });
+ await rename(temporary, join(root, "verification-latest.json"));
+}
+
+async function withLatestLock(root, action) {
+ // Keep this pathname stable: removing it could split concurrent lock owners.
+ // SQLite releases its OS-backed lock when the connection/process closes.
+ const database = new DatabaseSync(join(root, ".verification-lock.sqlite"));
+ try {
+ for (;;) {
+ try {
+ database.exec("BEGIN EXCLUSIVE");
+ break;
+ } catch (error) {
+ if (
+ error.code !== "ERR_SQLITE_ERROR" ||
+ (error.errcode !== 5 && error.errcode !== 6)
+ )
+ throw error;
+ // Yield while another process (or async operation here) owns the lock.
+ // A synchronous busy timeout would block that operation's fs callbacks.
+ await setTimeout(10);
+ }
+ }
+ return await action();
+ } finally {
+ database.close();
+ }
+}
+
+export async function prepareAttempt(root, inspectExisting, run) {
+ if (!inspectExisting) await mkdir(root, { mode: 0o700 });
+ const directory = join(root, "verifications", run.run_id);
+ await mkdir(directory, { recursive: true, mode: 0o700 });
+ await withLatestLock(root, async () => {
+ // Preserve older reports as history, without leaving an ambiguous success
+ // at the capture root. Registration is the shared marker's ordering point.
+ try {
+ await rename(
+ join(root, "verification.json"),
+ join(directory, "legacy-verification.json"),
+ );
+ } catch (error) {
+ if (error.code !== "ENOENT") throw error;
+ }
+ await latest(root, {
+ run_id: run.run_id,
+ status: "running",
+ started_at: new Date().toISOString(),
+ directory,
+ });
+ });
+ return { directory, captureRoot: join(root, "captures") };
+}
+
+export async function publishAttempt(root, directory, report) {
+ await writeFile(
+ join(directory, "result.json"),
+ JSON.stringify(report, null, 2),
+ { flag: "wx" },
+ );
+ if (report.status === "passed")
+ await writeFile(
+ join(directory, "verification.json"),
+ JSON.stringify(report, null, 2),
+ { flag: "wx" },
+ );
+ await withLatestLock(root, async () => {
+ const current = JSON.parse(
+ await readFile(join(root, "verification-latest.json"), "utf8"),
+ );
+ if (current.run_id !== report.run_id) return;
+ // Ownership check and replacement share one lock with attempt registration.
+ // An older attempt still keeps its reports, but cannot replace this marker.
+ await latest(root, {
+ run_id: report.run_id,
+ status: report.status,
+ directory,
+ finished_at: report.finished_at,
+ });
+ });
+}
+
+export async function finishProducerGroups(captureRoot, runId) {
+ let leases;
+ try {
+ leases = (await readFile(join(captureRoot, "process-groups.jsonl"), "utf8"))
+ .trim()
+ .split("\n")
+ .filter(Boolean)
+ .map(JSON.parse);
+ } catch (error) {
+ if (error.code === "ENOENT") return { groups: [], observation: [] };
+ throw error;
+ }
+ for (const lease of leases) assert.equal(lease.runId, runId);
+ const failures = await attemptAll(
+ leases.map((lease) => [
+ `producer-group-${lease.processGroupId}`,
+ async () => {
+ const result = await cleanupOwnedProcessGroup(lease);
+ assert.equal(result.cleaned, true, JSON.stringify(result));
+ },
+ ]),
+ );
+ // Inspect even when one close failed; the groups can outlive their bootloader
+ // parent and disappear from a plain descendant-only observation.
+ const observation = [
+ ...(await observeOwnedProcessGroups(
+ runId,
+ leases.map((lease) => lease.processGroupId),
+ )),
+ ];
+ return { groups: leases, failures, observation };
+}
diff --git a/scripts/lib/http3-verifier-lifecycle.test.mjs b/scripts/lib/http3-verifier-lifecycle.test.mjs
new file mode 100644
index 000000000..fc7c3f5d2
--- /dev/null
+++ b/scripts/lib/http3-verifier-lifecycle.test.mjs
@@ -0,0 +1,433 @@
+import assert from "node:assert/strict";
+import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { test } from "node:test";
+import { spawn } from "node:child_process";
+import { randomUUID } from "node:crypto";
+import { once } from "node:events";
+import { setTimeout } from "node:timers/promises";
+import { DatabaseSync } from "node:sqlite";
+import {
+ attemptAll,
+ prepareAttempt,
+ publishAttempt,
+ finishProducerGroups,
+} from "./http3-verifier-lifecycle.mjs";
+import { cleanupOwnedProcessGroup } from "../../dist/process/ProcessOwnership.js";
+import { runOwnedCommand } from "../../dist/process/OwnedCommand.js";
+import { verifyConcurrency } from "./http3-capture-assertions.mjs";
+
+async function waitForFile(path) {
+ const signal = AbortSignal.timeout(5_000);
+ for (;;) {
+ try {
+ await readFile(path);
+ return;
+ } catch (error) {
+ if (error.code !== "ENOENT") throw error;
+ }
+ await setTimeout(10, undefined, { signal });
+ }
+}
+
+function startAttemptWorker(root, report) {
+ const worker = `
+ import { readFile, writeFile } from "node:fs/promises";
+ import { join } from "node:path";
+ import { setTimeout } from "node:timers/promises";
+ import { prepareAttempt, publishAttempt } from ${JSON.stringify(
+ new URL("./http3-verifier-lifecycle.mjs", import.meta.url).href,
+ )};
+ const [root, serialized] = process.argv.slice(1);
+ const report = JSON.parse(serialized);
+ const attempt = await prepareAttempt(root, true, report);
+ await writeFile(join(root, report.run_id + ".ready"), "");
+ for (;;) {
+ try { await readFile(join(root, report.run_id + ".finish")); break; }
+ catch (error) { if (error.code !== "ENOENT") throw error; }
+ await setTimeout(10);
+ }
+ await publishAttempt(root, attempt.directory, report);
+ `;
+ return runOwnedCommand(
+ {
+ command: process.execPath,
+ arguments: [
+ "--input-type=module",
+ "--eval",
+ worker,
+ root,
+ JSON.stringify(report),
+ ],
+ cwd: root,
+ hostEnvironment: process.env,
+ runId: randomUUID(),
+ },
+ { timeoutMs: 10_000, diagnosticBytes: 4096 },
+ ).then(
+ (result) => ({ result }),
+ (error) => ({ error }),
+ );
+}
+
+test("cleanup continues through failed closes and reaches its audit", async () => {
+ const attempted = [];
+ const failures = await attemptAll(
+ ["client", "transport", "audit"].map((name) => [
+ name,
+ async () => {
+ attempted.push(name);
+ if (name !== "audit") throw new Error(`${name} failed`);
+ },
+ ]),
+ );
+ assert.deepEqual(attempted, ["client", "transport", "audit"]);
+ assert.deepEqual(
+ failures.map((failure) => failure.action),
+ ["client", "transport"],
+ );
+});
+
+test("a failed reinspection cannot leave the earlier success as the current result", async () => {
+ const root = await mkdtemp(join(tmpdir(), "rea-http3-results-"));
+ try {
+ await writeFile(
+ join(root, "verification.json"),
+ '{"status":"passed","run_id":"legacy"}',
+ );
+ const first = await prepareAttempt(root, true, { run_id: "first" });
+ await publishAttempt(root, first.directory, {
+ run_id: "first",
+ status: "passed",
+ });
+ const next = await prepareAttempt(root, true, { run_id: "next" });
+ assert.equal(
+ JSON.parse(await readFile(join(root, "verification-latest.json"))).status,
+ "running",
+ );
+ await publishAttempt(root, next.directory, {
+ run_id: "next",
+ status: "failed",
+ error: { message: "new failure" },
+ });
+ assert.equal(
+ JSON.parse(await readFile(join(root, "verification-latest.json"))).run_id,
+ "next",
+ );
+ assert.equal(
+ JSON.parse(await readFile(join(root, "verification-latest.json"))).status,
+ "failed",
+ );
+ await assert.rejects(readFile(join(next.directory, "verification.json")), {
+ code: "ENOENT",
+ });
+ await assert.rejects(readFile(join(root, "verification.json")), {
+ code: "ENOENT",
+ });
+ assert.equal(
+ JSON.parse(await readFile(join(first.directory, "verification.json")))
+ .status,
+ "passed",
+ );
+ assert.equal(
+ JSON.parse(
+ await readFile(join(first.directory, "legacy-verification.json")),
+ ).run_id,
+ "legacy",
+ );
+ } finally {
+ await rm(root, { recursive: true, force: true });
+ }
+});
+
+test("separate overlapping attempts finishing in reverse order retain the newer failure", async () => {
+ const root = await mkdtemp(join(tmpdir(), "rea-http3-overlap-"));
+ const commands = [];
+ const failed = {
+ run_id: "newer",
+ status: "failed",
+ error: { message: "newer inspection failed" },
+ cleanup_failures: [{ action: "transport-close", message: "close failed" }],
+ };
+ let primaryError;
+ const start = (report) => {
+ const command = startAttemptWorker(root, report);
+ commands.push(command);
+ return command;
+ };
+ try {
+ const older = start({ run_id: "older", status: "passed" });
+ await waitForFile(join(root, "older.ready"));
+ const newer = start(failed);
+ await waitForFile(join(root, "newer.ready"));
+ await writeFile(join(root, "newer.finish"), "");
+ const newerResult = await newer;
+ if (newerResult.error) throw newerResult.error;
+ assert.equal(newerResult.result.exitCode, 0);
+ assert.equal(newerResult.result.signal, null);
+ const marker = await readFile(join(root, "verification-latest.json"));
+ await writeFile(join(root, "older.finish"), "");
+ const olderResult = await older;
+ if (olderResult.error) throw olderResult.error;
+ assert.equal(olderResult.result.exitCode, 0);
+ assert.equal(olderResult.result.signal, null);
+ assert.deepEqual(
+ await readFile(join(root, "verification-latest.json")),
+ marker,
+ );
+ assert.deepEqual(
+ JSON.parse(await readFile(join(root, "verifications/newer/result.json"))),
+ failed,
+ );
+ await assert.rejects(
+ readFile(join(root, "verifications/newer/verification.json")),
+ {
+ code: "ENOENT",
+ },
+ );
+ assert.equal(
+ JSON.parse(
+ await readFile(join(root, "verifications/older/verification.json")),
+ ).status,
+ "passed",
+ );
+ } catch (error) {
+ primaryError = error;
+ } finally {
+ // Release both barriers even after an assertion/setup failure. Each command
+ // retains native supervision through termination before the root is removed.
+ const failures = await attemptAll(
+ ["older", "newer"].map((name) => [
+ name,
+ () => writeFile(join(root, name + ".finish"), ""),
+ ]),
+ );
+ const outcomes = await Promise.all(commands);
+ const retained = outcomes.filter(
+ ({ error }) => error?.cleanupFailure || error?.cleanupResult,
+ );
+ if (retained.length)
+ primaryError = new AggregateError(
+ [primaryError, ...retained.map(({ error }) => error)].filter(Boolean),
+ `Worker cleanup failed; retained test evidence at ${root}`,
+ { cause: primaryError },
+ );
+ else
+ failures.push(
+ ...(await attemptAll([
+ ["root", () => rm(root, { recursive: true, force: true })],
+ ])),
+ );
+ if (failures.length)
+ primaryError = new AggregateError(
+ [primaryError, ...failures].filter(Boolean),
+ "Test cleanup failed",
+ { cause: primaryError },
+ );
+ }
+ if (primaryError) throw primaryError;
+});
+
+test("publication waits for the lock before reading the current owner", async () => {
+ const root = await mkdtemp(join(tmpdir(), "rea-http3-lock-"));
+ let database;
+ let publication;
+ let primaryError;
+ try {
+ const older = await prepareAttempt(root, true, { run_id: "older" });
+ database = new DatabaseSync(join(root, ".verification-lock.sqlite"));
+ database.exec("BEGIN EXCLUSIVE");
+ publication = publishAttempt(root, older.directory, {
+ run_id: "older",
+ status: "passed",
+ });
+ await waitForFile(join(older.directory, "verification.json"));
+ const marker = join(root, "verification-latest.json");
+ assert.equal(JSON.parse(await readFile(marker)).status, "running");
+ // This connection represents another marker writer. Its replacement must
+ // be observed only after the blocked publisher acquires the same lock.
+ const newer = { run_id: "newer", status: "failed" };
+ await writeFile(marker, JSON.stringify(newer));
+ database.close();
+ database = undefined;
+ await publication;
+ assert.deepEqual(JSON.parse(await readFile(marker)), newer);
+ } catch (error) {
+ primaryError = error;
+ } finally {
+ const failures = await attemptAll([
+ ["lock", () => database?.close()],
+ [
+ "publication",
+ async () => {
+ if (publication) await publication;
+ },
+ ],
+ ["root", () => rm(root, { recursive: true, force: true })],
+ ]);
+ if (failures.length)
+ primaryError = new AggregateError(
+ [primaryError, ...failures].filter(Boolean),
+ "Test cleanup failed",
+ { cause: primaryError },
+ );
+ }
+ if (primaryError) throw primaryError;
+});
+
+test("a failed marker read releases the lock and preserves the attempt report", async () => {
+ const root = await mkdtemp(join(tmpdir(), "rea-http3-lock-failure-"));
+ try {
+ const older = await prepareAttempt(root, true, { run_id: "older" });
+ await writeFile(join(root, "verification-latest.json"), "{");
+ const report = {
+ run_id: "older",
+ status: "failed",
+ error: { message: "inspection failed" },
+ };
+ await assert.rejects(
+ publishAttempt(root, older.directory, report),
+ SyntaxError,
+ );
+ assert.deepEqual(
+ JSON.parse(await readFile(join(older.directory, "result.json"))),
+ report,
+ );
+ const next = await prepareAttempt(root, true, { run_id: "next" });
+ await publishAttempt(root, next.directory, {
+ run_id: "next",
+ status: "passed",
+ });
+ assert.equal(
+ JSON.parse(await readFile(join(root, "verification-latest.json"))).run_id,
+ "next",
+ );
+ } finally {
+ await rm(root, { recursive: true, force: true });
+ }
+});
+
+test("dispatch reverses origin completion; client and capture may arrive in other orders", () => {
+ const names = ["parallel-0", "parallel-1", "parallel-2"];
+ const header = (text) => ({ base64: Buffer.from(text).toString("base64") });
+ const messages = Object.fromEntries(
+ names.map((name) => [
+ name,
+ {
+ headers: [[header("x-lab-case"), header(name)]],
+ body: { base64: header(name).base64 },
+ },
+ ]),
+ );
+ const event = (kind, index, connection, monotonic) => ({
+ event: kind,
+ case: names[index],
+ stream_id: index * 4,
+ connection,
+ monotonic,
+ ...messages[names[index]],
+ });
+ const manifest = {
+ requests: messages,
+ responses: messages,
+ events: [
+ ...[0, 1, 2].map((i) => event("request-sent", i, "client", i)),
+ ...[1, 2, 0].map((i) => event("request-received", i, "origin", 10)),
+ ...[0, 2, 1].map((i) => event("response-sent", i, "origin", 20)),
+ ...[2, 0, 1].map((i) => event("response-received", i, "client", 30)),
+ ...["client", "origin"].map((connection) => ({
+ event: "handshake",
+ connection,
+ alpn: "h3",
+ quic_version: 1,
+ })),
+ ],
+ };
+ const record = (name) => ({
+ reported: {
+ id: name,
+ request: { headers: [["x-lab-case", name]] },
+ client_conn: { id: "client-uuid" },
+ server_conn: { id: "origin-uuid" },
+ },
+ });
+ const normal = {
+ records: [...[1, 0, 2].map((i) => record(names[i])), record("curl")],
+ };
+ const orders = verifyConcurrency(normal, manifest);
+ assert.deepEqual(orders.dispatched_at_origin, [
+ "parallel-0",
+ "parallel-2",
+ "parallel-1",
+ ]);
+ manifest.events = manifest.events.filter(
+ (item) => item.event !== "response-sent",
+ );
+ manifest.events.push(
+ ...[2, 1, 0].map((i) => event("response-sent", i, "origin", 20)),
+ );
+ assert.throws(() => verifyConcurrency(normal, manifest), {
+ code: "ERR_ASSERTION",
+ });
+});
+
+test("cleanup verifies and retires a separately owned producer group", async () => {
+ const root = await mkdtemp(join(tmpdir(), "rea-http3-group-"));
+ const runId = randomUUID();
+ const child = spawn("/usr/bin/sleep", ["60"], {
+ detached: true,
+ env: { ...process.env, REA_PROCESS_RUN_ID: runId },
+ stdio: "ignore",
+ });
+ let primaryError;
+ let cleaned = false;
+ try {
+ await once(child, "spawn");
+ await writeFile(
+ join(root, "process-groups.jsonl"),
+ JSON.stringify({
+ runId,
+ leaderPid: child.pid,
+ processGroupId: child.pid,
+ }) + "\n",
+ );
+ const result = await finishProducerGroups(root, runId);
+ assert.deepEqual(result.failures, []);
+ assert.equal(result.observation[0][1].state, "empty");
+ } catch (error) {
+ primaryError = error;
+ } finally {
+ const failures = await attemptAll([
+ [
+ "process",
+ async () => {
+ const result = await cleanupOwnedProcessGroup({
+ runId,
+ leaderPid: child.pid,
+ processGroupId: child.pid,
+ });
+ assert.equal(
+ result.cleaned,
+ true,
+ `Retained ownership evidence at ${root}: ${JSON.stringify(result)}`,
+ );
+ cleaned = true;
+ },
+ ],
+ [
+ "root",
+ async () => {
+ if (cleaned) await rm(root, { recursive: true, force: true });
+ },
+ ],
+ ]);
+ if (failures.length)
+ primaryError = new AggregateError(
+ [primaryError, ...failures].filter(Boolean),
+ "Test cleanup failed",
+ { cause: primaryError },
+ );
+ }
+ if (primaryError) throw primaryError;
+});
diff --git a/scripts/rea-android-workspace-guardian.mjs b/scripts/rea-android-workspace-guardian.mjs
new file mode 100644
index 000000000..93ba42d2d
--- /dev/null
+++ b/scripts/rea-android-workspace-guardian.mjs
@@ -0,0 +1,46 @@
+#!/usr/bin/env node
+
+import { setTimeout as delay } from "node:timers/promises";
+import { WindowsPrivateRuntime } from "../dist/windows/WindowsPrivateRuntime.js";
+
+if (process.platform !== "win32" || process.argv.length !== 3)
+ throw new Error(
+ "Android workspace guardian requires Windows and one parent directory",
+ );
+
+// This process owns the native directory handle. Detached process creation
+// keeps it alive when the REA owner is terminated without running JS cleanup.
+const runtime = WindowsPrivateRuntime.create(process.argv[2], "rea-android-");
+const ready = await new Promise((resolve) => {
+ process.stdout.once("error", () => resolve(false));
+ process.stdout.write(
+ JSON.stringify({
+ path: runtime.observation.path,
+ volumeSerial: runtime.observation.volumeSerial,
+ fileId: runtime.observation.fileId,
+ privateDacl: runtime.observation.privateDacl,
+ }) + "\n",
+ (error) => resolve(error == null),
+ );
+});
+
+// The owner holds the write end until close/death. A failed readiness pipe
+// also grants cleanup authority, including owner death during startup.
+if (ready) {
+ for await (const _ of process.stdin) {
+ // No commands are accepted; only EOF grants cleanup authority.
+ }
+} else {
+ process.stdin.destroy();
+}
+
+// A failed native walk retains the directory identity lease for a later retry.
+// Keep this process alive so a transient sharing violation cannot orphan it.
+for (;;) {
+ try {
+ await runtime.close();
+ process.exit(0);
+ } catch {
+ await delay(1000);
+ }
+}
diff --git a/scripts/test-website.py b/scripts/test-website.py
index 5c1aef876..ba87c9f15 100644
--- a/scripts/test-website.py
+++ b/scripts/test-website.py
@@ -261,7 +261,9 @@ class WebsiteTranslationChecks(unittest.TestCase):
inventory = [item for item in languages(ROOT) if item["code"] in {"en", "zh-CN", "ar"}]
(root / "website/locales/languages.json").write_text(json.dumps(inventory))
for item in inventory:
- (root / item["readme"]).write_text("# REA\n")
+ destination = root / item["readme"]
+ destination.parent.mkdir(parents=True, exist_ok=True)
+ destination.write_text("# REA\n")
WEBSITE.sync_pages(root)
extract_catalogs(root)
yield root
diff --git a/scripts/verify-agent-experience.mjs b/scripts/verify-agent-experience.mjs
index 99ab060dd..71cd6383c 100644
--- a/scripts/verify-agent-experience.mjs
+++ b/scripts/verify-agent-experience.mjs
@@ -1,4 +1,5 @@
import { spawn } from "node:child_process";
+import { createHash } from "node:crypto";
import {
chmod,
copyFile,
@@ -259,7 +260,7 @@ try {
const execution = await runCodex(
answerInstructions.length === 0
? scenario.prompt
- : `${scenario.prompt}\n\nThis closed factual rubric needs complete producer Evidence in the transcript. Request complete detail on each initial application analysis when supported; summary or view records alone are not accepted by this rubric.\n\n${answerInstructions}`,
+ : `${scenario.prompt}\n\nThis closed factual rubric needs complete producer Evidence in the transcript. Request complete detail on each initial inspection or analysis when supported; summary or view records alone are not accepted by this rubric.\n\n${answerInstructions}`,
scenario.approvedTools ?? [],
);
const transcriptDirectory = process.env.REA_AGENT_EVAL_TRANSCRIPT_DIR;
@@ -440,6 +441,13 @@ async function createTargets(root, includeManaged) {
...fixtures,
largeJavascript,
managed: join(managedOutput, "AgentEval.dll"),
+ ...(includeManaged
+ ? {
+ managedSha256: createHash("sha256")
+ .update(await readFile(join(managedOutput, "AgentEval.dll")))
+ .digest("hex"),
+ }
+ : {}),
};
}
diff --git a/scripts/verify-evm-interface.mjs b/scripts/verify-evm-interface.mjs
index b0bfdafa4..4bacc4bdf 100644
--- a/scripts/verify-evm-interface.mjs
+++ b/scripts/verify-evm-interface.mjs
@@ -225,6 +225,26 @@ try {
for (const mode of ["cli", "mcp"]) {
const report = await inspect(mode, path, encoding);
assert.equal(report.bytecode.hex, hex);
+ assert.equal(report.bytecode.metadata.status, "decoded");
+ assert.deepEqual(report.bytecode.metadata.compiler_version, {
+ representation: "release-bytes",
+ value: "0.8.30",
+ });
+ assert.equal(
+ report.bytecode.metadata.bytecode_offset +
+ report.bytecode.metadata.cbor_bytes +
+ 2,
+ Buffer.from(hex, "hex").length,
+ );
+ assert.ok(
+ report.bytecode.metadata.entries.some(
+ ({ key, value }) =>
+ key.kind === "text" &&
+ key.value === "solc" &&
+ value.kind === "bytes" &&
+ value.value === "00081e",
+ ),
+ );
assert.deepEqual(
report.functions
.filter(({ dispatch }) => dispatch === "abi")
diff --git a/scripts/verify-flutter.mjs b/scripts/verify-flutter.mjs
index 061ad8b20..76b5a1fda 100644
--- a/scripts/verify-flutter.mjs
+++ b/scripts/verify-flutter.mjs
@@ -1,12 +1,12 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
-import { readdir, readFile } from "node:fs/promises";
-import { resolve } from "node:path";
+import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join, resolve } from "node:path";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { toolContract } from "../dist/contracts/toolContracts.js";
-import { PrivateRuntimeRoot } from "../dist/process/PrivateRuntimeRoot.js";
import { hashStableFile } from "../dist/filesystem/StableFileHash.js";
import { mcpTextValue } from "./lib/mcp-verifier-results.mjs";
@@ -27,17 +27,16 @@ const expectations =
options.expectations === null
? null
: JSON.parse(await readFile(options.expectations, "utf8"));
-const root = await PrivateRuntimeRoot.create({
- prefix: "rea-flutter-verifier-",
-});
+// This is a shared test parent, not a leased provider-owned Windows runtime.
+const root = await mkdtemp(join(tmpdir(), "rea-flutter-verifier-"));
const entrypoint = resolve("scripts/rea.mjs");
const environment = Object.fromEntries(
Object.entries(process.env).filter(([, value]) => typeof value === "string"),
);
Object.assign(environment, {
- TMPDIR: root.path,
- TMP: root.path,
- TEMP: root.path,
+ TMPDIR: root,
+ TMP: root,
+ TEMP: root,
});
const client = new Client({ name: "flutter-verifier", version: "1" });
const transport = new StdioClientTransport({
@@ -190,15 +189,18 @@ try {
} finally {
// Server shutdown retries retained provider cleanup and must settle before success.
try {
- await client.close();
- await transport.close();
+ try {
+ await client.close();
+ } finally {
+ await transport.close();
+ }
assert.deepEqual(
- await readdir(root.path),
+ await readdir(root),
[],
- "Provider snapshots must be released before verification succeeds",
+ `Provider snapshots remain under ${root}`,
);
} finally {
- await root.close();
+ await rm(root, { recursive: true, force: true });
}
}
process.stdout.write(
diff --git a/scripts/verify-http3-network-captures.mjs b/scripts/verify-http3-network-captures.mjs
new file mode 100644
index 000000000..2b3493821
--- /dev/null
+++ b/scripts/verify-http3-network-captures.mjs
@@ -0,0 +1,408 @@
+#!/usr/bin/env node
+import assert from "node:assert/strict";
+import { createHash } from "node:crypto";
+import { readFile, writeFile } from "node:fs/promises";
+import { isAbsolute, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import { Client } from "@modelcontextprotocol/client";
+import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
+import { Ajv2020 } from "ajv/dist/2020.js";
+import { runOwnedCommand } from "../dist/process/OwnedCommand.js";
+import { toolContract } from "../dist/contracts/toolContracts.js";
+import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
+import {
+ bytes,
+ byCase,
+ verifyNormalRecords,
+ requireHttp3,
+ verifyConcurrency,
+} from "./lib/http3-capture-assertions.mjs";
+import {
+ attemptAll,
+ describeError,
+ prepareAttempt,
+ publishAttempt,
+ finishProducerGroups,
+} from "./lib/http3-verifier-lifecycle.mjs";
+import { mcpTextValue } from "./lib/mcp-verifier-results.mjs";
+
+const root = process.argv[2];
+const inspectExisting = process.argv[3] === "--inspect-existing";
+const mitmdump = process.env.REA_MITMDUMP_COMMAND;
+const python = process.env.REA_HTTP3_PYTHON_COMMAND;
+assert.equal(
+ process.platform,
+ "linux",
+ "HTTP/3 native verification requires Linux",
+);
+assert.ok(
+ root && isAbsolute(root),
+ "Select an absolute, new evidence directory",
+);
+assert.ok(
+ mitmdump && isAbsolute(mitmdump),
+ "Select caller-supplied mitmdump 12.2.3",
+);
+if (!inspectExisting)
+ assert.ok(
+ python && isAbsolute(python),
+ "Select a Python environment with aioquic 1.2.0",
+ );
+const run = createVerifierRun();
+const cwd = fileURLToPath(new URL("..", import.meta.url));
+const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url));
+const environment = Object.fromEntries(
+ Object.entries(process.env).filter(([, value]) => typeof value === "string"),
+);
+const client = new Client({
+ name: "real-http3-capture-verifier",
+ version: "1",
+});
+const transport = new StdioClientTransport({
+ command: process.execPath,
+ args: [entrypoint, "mcp"],
+ env: environment,
+ stderr: "pipe",
+});
+const contract = toolContract("inspect_web_network_capture");
+const maxMcpStderrBytes = 64 * 1024;
+let mcpStderr = Buffer.alloc(0);
+transport.stderr?.on("data", (chunk) => {
+ mcpStderr = Buffer.concat([mcpStderr, chunk]);
+ if (mcpStderr.length > maxMcpStderrBytes)
+ mcpStderr = mcpStderr.subarray(-maxMcpStderrBytes);
+});
+const checks = [];
+const retainedEvidence = [];
+const originals = new Map();
+let directory;
+let captureRoot = join(root, "captures");
+let primaryError;
+let verifier;
+let producerAudit;
+let cleanupFailures = [];
+let validateOutput;
+
+const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
+const readJson = async (path) => JSON.parse(await readFile(path, "utf8"));
+const readLines = async (path) =>
+ (await readFile(path, "utf8"))
+ .trim()
+ .split("\n")
+ .filter(Boolean)
+ .map(JSON.parse);
+
+async function cli(input, name) {
+ const command = await runOwnedCommand(
+ {
+ command: process.execPath,
+ arguments: [
+ entrypoint,
+ "inspect-web-network-capture",
+ input.capture_path,
+ input.format,
+ "--json",
+ ],
+ cwd,
+ hostEnvironment: environment,
+ runId: run.run_id,
+ },
+ {
+ timeoutMs: 40_000,
+ diagnosticBytes: 8 * 1024 * 1024,
+ },
+ );
+ assert.notEqual(command.diagnosticTruncated, true);
+ await writeFile(
+ join(directory, `${name}.cli.stdout.json`),
+ command.stdout.text,
+ );
+ await writeFile(
+ join(directory, `${name}.cli.stderr.log`),
+ command.stderr.text,
+ );
+ const value = JSON.parse(command.stdout.text);
+ return contract.outputSchema.parse(value);
+}
+
+async function mcp(input, name) {
+ const reply = await client.callTool({
+ name: contract.name,
+ arguments: input,
+ });
+ await writeFile(
+ join(directory, `${name}.mcp.reply.json`),
+ JSON.stringify(reply, null, 2),
+ );
+ const value = JSON.parse(mcpTextValue(reply));
+ assert.notEqual(reply.isError, true, mcpTextValue(reply));
+ assert.ok(
+ validateOutput(reply.structuredContent),
+ JSON.stringify(validateOutput.errors),
+ );
+ const evidence = contract.outputSchema.parse(value);
+ assert.deepEqual(reply.structuredContent, value);
+ retainedEvidence.push(evidence);
+ return evidence;
+}
+
+async function inspect(name) {
+ const input = {
+ capture_path: join(captureRoot, `${name}.mitm`),
+ format: "mitmproxy",
+ };
+ const cliResult = await cli(input, name);
+ const mcpResult = await mcp(input, name);
+ assert.deepEqual(cliResult.normalized_result, mcpResult.normalized_result);
+ for (const evidence of [cliResult, mcpResult]) {
+ assert.equal(evidence.authority, "historical-reference");
+ assert.equal(evidence.confidence, "observed");
+ assert.equal(evidence.provider.version, "12.2.3");
+ assert.equal(evidence.normalized_result.runtime_attribution, "unknown");
+ assert.deepEqual(evidence.provider, evidence.normalized_result.decoder);
+ assert.equal(
+ evidence.subject.digest.sha256,
+ digest(await readFile(input.capture_path)),
+ );
+ }
+ checks.push({
+ case: name,
+ outcome: "passed",
+ records: mcpResult.normalized_result.records.length,
+ });
+ return mcpResult.normalized_result;
+}
+
+try {
+ ({ directory, captureRoot } = await prepareAttempt(
+ root,
+ inspectExisting,
+ run,
+ ));
+ if (!inspectExisting) {
+ const produced = await runOwnedCommand(
+ {
+ command: python,
+ arguments: [
+ fileURLToPath(new URL("./fixtures/http3-lab.py", import.meta.url)),
+ "--mitmdump",
+ mitmdump,
+ "--output",
+ captureRoot,
+ ],
+ cwd,
+ hostEnvironment: environment,
+ runId: run.run_id,
+ },
+ { timeoutMs: 120_000, diagnosticBytes: 1024 * 1024 },
+ );
+ await writeFile(
+ join(directory, "producer.stdout.log"),
+ produced.stdout.text,
+ );
+ await writeFile(
+ join(directory, "producer.stderr.log"),
+ produced.stderr.text,
+ );
+ }
+ const manifest = await readJson(join(captureRoot, "manifest.json"));
+ assert.equal(manifest.aioquic, "1.2.0");
+ assert.equal(manifest.profile, "http3-minimal-v1");
+ await client.connect(transport);
+ const advertised = (await client.listTools()).tools.find(
+ (tool) => tool.name === contract.name,
+ );
+ assert.ok(advertised?.outputSchema, "Missing named advertised schema");
+ const ajv = new Ajv2020({ strict: false, validateFormats: false });
+ assert.ok(ajv.validateSchema(advertised.inputSchema));
+ assert.ok(ajv.validateSchema(advertised.outputSchema));
+ validateOutput = ajv.compile(advertised.outputSchema);
+ const inputValidator = ajv.compile(advertised.inputSchema);
+ assert.ok(
+ inputValidator({
+ capture_path: join(captureRoot, "normal.mitm"),
+ format: "mitmproxy",
+ }),
+ );
+ assert.equal(
+ inputValidator({
+ capture_path: join(captureRoot, "normal.mitm"),
+ format: "mitmproxy",
+ stream_id: 0,
+ }),
+ false,
+ );
+ await writeFile(
+ join(directory, "advertised-tool.json"),
+ JSON.stringify(advertised, null, 2),
+ );
+
+ for (const name of ["normal", "fallback"])
+ originals.set(name, await readFile(join(captureRoot, `${name}.mitm`)));
+ const normal = await inspect("normal");
+ assert.equal(normal.total_records, 4);
+ const finalProducer = (
+ await readLines(join(captureRoot, "normal.producer.jsonl"))
+ ).filter((event) => ["response", "error"].includes(event.event));
+ verifyNormalRecords(normal, manifest, finalProducer);
+ const orders = verifyConcurrency(normal, manifest);
+ assert.equal(
+ (await readJson(join(captureRoot, "curl.json"))).http_version,
+ "3",
+ );
+ assert.deepEqual(
+ await readFile(join(captureRoot, "curl.body")),
+ bytes(byCase(normal, "curl"), "/response/content"),
+ );
+ checks.push({
+ case: "concurrency-bytes",
+ outcome: "passed",
+ parallel_streams: 3,
+ orders,
+ });
+ const fallback = await inspect("fallback");
+ assert.equal(fallback.total_records, 1);
+ const record = byCase(fallback, "fallback");
+ assert.throws(
+ () => requireHttp3(record),
+ { code: "ERR_ASSERTION" },
+ "The same positive HTTP/3 check must reject the fallback record",
+ );
+ assert.equal(record.reported.client_conn.transport_protocol, "tcp");
+ assert.equal(record.reported.server_conn.transport_protocol, "tcp");
+ assert.equal(record.reported.request.http_version, "HTTP/2.0");
+ assert.equal(record.reported.response.http_version, "HTTP/1.1");
+ assert.equal(record.reported.server_conn.alpn, "http/1.1");
+ assert.equal(
+ (await readJson(join(captureRoot, "fallback.json"))).http_version,
+ "2",
+ );
+ checks.push({
+ case: "fallback-detected",
+ outcome: "passed",
+ client_http: "2",
+ origin_http: "1.1",
+ });
+
+ const bundleReply = await client.callTool({
+ name: "get_evidence_bundle",
+ arguments: {},
+ });
+ assert.notEqual(bundleReply.isError, true, mcpTextValue(bundleReply));
+ const bundle = JSON.parse(mcpTextValue(bundleReply)).result;
+ const records = bundle.records;
+ assert.ok(Array.isArray(records), "Evidence bundle omitted its records");
+ for (const evidence of retainedEvidence) {
+ const retained = records.find(
+ (record) => record.evidence_id === evidence.evidence_id,
+ );
+ assert.deepEqual(
+ retained,
+ evidence,
+ `MCP must retain the complete Evidence for ${evidence.evidence_id}`,
+ );
+ }
+ await writeFile(
+ join(directory, "session-bundle.reply.json"),
+ JSON.stringify(bundleReply, null, 2),
+ );
+ checks.push({
+ case: "mcp-session-retention",
+ outcome: "passed",
+ records: retainedEvidence.length,
+ });
+} catch (error) {
+ primaryError = error;
+} finally {
+ cleanupFailures = await attemptAll([
+ ["client-close", () => client.close()],
+ ["transport-close", () => transport.close()],
+ [
+ "producer-groups",
+ async () => {
+ if (!inspectExisting) {
+ producerAudit = await finishProducerGroups(captureRoot, run.run_id);
+ assert.deepEqual(producerAudit.failures ?? [], []);
+ for (const [, observation] of producerAudit.observation)
+ assert.equal(
+ observation.state,
+ "empty",
+ JSON.stringify(observation),
+ );
+ }
+ },
+ ],
+ ...[...originals].map(([name, original]) => [
+ `input-immutability-${name}`,
+ async () => {
+ assert.deepEqual(
+ await readFile(join(captureRoot, `${name}.mitm`)),
+ original,
+ );
+ },
+ ]),
+ [
+ "mcp-stderr-diagnostics",
+ async () => {
+ if (directory)
+ await writeFile(join(directory, "mcp.stderr.log"), mcpStderr);
+ },
+ ],
+ [
+ "failed-command-diagnostics",
+ async () => {
+ if (directory && primaryError?.snapshot) {
+ await writeFile(
+ join(directory, "failed-command.stdout.log"),
+ primaryError.snapshot.stdout.text,
+ );
+ await writeFile(
+ join(directory, "failed-command.stderr.log"),
+ primaryError.snapshot.stderr.text,
+ );
+ }
+ },
+ ],
+ [
+ "final-lineage",
+ async () => {
+ verifier = await completeVerifierRun(run);
+ assert.equal(verifier.process_lineage.status, "verified");
+ assert.deepEqual(verifier.process_lineage.descendants, []);
+ },
+ ],
+ ]);
+}
+const report = {
+ run_id: run.run_id,
+ status: primaryError || cleanupFailures.length ? "failed" : "passed",
+ finished_at: new Date().toISOString(),
+ ...(primaryError
+ ? {
+ error: describeError(primaryError),
+ mcp_stderr_log: "mcp.stderr.log",
+ }
+ : {}),
+ cleanup_failures: cleanupFailures,
+ checks,
+ verifier,
+ producer_audit: producerAudit,
+ scope:
+ "Real Linux reverse HTTP/3, mitmproxy 12.2.3, aioquic 1.2.0 and guest cURL",
+ limitations: [
+ "No local/WireGuard, browser, QUIC v2, migration, 0-RTT, mTLS or replay claim.",
+ "No HAR, interruption, certificate-failure or body-completeness coverage in this profile.",
+ ],
+};
+if (directory) {
+ try {
+ await publishAttempt(root, directory, report);
+ } catch (error) {
+ console.error("Could not publish this attempt's result:", error);
+ primaryError ??= error;
+ }
+}
+if (primaryError) throw primaryError;
+if (cleanupFailures.length)
+ throw new AggregateError(cleanupFailures, "HTTP/3 verifier cleanup failed");
+console.log(JSON.stringify(report, null, 2));
diff --git a/scripts/verify-package-environment.mjs b/scripts/verify-package-environment.mjs
index cd84655b2..e57f7784b 100644
--- a/scripts/verify-package-environment.mjs
+++ b/scripts/verify-package-environment.mjs
@@ -76,6 +76,7 @@ export async function verifyPackageEnvironment({
HOME: home,
USERPROFILE: home,
APPDATA: join(home, "AppData", "Roaming"),
+ LOCALAPPDATA: join(home, "AppData", "Local"),
CLAUDE_CONFIG_DIR: home,
CODEX_HOME: codexDir,
COPILOT_HOME: join(home, ".copilot"),
diff --git a/scripts/verify-package-update.mjs b/scripts/verify-package-update.mjs
index 5998ab77f..8d8582b44 100644
--- a/scripts/verify-package-update.mjs
+++ b/scripts/verify-package-update.mjs
@@ -98,6 +98,7 @@ export async function verifyPackageUpdate({
HOME: home,
USERPROFILE: home,
APPDATA: join(home, "AppData", "Roaming"),
+ LOCALAPPDATA: join(home, "AppData", "Local"),
CLAUDE_CONFIG_DIR: home,
CODEX_HOME: join(home, ".codex"),
COPILOT_HOME: join(home, ".copilot"),
diff --git a/scripts/verify-published-package.mjs b/scripts/verify-published-package.mjs
index 8c0281d56..8a5eb48d8 100644
--- a/scripts/verify-published-package.mjs
+++ b/scripts/verify-published-package.mjs
@@ -118,6 +118,7 @@ async function verifyPublishedUpdate({ version: targetVersion, canaryRoot }) {
HOME: home,
USERPROFILE: home,
APPDATA: join(home, "AppData", "Roaming"),
+ LOCALAPPDATA: join(home, "AppData", "Local"),
CLAUDE_CONFIG_DIR: home,
CODEX_HOME: join(home, ".codex"),
COPILOT_HOME: join(home, ".copilot"),
diff --git a/scripts/verify-real-browser.mjs b/scripts/verify-real-browser.mjs
index 48c9bec08..158877bca 100644
--- a/scripts/verify-real-browser.mjs
+++ b/scripts/verify-real-browser.mjs
@@ -9,7 +9,10 @@ import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { CdpBrowserProvider } from "../dist/browser/CdpBrowserProvider.js";
-import { waitForBrowserDevtoolsPort } from "../dist/browser/BrowserProcessStartup.js";
+import {
+ BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS,
+ waitForBrowserDevtoolsPort,
+} from "../dist/browser/BrowserProcessStartup.js";
import { createBrowserScenarioProvider } from "../dist/composition/browserScenario.js";
import {
inspectWebPageInputSchema,
@@ -42,12 +45,12 @@ import { verifyBrowserNetworkEvidence } from "./lib/browser-network-e2e.mjs";
import { verifyBrowserScriptExport } from "./lib/browser-script-export-e2e.mjs";
import { verifyBrowserModules } from "./lib/browser-module-e2e.mjs";
import { verifyBrowserDomDestinations } from "./lib/browser-dom-destinations-e2e.mjs";
+import { verifyBrowserSocketTransport } from "./lib/browser-socket-e2e.mjs";
import { verifyBrowserCaptureMetadataBudget } from "./lib/browser-capture-metadata-budget-e2e.mjs";
import { artifactCliEvidence, artifactMcpResult } from "./lib/artifact-e2e.mjs";
import { verifyScenarioEnvironment } from "./lib/browser-scenario-environment-e2e.mjs";
import { verifyScenarioStorage } from "./lib/browser-scenario-storage-e2e.mjs";
-const REAL_BROWSER_STARTUP_TIMEOUT_MS = 60_000;
const SCENARIO_SECRET_VALUE = "rea-browser-verifier-secret";
const SCENARIO_URL_SECRET_VALUE = "rea-browser-url-verifier-secret";
process.env.REA_BROWSER_VERIFIER_SECRET = SCENARIO_SECRET_VALUE;
@@ -92,7 +95,7 @@ try {
executable,
activePortPath: join(profile, "DevToolsActivePort"),
stderr: () => stderr,
- timeoutMs: REAL_BROWSER_STARTUP_TIMEOUT_MS,
+ timeoutMs: BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS,
});
const endpoint = `http://127.0.0.1:${String(port)}`;
const provider = new CdpBrowserProvider();
@@ -232,6 +235,13 @@ try {
pageProxy = await startPageCdpProxy(endpoint);
await verifyPageScopedTransport(provider, pageProxy, site.origin);
+ const browserSocketTransport = await verifyBrowserSocketTransport({
+ provider,
+ profile,
+ endpoint,
+ target,
+ origin: site.origin,
+ });
const attachedScenarioInput = browserScenario(
{
mode: "connect",
@@ -375,6 +385,7 @@ try {
bundle_cli_and_stdio_mcp: true,
sessionEvents: session.value.timeline.length,
pageScopedTransport: true,
+ browserSocketTransport,
screenshotBytes: screenshot.value.artifact.bytes,
largeScreenshot,
popupEvents,
diff --git a/scripts/verify-real-ghidra-mips.mjs b/scripts/verify-real-ghidra-mips.mjs
index 1c04ea978..12c00366d 100644
--- a/scripts/verify-real-ghidra-mips.mjs
+++ b/scripts/verify-real-ghidra-mips.mjs
@@ -12,6 +12,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import {
inspectMipsReadelf,
+ inspectLegacyMipsReadelf,
assertMipsLoadedImage,
assertMipsProbe,
assertMipsGlobal,
@@ -122,6 +123,157 @@ async function verifySoftFloat(call, symbols, byteOrder) {
const reports = [];
let primaryFailure;
try {
+ if (!psp) {
+ // Recordless legacy admission: ABI field 0, no ABI flags record, pinned
+ // 32-bit language. Independent GNU readelf checks the header interpretation.
+ for (const byteOrder of ["little", "big"]) {
+ const fixture = await buildMipsFixture(workspace, byteOrder, "hard", {
+ legacy: true,
+ });
+ const legacy = await inspectLegacyMipsReadelf(fixture.path, byteOrder);
+ const resolved = await parseBinaryTarget(fixture.path);
+ if (!resolved.ok) throw resolved.error;
+ assert.equal(resolved.value.mips.flags, 0x50000001);
+ assert.equal(resolved.value.mips.abiFlags, null);
+ assert.deepEqual(
+ resolved.value.mips.limitations ?? [],
+ legacy.limitations,
+ "REA legacy ABI-record absence interpretation disagrees with GNU readelf",
+ );
+ assert.equal(resolved.value.sha256, fixture.sha256);
+ const snapshotPath = join(workspace, `${byteOrder}-legacy.snapshot.json`);
+ const transport = new StdioClientTransport({
+ command: process.execPath,
+ args: [entrypoint, "mcp"],
+ env,
+ stderr: "pipe",
+ });
+ const client = new Client({
+ name: "rea-real-mips-legacy-proof",
+ version: "1",
+ });
+ let stderr = "";
+ transport.stderr?.on("data", (chunk) => {
+ stderr = (stderr + chunk.toString()).slice(-65536);
+ });
+ const call = async (name, args = {}) => {
+ const reply = await client.callTool(
+ { name, arguments: args },
+ undefined,
+ { timeout: 180000 },
+ );
+ return requireMcpOperationResult(reply, name);
+ };
+ try {
+ await client.connect(transport);
+ const selected = await call("open_binary", {
+ path: fixture.path,
+ provider_id: "ghidra",
+ });
+ assert.equal(selected.architecture, "mips");
+ assert.equal(selected.sha256, fixture.sha256);
+ const image = await call("inspect_native_load_image");
+ assertMipsLoadedImage(image.observations, byteOrder, fixture.sha256);
+ // The pinned import language is the checked one, not an auto-selection.
+ assert.equal(
+ image.observations.language_id,
+ `MIPS:${byteOrder === "little" ? "LE" : "BE"}:32:default`,
+ );
+ const procedures = await call("list_procedures");
+ const entry = procedures.find((p) => p.value === "rea_mips_entry");
+ assert.ok(entry, "Entry function missing from legacy inventory");
+ assert.equal(
+ BigInt(entry.address),
+ BigInt(legacy.symbols.rea_mips_entry),
+ );
+ const probe = await call("inspect_native_instruction", {
+ address: legacy.symbols.rea_mips_probe,
+ });
+ const probeBranch = await call("inspect_native_instruction", {
+ address: `0x${(BigInt(legacy.symbols.rea_mips_probe) + 4n).toString(16)}`,
+ });
+ assertMipsProbe(
+ probe,
+ probeBranch,
+ byteOrder,
+ legacy.symbols.rea_mips_probe,
+ );
+ const dossier = functionDossierSchema.parse(
+ await call("analyze_function", { procedure: entry.address }),
+ );
+ assert.ok(dossier.pseudocode.trim().length > 0);
+ const saved = await call("close_binary", {
+ snapshot_path: snapshotPath,
+ });
+ assert.ok(saved.evidence_records > 0);
+ const snapshot = parseAnalysisSnapshot(
+ JSON.parse(await readFile(snapshotPath, "utf8")),
+ );
+ assert.equal(snapshot.target.architecture, "mips");
+ const parameters = snapshot.binding.analysis_profile.parameters;
+ assert.equal(
+ parameters.language_id,
+ `MIPS:${byteOrder === "little" ? "LE" : "BE"}:32:default`,
+ );
+ assert.equal(parameters.language_selection, "provider-pinned-v1");
+ assert.equal(
+ parameters.mips_support_lane,
+ "elf32-exec-arch32-legacy-pinned-v1",
+ );
+ assert.equal(parameters.mips_abi_basis, "inferred-o32-assumption-v1");
+ assert.ok(
+ snapshot.evidence_bundle.records.some((record) =>
+ parseEvidence(record).limitations.some((item) =>
+ item.includes("analysis assumption"),
+ ),
+ ),
+ "Legacy o32 assumption limitation missing from retained Evidence",
+ );
+ assert.ok(
+ snapshot.evidence_bundle.records.some((record) =>
+ parseEvidence(record).limitations.some((item) =>
+ item.includes("Floating-point ABI semantics are unverified"),
+ ),
+ ),
+ "Legacy floating-point uncertainty missing from retained Evidence",
+ );
+ await call("open_binary", {
+ path: fixture.path,
+ provider_id: "ghidra",
+ snapshot_path: snapshotPath,
+ });
+ assert.deepEqual(await call("list_procedures"), procedures);
+ await call("close_binary");
+ } catch (cause) {
+ throw new Error(
+ `MIPS legacy ${byteOrder} verification failed; MCP stderr: ${stderr}`,
+ { cause },
+ );
+ } finally {
+ try {
+ await client.close();
+ } finally {
+ await transport.close();
+ }
+ }
+ reports.push({
+ ...fixture,
+ independent_reader: legacy,
+ variant: "recordless-legacy-pinned",
+ effective_language: `MIPS:${
+ byteOrder === "little" ? "LE" : "BE"
+ }:32:default`,
+ procedures: "verified-through-snapshot",
+ checks: [
+ "recordless header admitted with inferred o32 assumption",
+ "pinned 32-bit language checked after import",
+ "legacy limitations retained in Evidence",
+ "snapshot round-trip and profile parameters",
+ ],
+ status: "passed",
+ });
+ }
+ }
for (const [byteOrder, floatAbi] of psp
? [["little", "hard"]]
: [
diff --git a/scripts/verify-real-ghidra-raw.mjs b/scripts/verify-real-ghidra-raw.mjs
index befba80e0..4341b5cfd 100644
--- a/scripts/verify-real-ghidra-raw.mjs
+++ b/scripts/verify-real-ghidra-raw.mjs
@@ -114,6 +114,22 @@ try {
length: bytes.length,
});
assert.equal(memory.normalized_result.bytes_hex, bytes.toString("hex"));
+ const signature = await call("search_bytes", {
+ pattern: "1e ?? 2f e1",
+ limit: 1000,
+ });
+ assert.equal(signature.normalized_result.complete, true);
+ assert.equal(signature.normalized_result.next_cursor, null);
+ assert.equal(signature.normalized_result.searched.bytes, bytes.length);
+ assert.deepEqual(
+ signature.normalized_result.matches.map((match) => match.address),
+ [entry],
+ );
+ assert.equal(signature.normalized_result.matches[0].file_offset, 0);
+ assert.equal(
+ signature.normalized_result.matches[0].procedure.address,
+ entry,
+ );
const procedures = await call("list_procedures");
assert.ok(
procedures.normalized_result.some(
@@ -211,6 +227,69 @@ try {
return_42: true,
});
}
+ const repeatedPath = join(workspace, "signatures.bin");
+ const repeatedBytes = Buffer.from("b82a000000c3".repeat(1005), "hex");
+ const repeatedSha256 = createHash("sha256")
+ .update(repeatedBytes)
+ .digest("hex");
+ await writeFile(repeatedPath, repeatedBytes);
+ await call("open_binary", {
+ path: repeatedPath,
+ format: "raw-binary",
+ architecture: "x86_64",
+ entry_point: "0x1000",
+ });
+ // Import/analysis is complete before timing the first uncached search.
+ await call("read_bytes", { address: "0x1000", length: 6 }, repeatedSha256);
+ const started = performance.now();
+ const page = await call(
+ "search_bytes",
+ { pattern: "b8 ?? 00 00 00 c3", limit: 1000 },
+ repeatedSha256,
+ );
+ const searchElapsedMs = performance.now() - started;
+ assert.equal(page.normalized_result.matches.length, 1000);
+ assert.equal(page.normalized_result.complete, false);
+ const rest = await call(
+ "search_bytes",
+ {
+ pattern: "b8 ?? 00 00 00 c3",
+ limit: 1000,
+ cursor: page.normalized_result.next_cursor,
+ },
+ repeatedSha256,
+ );
+ assert.equal(rest.normalized_result.matches.length, 5);
+ assert.equal(
+ rest.normalized_result.matches[0].address,
+ `0x${(0x1000 + 6000).toString(16)}`,
+ );
+ assert.equal(rest.normalized_result.complete, true);
+ const masked = await call(
+ "search_bytes",
+ { pattern: "bf", mask: "f0", start: "0x1000", end: "0x1005" },
+ repeatedSha256,
+ );
+ assert.deepEqual(
+ masked.normalized_result.matches.map((match) => match.address),
+ ["0x1000"],
+ );
+ const overlapping = await call(
+ "search_bytes",
+ { pattern: "00 00", start: "0x1000", end: "0x1005" },
+ repeatedSha256,
+ );
+ assert.deepEqual(
+ overlapping.normalized_result.matches.map((match) => match.address),
+ ["0x1002", "0x1003"],
+ );
+ const wrongCursor = await client.callTool({
+ name: "search_bytes",
+ arguments: { pattern: "c3", cursor: page.normalized_result.next_cursor },
+ });
+ assert.equal(wrongCursor.isError, true);
+ await call("close_binary");
+ assert.deepEqual(await readFile(repeatedPath), repeatedBytes);
report = {
ok: true,
mocked: false,
@@ -231,6 +310,15 @@ try {
load_images_verified: true,
original_unchanged: true,
x86_targets: x86Targets,
+ byte_search: {
+ matches: 1005,
+ page_size: 1000,
+ first_search_elapsed_ms: searchElapsedMs,
+ timing_scope: "MCP request after import; first uncached search",
+ token_usage: "unmeasured",
+ process_family_cpu_time: "unmeasured",
+ peak_process_family_memory: "unmeasured",
+ },
};
} catch (error) {
if (stderr.trim()) process.stderr.write(stderr);
diff --git a/scripts/verify-solidity-metadata.mjs b/scripts/verify-solidity-metadata.mjs
new file mode 100644
index 000000000..f927477d8
--- /dev/null
+++ b/scripts/verify-solidity-metadata.mjs
@@ -0,0 +1,105 @@
+import assert from "node:assert/strict";
+import { readFile } from "node:fs/promises";
+import { isAbsolute } from "node:path";
+import { pathToFileURL } from "node:url";
+import { performance } from "node:perf_hooks";
+import { inspectSolidityMetadata } from "../dist/evm/SolidityMetadata.js";
+
+const compilerModule = process.env.REA_VERIFY_SOLC_MODULE;
+if (!isAbsolute(compilerModule ?? ""))
+ throw new Error(
+ "Select an absolute caller-installed solc 0.8.30 module with REA_VERIFY_SOLC_MODULE.",
+ );
+const solc = (await import(pathToFileURL(compilerModule).href)).default;
+assert.equal(solc.version(), "0.8.30+commit.73712a01.Emscripten.clang");
+const source = await readFile(
+ new URL("./fixtures/evm-interface.sol", import.meta.url),
+ "utf8",
+);
+const cases = [
+ { name: "unoptimized", optimizer: false, viaIR: false, metadata: {} },
+ { name: "viaIR", optimizer: true, viaIR: true, metadata: {} },
+ {
+ name: "without-content-hash",
+ optimizer: false,
+ viaIR: false,
+ metadata: { bytecodeHash: "none" },
+ },
+ {
+ name: "without-CBOR",
+ optimizer: false,
+ viaIR: false,
+ metadata: { appendCBOR: false },
+ },
+];
+const reports = [];
+for (const fixture of cases) {
+ const compiled = JSON.parse(
+ solc.compile(
+ JSON.stringify({
+ language: "Solidity",
+ sources: { "fixture.sol": { content: source } },
+ settings: {
+ optimizer: { enabled: fixture.optimizer },
+ viaIR: fixture.viaIR,
+ metadata: fixture.metadata,
+ outputSelection: { "*": { "*": ["evm.deployedBytecode.object"] } },
+ },
+ }),
+ ),
+ );
+ assert.ok(
+ !(compiled.errors ?? []).some((error) => error.severity === "error"),
+ JSON.stringify(compiled.errors),
+ );
+ const bytes = Buffer.from(
+ compiled.contracts["fixture.sol"].InterfaceFixture.evm.deployedBytecode
+ .object,
+ "hex",
+ );
+ const cpu = process.cpuUsage();
+ const started = performance.now();
+ const parsed = inspectSolidityMetadata(bytes);
+ const firstMs = performance.now() - started;
+ const firstCpu = process.cpuUsage(cpu);
+ if (fixture.metadata.appendCBOR === false)
+ assert.equal(parsed.status, "unavailable");
+ else {
+ assert.equal(parsed.status, "decoded");
+ assert.deepEqual(parsed.compiler_version, {
+ representation: "release-bytes",
+ value: "0.8.30",
+ });
+ assert.equal(parsed.bytecode_offset + parsed.cbor_bytes + 2, bytes.length);
+ if (fixture.metadata.bytecodeHash === "none")
+ assert.deepEqual(
+ parsed.entries.map(({ key }) => key),
+ [{ kind: "text", value: "solc" }],
+ );
+ }
+ const warmResults = new Array(100);
+ const warmStart = performance.now();
+ for (let index = 0; index < warmResults.length; index++)
+ warmResults[index] = inspectSolidityMetadata(bytes);
+ const warmMs = performance.now() - warmStart;
+ for (const result of warmResults) assert.deepEqual(result, parsed);
+ reports.push({
+ fixture: fixture.name,
+ bytes: bytes.length,
+ status: parsed.status,
+ first_decode_ms: firstMs,
+ first_decode_cpu_us: firstCpu,
+ warm_100_decode_ms: warmMs,
+ });
+}
+console.log(
+ JSON.stringify({
+ ok: true,
+ compiler_version: solc.version(),
+ contract_execution: "not-performed",
+ cases: reports,
+ process_peak_rss_kib: process.resourceUsage().maxRSS,
+ measurements:
+ "In-process decode timings; peak RSS includes the compiler. End-to-end Linux provider/CLI costs and agent tokens were not measured.",
+ }),
+);
diff --git a/scripts/verify-wasm-artifact.mjs b/scripts/verify-wasm-artifact.mjs
index 1f3bde2ca..305063ccc 100644
--- a/scripts/verify-wasm-artifact.mjs
+++ b/scripts/verify-wasm-artifact.mjs
@@ -310,7 +310,23 @@ try {
}),
);
assert.equal(error.category, "invalid_input");
- const failed = parseEvidence(error.details.partial_observation);
+ const partial = error.details.partial_observation;
+ assert.equal(partial.kind, "retained-evidence");
+ const failedBundleReply = await client.callTool({
+ name: "get_evidence_bundle",
+ arguments: {},
+ });
+ assert.notEqual(
+ failedBundleReply.isError,
+ true,
+ mcpTextValue(failedBundleReply),
+ );
+ const failedBundle = JSON.parse(mcpTextValue(failedBundleReply)).result;
+ const failed = parseEvidence(
+ failedBundle.records.find(
+ ({ evidence_id }) => evidence_id === partial.evidence_id,
+ ),
+ );
assert.equal(failed.subject.digest.sha256, sha256(invalidBytes));
assert.equal(
failed.normalized_result.validation,
diff --git a/scripts/verify-wasm-interface-comparison.mjs b/scripts/verify-wasm-interface-comparison.mjs
new file mode 100644
index 000000000..6b215eef3
--- /dev/null
+++ b/scripts/verify-wasm-interface-comparison.mjs
@@ -0,0 +1,208 @@
+import assert from "node:assert/strict";
+import { execFile } from "node:child_process";
+import { mkdtemp, readFile, writeFile, rm } from "node:fs/promises";
+import { join, resolve, isAbsolute } from "node:path";
+import { tmpdir } from "node:os";
+import { promisify } from "node:util";
+import { Client } from "@modelcontextprotocol/client";
+import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
+import { parseEvidence } from "../dist/domain/evidence.js";
+
+const execute = promisify(execFile);
+const bin = process.env.REA_WABT_BIN_DIRECTORY;
+if (!isAbsolute(bin ?? ""))
+ throw new Error("Supply absolute REA_WABT_BIN_DIRECTORY with WABT 1.0.42");
+const root = await mkdtemp(join(tmpdir(), "rea-wasm-interface-real-"));
+const cli = resolve("scripts/rea.mjs");
+const environment = { ...process.env };
+const command = async (file, args) =>
+ (
+ await execute(file, args, {
+ env: environment,
+ cwd: root,
+ timeout: 120000,
+ maxBuffer: 64 * 1024 * 1024,
+ })
+ ).stdout;
+const client = new Client({
+ name: "wasm-interface-real-verifier",
+ version: "1",
+});
+const transport = new StdioClientTransport({
+ command: process.execPath,
+ args: [cli, "mcp"],
+ env: environment,
+ stderr: "pipe",
+});
+const source = (parameter) =>
+ `(module (type (func (param ${parameter}) (result i64))) (import "env" "f" (func (type 0))) (table 1 3 funcref) (memory 1 2) (global (mut i64) (i64.const 0)) (tag (param i32)) (export "run\\22\\ef\\bb\\bf" (func 0)) (export "table" (table 0)) (export "memory" (memory 0)) (export "global" (global 0)) (export "tag" (tag 0)))`;
+const assemble = async (name, wat) => {
+ await writeFile(join(root, `${name}.wat`), wat);
+ const path = join(root, `${name}.wasm`);
+ await command(join(bin, "wat2wasm"), [
+ "--enable-all",
+ `${name}.wat`,
+ "-o",
+ path,
+ ]);
+ return path;
+};
+const inspect = async (path) => {
+ const result = await client.callTool({
+ name: "inspect_wasm_artifact",
+ arguments: { path },
+ });
+ assert.notEqual(result.isError, true, JSON.stringify(result));
+ return parseEvidence(result.structuredContent);
+};
+const compare = async (left, right, scope = "both") => {
+ const input = {
+ left: { kind: "retained-evidence", evidence_id: left.evidence_id },
+ right: { kind: "retained-evidence", evidence_id: right.evidence_id },
+ scope,
+ };
+ const reply = await client.callTool({
+ name: "compare_wasm_interfaces",
+ arguments: input,
+ });
+ assert.notEqual(reply.isError, true, JSON.stringify(reply));
+ const mcp = parseEvidence(reply.structuredContent);
+ const file = join(root, "comparison.json");
+ await writeFile(file, JSON.stringify({ left, right, scope }));
+ const local = parseEvidence(
+ JSON.parse(
+ await command(process.execPath, [
+ cli,
+ "compare-wasm-interfaces",
+ file,
+ "--json",
+ ]),
+ ),
+ );
+ assert.deepEqual(local, mcp);
+ return mcp.normalized_result;
+};
+try {
+ assert.equal(
+ (await command(join(bin, "wat2wasm"), ["--version"])).trim(),
+ "1.0.42",
+ );
+ await client.connect(transport);
+ const path = await assemble("baseline", source("i32"));
+ const original = await readFile(path);
+ assert.equal(original[8], 1);
+ assert.ok(original[9] < 128);
+ // Section lengths allow padded u32 LEB128 encodings. Different original
+ // byte identities must compare equal after validated type resolution.
+ const equivalentPath = join(root, "equivalent.wasm");
+ await writeFile(
+ equivalentPath,
+ Buffer.concat([
+ original.subarray(0, 9),
+ Buffer.from([original[9] | 128, 0]),
+ original.subarray(10),
+ ]),
+ );
+ const left = await inspect(path);
+ const equivalent = await inspect(equivalentPath);
+ assert.notEqual(left.subject.digest.sha256, equivalent.subject.digest.sha256);
+ const same = await compare(left, equivalent);
+ assert.deepEqual(same.summary, {
+ added: 0,
+ removed: 0,
+ changed: 0,
+ unchanged: 6,
+ unknown: 0,
+ });
+ const facts = left.normalized_result.interface.entries;
+ assert.deepEqual(facts.find((e) => e.name === "memory").signature, {
+ kind: "memory",
+ page_size: "65536",
+ address_type: "i32",
+ minimum: "1",
+ maximum: "2",
+ shared: false,
+ });
+ assert.deepEqual(facts.find((e) => e.name === "tag").signature, {
+ kind: "tag",
+ params: ["i32"],
+ results: [],
+ });
+ assert.deepEqual(facts.find((e) => e.name === 'run"\ufeff').signature, {
+ kind: "func",
+ params: ["i32"],
+ results: ["i64"],
+ });
+ const changed = await inspect(await assemble("changed", source("i64")));
+ assert.deepEqual((await compare(left, changed)).summary, {
+ added: 0,
+ removed: 0,
+ changed: 2,
+ unchanged: 4,
+ unknown: 0,
+ });
+ const smallPage = await inspect(
+ await assemble(
+ "small-page",
+ '(module (memory 1 2 (pagesize 1)) (export "memory" (memory 0)))',
+ ),
+ );
+ const defaultPage = await inspect(
+ await assemble(
+ "default-page",
+ '(module (memory 1 2) (export "memory" (memory 0)))',
+ ),
+ );
+ assert.equal((await compare(defaultPage, smallPage)).summary.changed, 1);
+ const empty = await inspect(await assemble("empty", "(module)"));
+ assert.equal((await compare(left, empty)).summary.removed, 6);
+ assert.equal((await compare(empty, left, "exports")).summary.added, 5);
+ const duplicate = await inspect(
+ await assemble(
+ "duplicate",
+ '(module (import "e" "same" (func (param i32))) (import "e" "same" (func (param i64))))',
+ ),
+ );
+ const duplicates = await compare(duplicate, duplicate, "imports");
+ assert.equal(duplicates.summary.unknown, 1);
+ assert.equal(duplicates.changes[0].left.length, 2);
+ const unsupported = await inspect(
+ await assemble(
+ "exnref",
+ '(module (import "e" "abstract-ref" (global exnref)))',
+ ),
+ );
+ assert.equal((await compare(unsupported, unsupported)).summary.unknown, 1);
+ console.log(
+ JSON.stringify(
+ {
+ verified: true,
+ toolchain: "WABT 1.0.42",
+ transport: "compiled CLI + stdio MCP",
+ interface_fact_bytes: Buffer.byteLength(
+ JSON.stringify(left.normalized_result.interface),
+ ),
+ comparison_result_bytes: Buffer.byteLength(JSON.stringify(same)),
+ original_sha256: left.subject.digest.sha256,
+ equivalent_sha256: equivalent.subject.digest.sha256,
+ cases: [
+ "equivalent padded LEB128",
+ "function signature change",
+ "all admitted kinds",
+ "custom memory page-size change",
+ "escaped/BOM name",
+ "empty interface additions/removals",
+ "scope",
+ "duplicate imports",
+ "unsupported exception reference",
+ ],
+ runtime_execution: "not-performed",
+ },
+ null,
+ 2,
+ ),
+ );
+} finally {
+ await client.close();
+ await rm(root, { recursive: true, force: true });
+}
diff --git a/scripts/verify-windows-native.mjs b/scripts/verify-windows-native.mjs
index 8623081f9..0a42cb604 100644
--- a/scripts/verify-windows-native.mjs
+++ b/scripts/verify-windows-native.mjs
@@ -472,10 +472,9 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
signal: AbortSignal.timeout(15_000),
});
assert.equal(String(ready).trim(), "ready");
- await assert.rejects(
- retryRuntime.close(),
- /owned cleanup object|Delete owned object/u,
- );
+ // The reader denies delete sharing. Cleanup must retain ownership when
+ // its handle-relative open receives ERROR_SHARING_VIOLATION (32).
+ await assert.rejects(retryRuntime.close(), { win32Code: 32 });
assert.equal(
windowsPrivateRuntime(retryRuntime.observation.path),
retryRuntime,
diff --git a/scripts/verify-windows-package.mjs b/scripts/verify-windows-package.mjs
index 67dee06c2..425b5cbd3 100644
--- a/scripts/verify-windows-package.mjs
+++ b/scripts/verify-windows-package.mjs
@@ -52,8 +52,10 @@ try {
"bridge/ghidra/ReaGhidraBridge.java",
"native/windows/build/manifest.json",
"native/windows/build/rea-windows-x64.node",
+ "dist/android/JadxWorkspaceRoot.js",
"dist/main.js",
"dist/cli.js",
+ "scripts/rea-android-workspace-guardian.mjs",
"scripts/rea.mjs",
])
if (!packagedPaths.has(required))
@@ -108,6 +110,7 @@ try {
HOME: home,
USERPROFILE: home,
APPDATA: join(home, "AppData", "Roaming"),
+ LOCALAPPDATA: join(home, "AppData", "Local"),
CLAUDE_CONFIG_DIR: home,
CODEX_HOME: join(home, ".codex"),
XDG_CONFIG_HOME: join(home, ".config"),
diff --git a/scripts/verify/android/analysis.mjs b/scripts/verify/android/analysis.mjs
index 43c51d82b..30024b557 100644
--- a/scripts/verify/android/analysis.mjs
+++ b/scripts/verify/android/analysis.mjs
@@ -173,6 +173,50 @@ try {
assert.deepEqual(structured.normalized_result, evidence.normalized_result);
console.log(`PASS MCP/CLI parity ${task.name}`);
}
+ const overloadedInput = {
+ path,
+ class_name: "androidx.activity.ComponentActivity",
+ method_name: "setContentView",
+ overload_index: 0,
+ };
+ const overloadedCli = await execute(
+ process.execPath,
+ [
+ entrypoint,
+ "trace-android-references",
+ path,
+ overloadedInput.class_name,
+ "--method-name",
+ overloadedInput.method_name,
+ "--overload-index",
+ "0",
+ "--format",
+ "json",
+ ],
+ {
+ cwd: repository,
+ env: environment,
+ timeout: 150_000,
+ maxBuffer: 8 * 1024 * 1024,
+ },
+ );
+ const overloadedEvidence = parseEvidence(JSON.parse(overloadedCli.stdout));
+ assert.equal(overloadedEvidence.normalized_result.overload_index, 0);
+ assert.equal(
+ overloadedEvidence.normalized_result.reported_signature,
+ "setContentView(int): void",
+ );
+ assert.equal(overloadedEvidence.normalized_result.total, 1);
+ const overloadedMcp = await client.callTool(
+ { name: "trace_android_references", arguments: overloadedInput },
+ { timeout: 150_000 },
+ );
+ assert.notEqual(overloadedMcp.isError, true, JSON.stringify(overloadedMcp));
+ assert.deepEqual(
+ parseEvidence(overloadedMcp.structuredContent).normalized_result,
+ overloadedEvidence.normalized_result,
+ );
+ console.log("PASS real overloaded method references through CLI/MCP");
} catch (cause) {
process.stderr.write(stderr);
throw cause;
@@ -205,7 +249,7 @@ assert.equal(method.body_status, "available");
assert.equal(method.source.status, "complete");
assert.ok(method.source.text.includes("getIntent()"));
assert.ok(method.source.text.includes("setListAdapter"));
-assert.equal(method.method.dex_descriptor, null);
+assert.equal(method.method.dex_descriptor, "(Landroid/os/Bundle;)V");
const references = cliResults.get("trace_android_references").normalized_result;
assert.ok(references.total > 0);
assert.ok(
@@ -264,3 +308,8 @@ if (process.env.REA_ANDROID_REPORT !== undefined)
JSON.stringify(report, null, 2) + "\n",
);
console.log(JSON.stringify(report, null, 2));
+assert.notEqual(
+ windowsLifecycle?.cli_abrupt_owner_exit_workspace_cleanup,
+ false,
+ "Android workspace remained after abrupt owner exit",
+);
diff --git a/scripts/verify/android/direct-invokes.mjs b/scripts/verify/android/direct-invokes.mjs
new file mode 100644
index 000000000..7cb9972db
--- /dev/null
+++ b/scripts/verify/android/direct-invokes.mjs
@@ -0,0 +1,112 @@
+import assert from "node:assert/strict";
+import { Client } from "@modelcontextprotocol/client";
+import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
+import { parseEvidence } from "../../../dist/domain/evidence.js";
+import { hashAndroidFile } from "./digest.mjs";
+
+/** The public result keeps one using node and exposes its two raw DEX invokes. */
+export const verifyAndroidDirectInvokes = async ({
+ apk,
+ dexEntry,
+ repository,
+ entrypoint,
+ environment,
+ execute,
+}) => {
+ const digest = await hashAndroidFile(apk);
+ const check = (value) => {
+ const evidence = parseEvidence(value);
+ assert.equal(evidence.subject.digest.sha256, digest);
+ assert.equal(evidence.operation, "trace_android_references");
+ const result = evidence.normalized_result;
+ assert.equal(result.class_name, "fixture.Target");
+ assert.equal(result.method_name, "mark");
+ assert.equal(result.dex_descriptor, "()V");
+ assert.equal(result.reference_unit, "using_node");
+ assert.equal(result.total, 1);
+ assert.equal(result.references.length, 1);
+ const source = result.references[0];
+ assert.equal(source.reported_full_name, "fixture.Caller.twice");
+ assert.deepEqual(
+ source.direct_invokes.map((site) => site.dex_code_unit_offset),
+ [0, 3],
+ );
+ for (const site of source.direct_invokes) {
+ assert.equal(site.dex_entry, dexEntry);
+ assert.equal(site.source_raw_method_id, "fixture.Caller.twice()V");
+ assert.equal(site.opcode, "INVOKE_STATIC");
+ }
+ assert.equal(
+ source.direct_invokes[0].dex_method_index,
+ source.direct_invokes[1].dex_method_index,
+ );
+ return evidence;
+ };
+ const cliResponse = await execute(
+ process.execPath,
+ [
+ entrypoint,
+ "trace-android-references",
+ apk,
+ "fixture.Target",
+ "--method-name",
+ "mark",
+ "--format",
+ "json",
+ ],
+ {
+ cwd: repository,
+ env: environment,
+ timeout: 150_000,
+ maxBuffer: 8 * 1024 * 1024,
+ },
+ );
+ const cli = check(JSON.parse(cliResponse.stdout));
+ const transport = new StdioClientTransport({
+ command: process.execPath,
+ args: [entrypoint, "mcp"],
+ cwd: repository,
+ env: environment,
+ stderr: "pipe",
+ });
+ let stderr = "";
+ transport.stderr?.on("data", (chunk) => {
+ stderr += chunk.toString();
+ });
+ const client = new Client({
+ name: "rea-real-android-direct-invokes",
+ version: "1",
+ });
+ try {
+ await client.connect(transport, { timeout: 30_000 });
+ const response = await client.callTool(
+ {
+ name: "trace_android_references",
+ arguments: {
+ path: apk,
+ class_name: "fixture.Target",
+ method_name: "mark",
+ },
+ },
+ { timeout: 150_000 },
+ );
+ assert.notEqual(response.isError, true, JSON.stringify(response));
+ const mcp = check(response.structuredContent);
+ assert.deepEqual(mcp.normalized_result, cli.normalized_result);
+ assert.deepEqual(mcp.parameters, cli.parameters);
+ assert.deepEqual(mcp.provider, cli.provider);
+ } catch (cause) {
+ process.stderr.write(stderr);
+ throw cause;
+ } finally {
+ await client.close();
+ }
+ assert.equal(
+ await hashAndroidFile(apk),
+ digest,
+ "Static inspection changed fixture bytes",
+ );
+ console.log(
+ `PASS real CLI/MCP direct-invoke offsets from ${dexEntry}, parity and unchanged APK`,
+ );
+};
diff --git a/scripts/verify/android/metadata.mjs b/scripts/verify/android/metadata.mjs
index eebcaf812..a44da336c 100644
--- a/scripts/verify/android/metadata.mjs
+++ b/scripts/verify/android/metadata.mjs
@@ -3,6 +3,7 @@ import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { delimiter, join, resolve } from "node:path";
import { verifyAndroidInitializers } from "./initializers.mjs";
+import { verifyAndroidDirectInvokes } from "./direct-invokes.mjs";
/** Prove cold metadata stays unmaterialized and overload identities survive code generation. */
export const verifyAndroidMetadata = async ({
@@ -60,6 +61,7 @@ export const verifyAndroidMetadata = async ({
apk,
resolve(repository, "tests/fixtures/android/static-initializers"),
classes,
+ resolve(repository, "tests/fixtures/android/direct-invokes"),
],
{ timeout: 120_000, maxBuffer: 8 * 1024 * 1024 },
);
@@ -71,12 +73,19 @@ export const verifyAndroidMetadata = async ({
result.stdout,
/INITIALIZER_REGRESSION_PASS folded=2 bounded=true/u,
);
+ assert.match(
+ result.stdout,
+ /DIRECT_INVOKE_REGRESSION_PASS nodes=1 offsets=0,3 cross_dex=true/u,
+ );
console.log(
"PASS real cold metadata, full generic types and stable overloaded method selection",
);
console.log(
"PASS real folded and mixed static initializers, absent bodies and bounded smali",
);
+ console.log(
+ "PASS real raw DEX direct-invoke offsets and cross-DEX source identity",
+ );
await verifyAndroidInitializers({
apk: resolve(classes, "initializers.apk"),
repository,
@@ -84,6 +93,22 @@ export const verifyAndroidMetadata = async ({
environment,
execute,
});
+ await verifyAndroidDirectInvokes({
+ apk: resolve(classes, "direct-invokes.apk"),
+ dexEntry: "classes.dex",
+ repository,
+ entrypoint,
+ environment,
+ execute,
+ });
+ await verifyAndroidDirectInvokes({
+ apk: resolve(classes, "direct-invokes-multidex.apk"),
+ dexEntry: "classes2.dex",
+ repository,
+ entrypoint,
+ environment,
+ execute,
+ });
} finally {
await rm(classes, { recursive: true, force: true });
}
diff --git a/scripts/verify/android/windows-guardian-isolation.mjs b/scripts/verify/android/windows-guardian-isolation.mjs
new file mode 100644
index 000000000..1a6308970
--- /dev/null
+++ b/scripts/verify/android/windows-guardian-isolation.mjs
@@ -0,0 +1,105 @@
+import assert from "node:assert/strict";
+import { spawn } from "node:child_process";
+import { stat, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+import { createInterface } from "node:readline";
+import { setTimeout as delay } from "node:timers/promises";
+import { fileURLToPath } from "node:url";
+import { createJadxWorkspaceRoot } from "../../../dist/android/JadxWorkspaceRoot.js";
+
+const source = fileURLToPath(import.meta.url);
+const marker = "REA synthetic guardian isolation test\n";
+const exists = (path) =>
+ stat(path).then(
+ () => true,
+ () => false,
+ );
+
+if (process.argv[1] === source && process.argv[2] === "owner") {
+ const root = await createJadxWorkspaceRoot();
+ await writeFile(join(root.path, "synthetic.txt"), marker);
+ process.stdout.write(JSON.stringify({ path: root.path }) + "\n");
+ await new Promise(() => {});
+}
+
+const bounded = async (promise, milliseconds, label) => {
+ let timer;
+ try {
+ return await Promise.race([
+ promise,
+ new Promise((_, reject) => {
+ timer = setTimeout(() => reject(new Error(label)), milliseconds);
+ }),
+ ]);
+ } finally {
+ clearTimeout(timer);
+ }
+};
+
+const launch = async () => {
+ const child = spawn(process.execPath, [source, "owner"], {
+ stdio: ["ignore", "pipe", "pipe"],
+ windowsHide: true,
+ });
+ let stderr = "";
+ child.stderr.on("data", (chunk) => {
+ stderr = (stderr + chunk.toString()).slice(-8192);
+ });
+ const exit = new Promise((resolve, reject) => {
+ child.once("close", resolve);
+ child.once("error", reject);
+ });
+ const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
+ try {
+ const ready = await bounded(
+ lines[Symbol.asyncIterator]().next(),
+ 20000,
+ `Android guardian owner readiness timed out: ${stderr}`,
+ );
+ assert.equal(ready.done, false, stderr);
+ const { path } = JSON.parse(ready.value);
+ assert.equal(typeof path, "string");
+ assert.equal(await exists(join(path, "synthetic.txt")), true);
+ return { child, exit, path };
+ } catch (cause) {
+ child.kill();
+ await bounded(exit, 15000, "Android guardian owner did not exit");
+ throw cause;
+ }
+};
+
+const stop = async ({ child, exit, path }) => {
+ assert.equal(child.kill(), true);
+ await bounded(exit, 15000, "Android guardian owner did not exit");
+ for (let attempt = 0; attempt < 100; attempt++) {
+ if (!(await exists(path))) return;
+ await delay(100);
+ }
+ throw new Error(`Android guardian retained ${path} after owner death`);
+};
+
+/** Two independent owners must never delete one another's private workspace. */
+export async function verifyWindowsGuardianIsolation() {
+ assert.equal(process.platform, "win32");
+ const first = await launch();
+ let second;
+ try {
+ second = await launch();
+ assert.notEqual(first.path, second.path);
+ await stop(first);
+ assert.equal(
+ await exists(join(second.path, "synthetic.txt")),
+ true,
+ "First owner deleted the second owner's workspace",
+ );
+ await stop(second);
+ console.log("PASS Windows Android guardian two-owner cleanup isolation");
+ } finally {
+ for (const owner of [first, second]) {
+ if (owner === undefined) continue;
+ if (owner.child.exitCode === null && owner.child.signalCode === null)
+ owner.child.kill();
+ await bounded(owner.exit, 15000, "Android guardian owner did not exit");
+ }
+ }
+}
diff --git a/scripts/verify/android/windows-lifecycle.mjs b/scripts/verify/android/windows-lifecycle.mjs
index 5c1a9c02d..d2b281c60 100644
--- a/scripts/verify/android/windows-lifecycle.mjs
+++ b/scripts/verify/android/windows-lifecycle.mjs
@@ -17,6 +17,7 @@ import {
resolveJadxConfigurationInputs,
validateJadxConfiguration,
} from "../../../dist/android/JadxConfiguration.js";
+import { verifyWindowsGuardianIsolation } from "./windows-guardian-isolation.mjs";
/** Verify real Windows cancellation, disconnect and abrupt CLI owner exit. */
export async function verifyWindowsAndroidLifecycle(options) {
@@ -24,15 +25,16 @@ export async function verifyWindowsAndroidLifecycle(options) {
await verifyJavaSelection(options.environment);
const cancellation = await verifyMcp(options, "cancel");
await verifyMcp(options, "disconnect");
- await verifyCliOwnerExit(options);
+ const abruptWorkspaceRemoved = await verifyCliOwnerExit(options);
+ await verifyWindowsGuardianIsolation();
return {
mcp_cancelled_engine_exited: true,
mcp_cancelled_workspace_removed: true,
mcp_ping_and_next_call: true,
mcp_disconnect_engine_exited: true,
cli_abrupt_owner_exit_engine_exited: true,
- // A forced Windows process exit cannot execute JavaScript workspace cleanup.
- cli_abrupt_owner_exit_workspace_cleanup: false,
+ cli_abrupt_owner_exit_workspace_cleanup: abruptWorkspaceRemoved,
+ guardian_two_owner_isolation: true,
observed_resources: cancellation,
java_home_and_quoted_case_insensitive_path: true,
};
@@ -161,6 +163,7 @@ async function verifyCliOwnerExit({
path,
repository,
}) {
+ const baseline = new Set(await runtimeRoots());
const child = spawn(
process.execPath,
[entrypoint, "inspect-android-package", path, "--format", "json"],
@@ -185,7 +188,40 @@ async function verifyCliOwnerExit({
await deadline(exited, 15000);
await observer.verifyExit();
assert.equal(output, "", "Aborted CLI published successful Evidence");
- console.log("PASS Windows CLI abrupt owner exit: real Java job cleanup");
+ const created = (await runtimeRoots()).filter(
+ (name) => !baseline.has(name),
+ );
+ assert.ok(
+ created.length <= 1,
+ "Concurrent Android workspaces make abrupt cleanup observation ambiguous",
+ );
+ if (created.length === 0) {
+ console.log(
+ "PASS Windows CLI abrupt owner exit: real Java job and workspace cleanup",
+ );
+ return true;
+ }
+ const ownedName = created[0];
+ try {
+ await waitFor(
+ async () => !(await runtimeRoots()).includes(ownedName),
+ 5000,
+ "Android workspace remained after abrupt owner exit",
+ );
+ } catch (cause) {
+ if (
+ !(cause instanceof Error) ||
+ cause.message !== "Android workspace remained after abrupt owner exit"
+ )
+ throw cause;
+ }
+ const removed = !(await runtimeRoots()).includes(ownedName);
+ console.log(
+ removed
+ ? "PASS Windows CLI abrupt owner exit: real Java job and workspace cleanup"
+ : "OBSERVED Windows CLI abrupt owner exit: Java job cleaned, workspace residue remains",
+ );
+ return removed;
} finally {
if (child.exitCode === null && child.signalCode === null) child.kill();
await deadline(exited, 15000);
diff --git a/scripts/verify/architecture-guards.mjs b/scripts/verify/architecture-guards.mjs
index 967e210de..3ca254d7e 100644
--- a/scripts/verify/architecture-guards.mjs
+++ b/scripts/verify/architecture-guards.mjs
@@ -89,6 +89,30 @@ check(
["src/domain/javascript/javascriptSemanticTraversal.ts"],
);
+// A producer that pins both the soft and hard ceiling of every resource must take
+// the vector from one owner. Audited 2026-10-13 against the other prlimit lanes:
+// src/pcap/PcapNativeBound.ts pairs soft and hard but budgets 300 CPU seconds and
+// has no resident-input ceiling; src/evm/EvmoleInterfaceProvider.ts,
+// src/firmware/FirmwareCommand.ts and src/javascript/recovery/WakaruCommand.ts
+// spell soft-only forms (`--core=0:`, `--core=0`). Those four stay as they are;
+// a new paired producer calls prlimitArgumentVector instead of spelling options.
+check(
+ "inline paired prlimit ceilings (use prlimitArgumentVector from src/process/LinuxResourceLimits)",
+ files,
+ /--core=0:0/,
+ ["src/process/LinuxResourceLimits.ts", "src/pcap/PcapNativeBound.ts"],
+);
+
+// Census is spelling-shaped, like the rules above: a lane that expresses the same
+// ceilings differently (`1024 ** 3`, camelCase fields) is not caught here, which is
+// why the four lanes above are listed rather than left to a future reader.
+check(
+ "inline resident-input budget (use BOUNDED_PRODUCER_LIMITS from src/process/LinuxResourceLimits)",
+ files,
+ /input_bytes:\s*32 \* 1024 \* 1024/,
+ ["src/process/LinuxResourceLimits.ts"],
+);
+
// Fixed-arity backtick keys audited 2026-10-10: every prefix field is NUL-free
// (hex digests, file paths, enums, numbers, or compositeKey JSON which escapes
// NUL), with free-form source strings only in final position. New backtick-\0
diff --git a/scripts/website_i18n.py b/scripts/website_i18n.py
index 928594df7..0f93546f0 100644
--- a/scripts/website_i18n.py
+++ b/scripts/website_i18n.py
@@ -25,11 +25,27 @@ LITERALS = {"REA", "Reverse Engineer Anything", "REA · Reverse Engineer Anythin
FALLBACK_NOTICE = "Translation is being updated. Read this page in English for now."
+def readme_edition_paths(root):
+ """Return repo-relative README editions: root English plus docs/readme translations."""
+ editions = set()
+ if (root / "README.md").is_file():
+ editions.add("README.md")
+ folder = root / "docs" / "readme"
+ if folder.is_dir():
+ editions.update(
+ f"docs/readme/{path.name}"
+ for path in folder.glob("README_*.md")
+ if path.is_file()
+ )
+ return editions
+
+
def languages(root):
"""Load the website language inventory and check it against README editions."""
path = root / "website/locales/languages.json"
+ editions = readme_edition_paths(root)
if not path.exists():
- if any(root.glob("README_*.md")):
+ if any(edition != "README.md" for edition in editions):
raise ValueError("Missing languages.json for the translated README editions")
return [{"code": "en", "name": "English", "readme": "README.md", "dir": "ltr"}]
data = json.loads(path.read_text(encoding="utf-8"))
@@ -49,8 +65,7 @@ def languages(root):
raise ValueError(f"Duplicate README edition: {item['readme']}")
codes.add(item["code"])
filenames.add(item["readme"])
- readmes = {path.name for path in root.glob("README*.md")}
- if readmes and ({item["readme"] for item in data} != readmes):
+ if editions and ({item["readme"] for item in data} != editions):
raise ValueError("Website languages must match the README language editions")
return data
diff --git a/src/adb/AdbProvider.test.ts b/src/adb/AdbProvider.test.ts
index 23645e456..3d15325fd 100644
--- a/src/adb/AdbProvider.test.ts
+++ b/src/adb/AdbProvider.test.ts
@@ -1127,6 +1127,68 @@ exit 1`,
expect(projected.message).toContain("not installed for 0");
});
+// adb exits 1 on a refused package change: `adb install` after printing the
+// status line to stderr, `cmd package uninstall` after printing it to stdout.
+it("preserves the device's install refusal when adb exits nonzero", async () => {
+ const { binary, root } = await writeAdbStub(
+ () => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
+if [ "$1" = "-s" ] && [ "$3" = "install" ]; then
+ printf 'Performing Streamed Install\\n'
+ printf 'adb: failed to install app.apk: Failure [INSTALL_FAILED_ALREADY_EXISTS: Attempt to re-install com.example without first uninstalling.]\\n' >&2
+ exit 1
+fi
+exit 1`,
+ );
+ const apk = join(root, "app.apk");
+ await writeFile(apk, "apk");
+ const projected = await refusedBy(providerOver(binary), {
+ operation: "install_adb_package",
+ input: { serial: "emulator-5554", apk_path: apk, replace: false },
+ });
+ expect(projected).toMatchObject({
+ code: "unsupported_target",
+ details: {
+ reason:
+ "INSTALL_FAILED_ALREADY_EXISTS: Attempt to re-install com.example without first uninstalling.",
+ },
+ });
+ expect(projected.message).toContain(
+ "INSTALL_FAILED_ALREADY_EXISTS: Attempt to re-install com.example without first uninstalling.",
+ );
+});
+
+it("preserves the device's uninstall refusal when adb exits nonzero", async () => {
+ const { binary } = await writeAdbStub(
+ () => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
+if [ "$1" = "-s" ] && [ "$3" = "uninstall" ]; then printf 'Failure [not installed for 0]\\n'; exit 1; fi
+exit 1`,
+ );
+ const projected = await refusedBy(providerOver(binary), {
+ operation: "uninstall_adb_package",
+ input: { serial: "emulator-5554", package: "com.absent" },
+ });
+ expect(projected).toMatchObject({
+ code: "unsupported_target",
+ details: { reason: "not installed for 0" },
+ });
+ expect(projected.message).toContain("not installed for 0");
+});
+
+it("keeps a nonzero install without a device verdict as an unreadable result", async () => {
+ const { binary, root } = await writeAdbStub(
+ () => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
+if [ "$1" = "-s" ] && [ "$3" = "install" ]; then printf 'adb: failed to install app.apk: closed\\n' >&2; exit 1; fi
+exit 1`,
+ );
+ const apk = join(root, "app.apk");
+ await writeFile(apk, "apk");
+ const projected = await refusedBy(providerOver(binary), {
+ operation: "install_adb_package",
+ input: { serial: "emulator-5554", apk_path: apk, replace: false },
+ });
+ expect(projected).toMatchObject({ code: "unreadable_output" });
+});
+
it("starts an app through resolved launcher component and force-stops it", async () => {
const { binary } = await writeAdbStub(
shellStub({
diff --git a/src/adb/AdbProvider.ts b/src/adb/AdbProvider.ts
index d26345eba..bce989aca 100644
--- a/src/adb/AdbProvider.ts
+++ b/src/adb/AdbProvider.ts
@@ -71,6 +71,7 @@ import {
parseBugreportCopiedPath,
parseDumpsysPackageOutput,
parseFeatureListOutput,
+ packageChangeFailureReason,
parsePackageChangeOutput,
parseProcessListOutput,
parsePngDimensions,
@@ -248,6 +249,31 @@ const runAdb = async (
});
};
+/**
+ * adb exits nonzero when the device refuses a package change, printing the
+ * `Failure [...]` verdict to stderr (install) or stdout (uninstall). Read the
+ * refusal from that failed run so it is not reported as unreadable output.
+ */
+const packageChangeRefusal = (
+ operation: string,
+ serial: string,
+ error: AnalysisError,
+): Result | undefined => {
+ if (!(error instanceof AnalysisProtocolError)) return undefined;
+ const failure = execFileOutputFailure(error.cause);
+ if (failure === undefined) return undefined;
+ const reason = packageChangeFailureReason(
+ `${failure.stdout}\n${failure.stderr}`,
+ );
+ return reason === null
+ ? undefined
+ : err(
+ new AnalysisUnsupportedTargetError(operation, serial, reason, {
+ cause: error,
+ }),
+ );
+};
+
const clientIdentityResult = (
selection: AdbBinarySelection,
identity: AdbClientIdentity,
@@ -1754,7 +1780,11 @@ export class AdbProvider {
{ timeoutMs: PULL_TIMEOUT_MS, maxOutputBytes: PULL_MAX_OUTPUT_BYTES },
signal,
);
- if (!installed.ok) return installed;
+ if (!installed.ok)
+ return (
+ packageChangeRefusal(operation, input.serial, installed.error) ??
+ installed
+ );
const verdict = parsePackageChangeOutput(installed.value.stdout);
if (verdict.status === "failure")
return err(
@@ -1803,7 +1833,11 @@ export class AdbProvider {
},
signal,
);
- if (!uninstalled.ok) return uninstalled;
+ if (!uninstalled.ok)
+ return (
+ packageChangeRefusal(operation, input.serial, uninstalled.error) ??
+ uninstalled
+ );
const verdict = parsePackageChangeOutput(uninstalled.value.stdout);
if (verdict.status === "failure")
return err(
diff --git a/src/adb/AdbShellOutput.ts b/src/adb/AdbShellOutput.ts
index 726e6164d..6dac4cc0c 100644
--- a/src/adb/AdbShellOutput.ts
+++ b/src/adb/AdbShellOutput.ts
@@ -293,6 +293,10 @@ export const parseBugreportCopiedPath = (stdout: string): string | null => {
return match === null ? null : match[1]!;
};
+/** Read the device's `Failure [REASON]` verdict from package-change output. */
+export const packageChangeFailureReason = (output: string): string | null =>
+ /Failure \[([^\]]+)\]/u.exec(output)?.[1] ?? null;
+
/**
* Parse `adb install`/`adb uninstall` output. adb can print `Failure [...]`
* with a zero exit code, so the text decides, not the exit status.
@@ -305,10 +309,11 @@ export const parsePackageChangeOutput = (
} => {
const text = stdout.trim();
if (/^Success$/mu.test(text)) return { status: "success", reason: null };
- const failure = /Failure \[([^\]]+)\]/u.exec(text);
return {
status: "failure",
- reason: failure?.[1] ?? (text === "" ? null : text.slice(0, 512)),
+ reason:
+ packageChangeFailureReason(text) ??
+ (text === "" ? null : text.slice(0, 512)),
};
};
diff --git a/src/android/JadxAnalysis.ts b/src/android/JadxAnalysis.ts
index d8afed20c..502c90a69 100644
--- a/src/android/JadxAnalysis.ts
+++ b/src/android/JadxAnalysis.ts
@@ -45,7 +45,7 @@ const methodsOf = (summary: ClassSummary) => {
reported_signature: method.signature,
overload_index: index,
is_constructor: method.is_constructor,
- dex_descriptor: null,
+ dex_descriptor: method.dex_descriptor,
};
});
};
@@ -314,11 +314,25 @@ const traceReferences = async (
): Promise => {
const methodName = request.input.method_name;
const candidates = matchingMethods(summary, request);
- if (candidates.length > 1)
- throw new AnalysisCapabilityUnavailableError(
- "jadx",
- request.operation,
- `References require a unique method name; ${summary.full_name}.${methodName} has ${candidates.length} overloads. No unambiguous reference result is available.`,
+ if (methodName === undefined && request.input.overload_index !== undefined)
+ inputFailure(
+ request,
+ "overload_index",
+ "Method name is required for an overload index.",
+ );
+ if (candidates.length > 1 && request.input.overload_index === undefined)
+ inputFailure(
+ request,
+ "overload_index",
+ `Select one of ${candidates.length} overloads: ${candidates.map((method) => `${method.overload_index}: ${method.reported_signature}`).join("; ")}`,
+ );
+ const index = request.input.overload_index ?? 0;
+ const selected = methodName === undefined ? undefined : candidates[index];
+ if (methodName !== undefined && selected === undefined)
+ inputFailure(
+ request,
+ "overload_index",
+ `Index ${index} is outside the ${candidates.length} matching overloads.`,
);
const expectedTarget =
methodName === undefined
@@ -329,7 +343,9 @@ const traceReferences = async (
methodName === undefined ? "get_xrefs_to_class" : "get_xrefs_to_method",
{
class_name: summary.full_name,
- ...(methodName === undefined ? {} : { method_name: methodName }),
+ ...(methodName === undefined
+ ? {}
+ : { method_name: methodName, overload_index: index }),
limit: 2_147_483_647,
resolve_line: false,
},
@@ -337,6 +353,9 @@ const traceReferences = async (
);
if (
xrefs.target !== expectedTarget ||
+ (selected !== undefined &&
+ (xrefs.overload_index !== index ||
+ xrefs.reported_signature !== selected.reported_signature)) ||
xrefs.total !== xrefs.count ||
xrefs.count !== xrefs.items.length
)
@@ -348,6 +367,9 @@ const traceReferences = async (
engine,
class_name: summary.full_name,
method_name: methodName ?? null,
+ overload_index: selected?.overload_index ?? null,
+ reported_signature: selected?.reported_signature ?? null,
+ dex_descriptor: selected?.dex_descriptor ?? null,
total: xrefs.total,
references: xrefs.items.map((reference) => ({
kind: reference.kind,
@@ -357,7 +379,10 @@ const traceReferences = async (
top_class: reference.top_class,
source_line: null,
dex_offset: null,
+ direct_invokes: reference.direct_invokes ?? null,
+ direct_invokes_exhaustive: reference.direct_invokes_exhaustive ?? null,
})),
coverage: "complete",
+ reference_unit: "using_node",
});
};
diff --git a/src/android/JadxProtocol.ts b/src/android/JadxProtocol.ts
index 804b7ffa7..22a457b0f 100644
--- a/src/android/JadxProtocol.ts
+++ b/src/android/JadxProtocol.ts
@@ -2,6 +2,7 @@ import { z } from "zod";
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import type { AndroidOperation } from "../domain/android/androidAnalysis.js";
+import { dexDescriptorSchema } from "../domain/android/androidAnalysis.js";
const count = z.number().int().nonnegative();
/** Selector failures authored by REA's bridge, separate from arbitrary engine errors. */
@@ -50,6 +51,7 @@ export const jadxClassSchema = z.object({
z.object({
name: z.string(),
signature: z.string(),
+ dex_descriptor: dexDescriptorSchema,
is_constructor: z.boolean(),
def_pos: z.number().int(),
}),
@@ -72,6 +74,8 @@ export const jadxMethodSchema = z.object({
});
export const jadxXrefsSchema = z.object({
target: z.string(),
+ overload_index: count.optional(),
+ reported_signature: z.string().optional(),
total: count,
count,
resolve_line: z.literal(false),
@@ -83,6 +87,18 @@ export const jadxXrefsSchema = z.object({
containing_class: z.string(),
top_class: z.string(),
def_pos: z.number().int(),
+ direct_invokes_exhaustive: z.boolean().optional(),
+ direct_invokes: z
+ .array(
+ z.object({
+ dex_entry: z.string().min(1),
+ source_raw_method_id: z.string().min(1),
+ dex_code_unit_offset: count,
+ dex_method_index: count,
+ opcode: z.string().regex(/^INVOKE_/u),
+ }),
+ )
+ .optional(),
}),
),
});
diff --git a/src/android/JadxProvider.ts b/src/android/JadxProvider.ts
index 6fda5a143..3980ce318 100644
--- a/src/android/JadxProvider.ts
+++ b/src/android/JadxProvider.ts
@@ -18,7 +18,10 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { AndroidRequest } from "../domain/android/androidAnalysis.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { err, ok, type Result } from "../domain/result.js";
-import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
+import {
+ createJadxWorkspaceRoot,
+ type JadxWorkspaceRoot,
+} from "./JadxWorkspaceRoot.js";
import {
snapshotAndroidEngine,
snapshotAndroidTarget,
@@ -152,7 +155,7 @@ const hashAndroidSource = async (
interface PendingCleanup {
session: JadxSession | undefined;
- root: PrivateRuntimeRoot | undefined;
+ root: JadxWorkspaceRoot | undefined;
readonly previousError: AnalysisError | undefined;
}
@@ -211,7 +214,7 @@ const cleanup = async (
interface RetainedSession {
readonly key: string;
- readonly root: PrivateRuntimeRoot;
+ readonly root: JadxWorkspaceRoot;
readonly session: JadxSession;
readonly snapshot: string;
readonly jarHash: string;
@@ -325,7 +328,7 @@ export class JadxProvider implements AndroidAnalysisPort {
options?.signal === undefined
? timeout
: AbortSignal.any([options.signal, timeout]);
- let root: PrivateRuntimeRoot | undefined;
+ let root: JadxWorkspaceRoot | undefined;
let session: JadxSession | undefined;
try {
if (target.format !== "apk")
@@ -389,20 +392,24 @@ export class JadxProvider implements AndroidAnalysisPort {
request.operation,
signal,
);
- root = await PrivateRuntimeRoot.create({ prefix: "rea-android-" });
+ root = await createJadxWorkspaceRoot();
+ const workspaceSignal =
+ root.failureSignal === undefined
+ ? signal
+ : AbortSignal.any([signal, root.failureSignal]);
const engine = await snapshotAndroidEngine(
configuration.jar,
jarHash,
root.path,
request.operation,
- signal,
+ workspaceSignal,
);
const snapshot = await snapshotAndroidTarget(
target.path,
target.sha256,
root.path,
request.operation,
- signal,
+ workspaceSignal,
);
const bridge = join(root.path, "ReaJadxBridge.java");
await copyFile(BRIDGE_SOURCE, bridge);
@@ -410,7 +417,7 @@ export class JadxProvider implements AndroidAnalysisPort {
const copiedBridgeHash = await hashAndroidSource(
bridge,
request.operation,
- signal,
+ workspaceSignal,
);
if (copiedBridgeHash !== bridgeHash)
throw androidSourceChanged(
@@ -418,7 +425,7 @@ export class JadxProvider implements AndroidAnalysisPort {
BRIDGE_SOURCE,
undefined,
);
- signal.throwIfAborted();
+ workspaceSignal.throwIfAborted();
session = new JadxSession(
{
command: configuration.java,
@@ -430,7 +437,7 @@ export class JadxProvider implements AndroidAnalysisPort {
],
cwd: root.path,
hostEnvironment: this.environment,
- signal,
+ signal: workspaceSignal,
},
this.launcher,
);
@@ -445,6 +452,10 @@ export class JadxProvider implements AndroidAnalysisPort {
}
root = retained.root;
session = retained.session;
+ const activeSignal =
+ root.failureSignal === undefined
+ ? signal
+ : AbortSignal.any([signal, root.failureSignal]);
const { snapshot } = retained;
const outcome = ok(
await session.execute({
@@ -453,10 +464,10 @@ export class JadxProvider implements AndroidAnalysisPort {
snapshot,
jarHash: retained.jarHash,
bridgeHash: retained.bridgeHash,
- signal,
+ signal: activeSignal,
}),
);
- signal.throwIfAborted();
+ activeSignal.throwIfAborted();
this.#retained = retained;
return outcome;
} catch (cause) {
diff --git a/src/android/JadxRelease.ts b/src/android/JadxRelease.ts
index b5b6bd058..8c7d16840 100644
--- a/src/android/JadxRelease.ts
+++ b/src/android/JadxRelease.ts
@@ -25,7 +25,8 @@ export const JADX_PROVIDER_IDENTITY = Object.freeze({
/** Audited engine coverage, kept outside portable Android semantics. */
export const JADX_LIMITATIONS = [
"Static decompiler observations do not establish runtime behavior; the APK is never executed.",
- "Method signatures use provider display types; exact DEX descriptors and instruction offsets are unavailable. Overload indices are local to this engine and artifact.",
+ "Method signatures use provider display types; DEX prototypes come from frozen JADX metadata. Display names may be aliases, and overload indices are local to this engine and artifact.",
"Class inventories use parsed metadata before code generation, including synthetic members that may be omitted from decompiled source. Source positions remain unknown.",
+ "Incoming reference totals count JADX using nodes. Direct invoke entries within a using method report raw DEX code-unit offsets and method indexes, but coverage is limited to those nodes and directly matched invokes; it does not establish every possible dynamic or reflective call site.",
"Manifest summary fields are extracted by the upstream engine; decoded XML is retained for verification. APK signatures, split APKs, native libraries and Android runtime capture are not verified by this provider.",
] as const;
diff --git a/src/android/JadxWorkspaceRoot.ts b/src/android/JadxWorkspaceRoot.ts
new file mode 100644
index 000000000..15b862320
--- /dev/null
+++ b/src/android/JadxWorkspaceRoot.ts
@@ -0,0 +1,224 @@
+import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
+import { lstat } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { win32 } from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { z } from "zod";
+
+import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
+
+/** Match the native owner's ordinary spelling only for extended local drive paths. */
+const localDrivePath = (path: string): string =>
+ path.replace(/^\\\\\?\\(?=[A-Za-z]:\\)/u, "");
+
+const guardianSource = fileURLToPath(
+ new URL("../../scripts/rea-android-workspace-guardian.mjs", import.meta.url),
+);
+const readySchema = z.strictObject({
+ path: z.string(),
+ volumeSerial: z.string().regex(/^[a-f0-9]{16}$/u),
+ fileId: z.string().regex(/^[a-f0-9]{32}$/u),
+ privateDacl: z.literal(true),
+});
+
+/** The Android provider needs a path and a cleanup owner, regardless of host. */
+export interface JadxWorkspaceRoot {
+ readonly path: string;
+ readonly failureSignal?: AbortSignal;
+ close(): Promise;
+}
+
+type GuardianExit = {
+ readonly code: number | null;
+ readonly signal: NodeJS.Signals | null;
+ readonly error?: Error;
+};
+
+const boundedWait = async (
+ promise: Promise,
+ milliseconds: number,
+ label: string,
+): Promise => {
+ let timer: NodeJS.Timeout | undefined;
+ try {
+ return await Promise.race([
+ promise,
+ new Promise((_resolve, reject) => {
+ timer = setTimeout(() => reject(new Error(label)), milliseconds);
+ }),
+ ]);
+ } finally {
+ clearTimeout(timer);
+ }
+};
+
+/** A detached guardian retains native cleanup ownership without keeping its caller alive. */
+const releaseGuardianReferences = (
+ child: ChildProcessWithoutNullStreams,
+): void => {
+ child.stdin.destroy();
+ child.stdout.destroy();
+ child.stderr.destroy();
+ child.unref();
+};
+
+class WindowsJadxWorkspaceRoot implements JadxWorkspaceRoot {
+ readonly #failure = new AbortController();
+ readonly failureSignal = this.#failure.signal;
+ readonly #child: ChildProcessWithoutNullStreams;
+ readonly #exit: Promise;
+ readonly #diagnostics: () => string;
+ #closing = false;
+ #closePromise: Promise | undefined;
+
+ private constructor(
+ readonly path: string,
+ child: ChildProcessWithoutNullStreams,
+ exit: Promise,
+ diagnostics: () => string,
+ ) {
+ this.#child = child;
+ this.#exit = exit;
+ this.#diagnostics = diagnostics;
+ void this.#exit.then((result) => {
+ if (!this.#closing)
+ this.#failure.abort(
+ new Error(
+ `Android workspace guardian exited before cleanup: ${result.error?.message ?? result.code ?? result.signal ?? "unknown"}`,
+ ),
+ );
+ });
+ }
+
+ static async create(): Promise {
+ const parent = tmpdir();
+ const child = spawn(process.execPath, [guardianSource, parent], {
+ detached: true,
+ windowsHide: true,
+ stdio: ["pipe", "pipe", "pipe"],
+ env: {
+ SystemRoot: process.env.SystemRoot,
+ WINDIR: process.env.WINDIR,
+ TEMP: parent,
+ TMP: parent,
+ },
+ });
+ let diagnostics = "";
+ child.stderr.on("data", (chunk: Buffer) => {
+ diagnostics = (diagnostics + chunk.toString()).slice(-8192);
+ });
+ const exit = new Promise((resolve) => {
+ child.once("error", (error: Error) =>
+ resolve({ code: null, signal: null, error }),
+ );
+ child.once("close", (code, signal) => resolve({ code, signal }));
+ });
+ const ready = new Promise((resolve, reject) => {
+ let frame = "";
+ child.stdout.on("data", (chunk: Buffer) => {
+ frame += chunk.toString();
+ if (frame.length > 4096) {
+ reject(
+ new Error("Android workspace guardian readiness exceeded 4 KiB"),
+ );
+ return;
+ }
+ const end = frame.indexOf("\n");
+ if (end !== -1) resolve(frame.slice(0, end));
+ });
+ child.stdout.once("error", reject);
+ });
+ try {
+ const line = await boundedWait(
+ Promise.race([
+ ready,
+ exit.then((result) => {
+ throw new Error(
+ `Android workspace guardian exited during startup: ${result.error?.message ?? result.code ?? result.signal ?? "unknown"}; ${diagnostics}`,
+ );
+ }),
+ ]),
+ 15000,
+ "Android workspace guardian startup timed out",
+ );
+ const observation = readySchema.parse(JSON.parse(line));
+ if (
+ win32.dirname(localDrivePath(observation.path)).toLowerCase() !==
+ localDrivePath(win32.resolve(parent)).toLowerCase() ||
+ !/^rea-android-[a-f0-9]{32}$/u.test(win32.basename(observation.path))
+ )
+ throw new Error(
+ "Android workspace guardian returned an unexpected root",
+ );
+ child.stdout.resume();
+ return new WindowsJadxWorkspaceRoot(
+ observation.path,
+ child,
+ exit,
+ () => diagnostics,
+ );
+ } catch (cause) {
+ if (!child.stdin.destroyed) child.stdin.end();
+ try {
+ await boundedWait(
+ exit,
+ 30000,
+ "Android workspace guardian did not settle after startup failure",
+ );
+ } catch (cleanupFailure) {
+ throw new AggregateError(
+ [cause, cleanupFailure],
+ "Android workspace guardian startup and cleanup failed",
+ );
+ } finally {
+ releaseGuardianReferences(child);
+ }
+ throw cause;
+ }
+ }
+
+ close(): Promise {
+ this.#closing = true;
+ this.#closePromise ??= this.#finish().catch((cause: unknown) => {
+ this.#closePromise = undefined;
+ throw cause;
+ });
+ return this.#closePromise;
+ }
+
+ async #finish(): Promise {
+ if (!this.#child.stdin.destroyed) this.#child.stdin.end();
+ let result: GuardianExit;
+ try {
+ result = await boundedWait(
+ this.#exit,
+ 120000,
+ `Android workspace guardian cleanup timed out for ${this.path}`,
+ );
+ } catch (cause) {
+ releaseGuardianReferences(this.#child);
+ throw cause;
+ }
+ if (result.error !== undefined || result.code !== 0)
+ throw new Error(
+ `Android workspace guardian cleanup failed for ${this.path}: ${result.error?.message ?? result.code ?? result.signal ?? "unknown"}; ${this.#diagnostics()}`,
+ );
+ try {
+ await lstat(this.path);
+ } catch (cause) {
+ if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
+ return;
+ throw cause;
+ }
+ throw new Error(
+ `Android workspace guardian exited but retained ${this.path}`,
+ );
+ }
+}
+
+/** POSIX retains its in-process root; Windows keeps a detached native owner. */
+export const createJadxWorkspaceRoot = (): Promise =>
+ process.platform === "win32"
+ ? WindowsJadxWorkspaceRoot.create()
+ : PrivateRuntimeRoot.create({ prefix: "rea-android-" });
diff --git a/src/apktool/ApktoolConfiguration.test.ts b/src/apktool/ApktoolConfiguration.test.ts
index d2d84874c..39cf1c652 100644
--- a/src/apktool/ApktoolConfiguration.test.ts
+++ b/src/apktool/ApktoolConfiguration.test.ts
@@ -1,4 +1,4 @@
-import { describe, expect, it } from "vitest";
+import { beforeEach, describe, expect, it, vi } from "vitest";
import {
APKTOOL_COMMAND_ENV,
@@ -10,29 +10,43 @@ import {
resolveApktoolCommand,
} from "./ApktoolConfiguration.js";
+const lookup = vi.hoisted(() => ({ stat: vi.fn() }));
+vi.mock("node:fs/promises", async (importOriginal) => ({
+ ...(await importOriginal()),
+ stat: lookup.stat,
+}));
+beforeEach(() => {
+ lookup.stat
+ .mockReset()
+ .mockRejectedValue(Object.assign(new Error("missing"), { code: "ENOENT" }));
+});
+
describe("resolveApktoolCommand", () => {
- it("prefers an absolute REA_APKTOOL_COMMAND", () => {
+ it("prefers an absolute REA_APKTOOL_COMMAND", async () => {
expect(
- resolveApktoolCommand({
- [APKTOOL_COMMAND_ENV]: "/usr/local/bin/apktool",
- }),
+ await resolveApktoolCommand(
+ {
+ [APKTOOL_COMMAND_ENV]: "/usr/local/bin/apktool",
+ },
+ "linux",
+ ),
).toEqual({
command: "/usr/local/bin/apktool",
commandSource: "environment",
});
});
- it("falls back to PATH resolution without the environment key", () => {
- expect(resolveApktoolCommand({})).toEqual({
+ it("leaves POSIX PATH lookup to process creation", async () => {
+ expect(await resolveApktoolCommand({}, "linux")).toEqual({
command: "apktool",
commandSource: "path",
});
});
- it("rejects a relative environment path explicitly", () => {
- expect(() =>
+ it("rejects a relative environment path explicitly", async () => {
+ await expect(
resolveApktoolCommand({ [APKTOOL_COMMAND_ENV]: "bin/apktool" }),
- ).toThrow(/must be an absolute path/u);
+ ).rejects.toThrow(/must be an absolute path/u);
});
});
@@ -119,3 +133,250 @@ describe("apktoolInvocation", () => {
});
});
});
+
+describe("Windows PATH launcher selection", () => {
+ const environment = {
+ Path: '"C:\\Selected Tools";C:\\Other Tools',
+ PathExt: ".EXE;.CMD;.BAT",
+ ComSpec: "C:\\Windows\\System32\\cmd.exe",
+ NoDefaultCurrentDirectoryInExePath: "",
+ };
+ const files = (...paths: string[]) => {
+ const selected = new Set(paths.map((path) => path.toLowerCase()));
+ lookup.stat.mockImplementation(async (path: string) => {
+ if (selected.has(path.toLowerCase())) return { isFile: () => true };
+ throw Object.assign(new Error("missing"), { code: "ENOENT" });
+ });
+ };
+
+ it.each([".BAT", ".CMD"])(
+ "resolves a default %s launcher before choosing cmd.exe for version and decode",
+ async (extension) => {
+ const command = `C:\\Selected Tools\\apktool${extension}`;
+ files(command);
+ const selection = await resolveApktoolCommand(environment, "win32");
+ expect(selection).toEqual({ command, commandSource: "path" });
+ for (const arguments_ of [
+ ["--version"],
+ ["d", "-o", "C:\\output folder", "C:\\target.apk"],
+ ]) {
+ const invocation = apktoolInvocation(
+ selection,
+ arguments_,
+ environment,
+ "win32",
+ );
+ expect(invocation).toEqual({
+ command: environment.ComSpec,
+ arguments: [
+ "/d",
+ "/e:on",
+ "/v:off",
+ "/s",
+ "/c",
+ `"${[command, ...arguments_].map((token) => `"${token}"`).join(" ")}"`,
+ ],
+ windowsVerbatimArguments: true,
+ });
+ }
+ },
+ );
+
+ it("honors PATH directory order before extension order", async () => {
+ files("C:\\Selected Tools\\apktool.CMD", "C:\\Other Tools\\apktool.EXE");
+ expect(await resolveApktoolCommand(environment, "win32")).toMatchObject({
+ command: "C:\\Selected Tools\\apktool.CMD",
+ });
+ });
+
+ it("honors the selected PATHEXT ordering", async () => {
+ files("C:\\Selected Tools\\apktool.CMD", "C:\\Selected Tools\\apktool.EXE");
+ expect(
+ await resolveApktoolCommand(
+ { ...environment, PathExt: ".CMD;.EXE" },
+ "win32",
+ ),
+ ).toMatchObject({ command: "C:\\Selected Tools\\apktool.CMD" });
+ });
+
+ it("keeps native executable arguments outside cmd.exe interpretation", async () => {
+ const command = "C:\\Selected Tools\\apktool.EXE";
+ files(command);
+ const selection = await resolveApktoolCommand(environment, "win32");
+ const arguments_ = ["d", "C:\\input & literal.apk"];
+ expect(
+ apktoolInvocation(selection, arguments_, environment, "win32"),
+ ).toEqual({
+ command,
+ arguments: arguments_,
+ windowsVerbatimArguments: false,
+ });
+ });
+
+ it("preserves explicit launcher spelling without searching PATH", async () => {
+ const command = "C:\\selected alias\\APKTOOL.cmd";
+ expect(
+ await resolveApktoolCommand(
+ { ...environment, [APKTOOL_COMMAND_ENV]: command },
+ "win32",
+ ),
+ ).toEqual({ command, commandSource: "environment" });
+ expect(lookup.stat).not.toHaveBeenCalled();
+ });
+
+ it("does not inspect the current directory when the caller disables it", async () => {
+ await expect(
+ resolveApktoolCommand(environment, "win32"),
+ ).rejects.toMatchObject({ code: "command_missing" });
+ expect(
+ lookup.stat.mock.calls.every(
+ ([path]) =>
+ String(path).startsWith("C:\\Selected Tools\\") ||
+ String(path).startsWith("C:\\Other Tools\\"),
+ ),
+ ).toBe(true);
+ });
+
+ it("does not turn an explicitly empty PATHEXT into default extensions", async () => {
+ files("C:\\Selected Tools\\apktool.EXE");
+ await expect(
+ resolveApktoolCommand({ ...environment, PathExt: "" }, "win32"),
+ ).rejects.toMatchObject({ code: "command_missing" });
+ expect(lookup.stat).not.toHaveBeenCalled();
+ });
+
+ it("skips directories without treating them as executable launchers", async () => {
+ files("C:\\Other Tools\\apktool.EXE");
+ const stat = lookup.stat.getMockImplementation()!;
+ lookup.stat.mockImplementation(async (path: string) =>
+ path === "C:\\Selected Tools\\apktool.EXE"
+ ? { isFile: () => false }
+ : stat(path),
+ );
+ expect(await resolveApktoolCommand(environment, "win32")).toMatchObject({
+ command: "C:\\Other Tools\\apktool.EXE",
+ });
+ });
+
+ it("preserves inspection failures instead of reporting a missing command or selecting a later file", async () => {
+ lookup.stat.mockRejectedValue(
+ Object.assign(new Error("denied"), { code: "EACCES" }),
+ );
+ const failure = await resolveApktoolCommand(environment, "win32").catch(
+ (cause: unknown) => cause,
+ );
+ expect(failure).toBeInstanceOf(ApktoolConfigurationFailure);
+ if (!(failure instanceof ApktoolConfigurationFailure)) throw failure;
+ expect(failure).toMatchObject({
+ code: "command_unresolved",
+ diagnostics: {
+ candidate_path: "C:\\Selected Tools\\apktool.EXE",
+ filesystem_error: "EACCES",
+ },
+ });
+ expect(apktoolConfigurationAvailability(failure)).toMatchObject({
+ code: "not_configured",
+ });
+ expect(lookup.stat).toHaveBeenCalledTimes(1);
+ });
+
+ it("honors cancellation before inspecting launchers", async () => {
+ const controller = new AbortController();
+ controller.abort();
+ await expect(
+ resolveApktoolCommand(environment, "win32", controller.signal),
+ ).rejects.toMatchObject({ _tag: "AnalysisCancelledError" });
+ expect(lookup.stat).not.toHaveBeenCalled();
+ });
+});
+
+it("does not select a launcher when cancellation arrives during lookup", async () => {
+ const controller = new AbortController();
+ lookup.stat.mockImplementation(async () => {
+ controller.abort();
+ return { isFile: () => true };
+ });
+ await expect(
+ resolveApktoolCommand(
+ {
+ PATH: "C:\\Tools",
+ PATHEXT: ".CMD",
+ NoDefaultCurrentDirectoryInExePath: "1",
+ },
+ "win32",
+ controller.signal,
+ ),
+ ).rejects.toMatchObject({ _tag: "AnalysisCancelledError" });
+ expect(lookup.stat).toHaveBeenCalledTimes(1);
+});
+
+it.each(["EXE", ".CMD\\..\\outside.exe", ".CMD\0"])(
+ "rejects malformed PATHEXT %j before path lookup",
+ async (PATHEXT) => {
+ await expect(
+ resolveApktoolCommand({ PATH: "C:\\Tools", PATHEXT }, "win32"),
+ ).rejects.toMatchObject({
+ code: "command_unresolved",
+ diagnostics: { invalid_extension: PATHEXT },
+ });
+ expect(lookup.stat).not.toHaveBeenCalled();
+ },
+);
+
+it("cancels a pending filesystem lookup and owns its eventual rejection", async () => {
+ const controller = new AbortController();
+ let started!: () => void;
+ let rejectLookup!: (cause: Error) => void;
+ const pendingStat = new Promise((_resolve, reject) => {
+ rejectLookup = reject;
+ });
+ const observed = new Promise((resolve) => {
+ started = resolve;
+ });
+ lookup.stat.mockImplementation(() => {
+ started();
+ return pendingStat;
+ });
+ const pending = resolveApktoolCommand(
+ {
+ PATH: "C:\\Tools",
+ PATHEXT: ".CMD",
+ NODEFAULTCURRENTDIRECTORYINEXEPATH: "1",
+ },
+ "win32",
+ controller.signal,
+ );
+ await observed;
+ controller.abort();
+ await expect(pending).rejects.toMatchObject({
+ _tag: "AnalysisCancelledError",
+ });
+ rejectLookup(new Error("filesystem completed after cancellation"));
+ await Promise.resolve();
+ expect(lookup.stat).toHaveBeenCalledTimes(1);
+});
+
+it("bounds the entire pending PATH lookup by the selection deadline", async () => {
+ vi.useFakeTimers();
+ try {
+ lookup.stat.mockReturnValue(new Promise(() => {}));
+ const failure = expect(
+ resolveApktoolCommand(
+ {
+ PATH: "C:\\Tools",
+ PATHEXT: ".CMD",
+ NODEFAULTCURRENTDIRECTORYINEXEPATH: "1",
+ },
+ "win32",
+ ),
+ ).rejects.toMatchObject({
+ _tag: "AnalysisTimeoutError",
+ timeoutMs: 20_000,
+ });
+ await vi.advanceTimersByTimeAsync(20_000);
+ await failure;
+ expect(lookup.stat).toHaveBeenCalledTimes(1);
+ } finally {
+ vi.useRealTimers();
+ }
+});
diff --git a/src/apktool/ApktoolConfiguration.ts b/src/apktool/ApktoolConfiguration.ts
index 12532b104..b1861f2ac 100644
--- a/src/apktool/ApktoolConfiguration.ts
+++ b/src/apktool/ApktoolConfiguration.ts
@@ -1,5 +1,12 @@
-import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
-import { isAbsolute } from "node:path";
+import {
+ AnalysisCancelledError,
+ AnalysisTimeoutError,
+} from "../domain/analysisErrorCore.js";
+import { isAbsolute, win32 } from "node:path";
+import { stat } from "node:fs/promises";
+import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
+import { ABORTED, waitForAbortable } from "../process/AbortablePromise.js";
+import { ProviderStartupDeadline } from "../process/ProviderDeadline.js";
import {
execFileOutput,
@@ -27,30 +34,119 @@ export interface ApktoolCommandSelection {
}
/**
- * Resolve the caller-supplied apktool launcher. `REA_APKTOOL_COMMAND` must
- * be absolute so the selection stays explicit; without it, apktool is
- * resolved from `PATH` at run time. Both the upstream wrapper script and
- * `java -jar` launcher scripts work: REA only runs ` --version`
- * and ` d ...`.
+ * Preserve an explicit launcher, or resolve the default Windows command before
+ * choosing native versus batch process creation. POSIX keeps execFile's PATH
+ * lookup. The resolved selection is reused for the version and decode runs.
*/
-export const resolveApktoolCommand = (
+export const resolveApktoolCommand = async (
environment: Readonly>,
-): ApktoolCommandSelection => {
- const configured = environment[APKTOOL_COMMAND_ENV];
- if (configured === undefined || configured.trim() === "")
+ platform: NodeJS.Platform = process.platform,
+ signal?: AbortSignal,
+): Promise => {
+ if (signal?.aborted === true)
+ throw new AnalysisCancelledError("inspect_apktool_client");
+ const selectedEnvironment = snapshotEnvironment(environment, platform);
+ const configured = selectedEnvironment[APKTOOL_COMMAND_ENV];
+ if (configured !== undefined && configured.trim() !== "") {
+ if (
+ !(platform === "win32"
+ ? win32.isAbsolute(configured)
+ : isAbsolute(configured))
+ )
+ throw new ApktoolConfigurationFailure(
+ "not_absolute",
+ `${APKTOOL_COMMAND_ENV} must be an absolute path to the apktool launcher`,
+ { configured_command: configured },
+ );
+ return { command: configured, commandSource: "environment" };
+ }
+ if (platform !== "win32")
return { command: "apktool", commandSource: "path" };
- if (!isAbsolute(configured))
- throw new ApktoolConfigurationFailure(
- "not_absolute",
- `${APKTOOL_COMMAND_ENV} must be an absolute path to the apktool launcher`,
- { configured_command: configured },
- );
- return { command: configured, commandSource: "environment" };
+ const extensions = (selectedEnvironment.PATHEXT ?? ".EXE;.COM;.BAT;.CMD")
+ .split(";")
+ .filter((extension) => extension !== "");
+ for (const extension of extensions)
+ if (!extension.startsWith(".") || /[\\/:\0]/u.test(extension))
+ throw new ApktoolConfigurationFailure(
+ "command_unresolved",
+ "Windows PATHEXT must contain filename extensions, not paths",
+ {
+ selected_command: "apktool",
+ command_source: "path",
+ invalid_extension: extension,
+ },
+ );
+ const directories = (selectedEnvironment.PATH ?? "")
+ .split(";")
+ .filter((directory) => directory !== "");
+ if (selectedEnvironment.NODEFAULTCURRENTDIRECTORYINEXEPATH === undefined)
+ directories.unshift(process.cwd());
+ // A single deadline owns the whole PATH search, including filesystem waits.
+ const deadline = new ProviderStartupDeadline(VERSION_TIMEOUT_MS, signal);
+ const lookupInterruption = () =>
+ deadline.interruption === "cancelled"
+ ? new AnalysisCancelledError("inspect_apktool_client")
+ : new AnalysisTimeoutError("inspect_apktool_client", VERSION_TIMEOUT_MS, {
+ diagnostics: { phase: "windows_path_lookup" },
+ });
+ try {
+ for (const entry of directories) {
+ const directory =
+ entry.startsWith('"') && entry.endsWith('"')
+ ? entry.slice(1, -1)
+ : entry;
+ for (const extension of extensions) {
+ if (deadline.interruption !== undefined) throw lookupInterruption();
+ const candidate = win32.resolve(directory, `apktool${extension}`);
+ try {
+ const metadata = await waitForAbortable(
+ stat(candidate),
+ deadline.signal,
+ );
+ if (metadata === ABORTED || deadline.interruption !== undefined)
+ throw lookupInterruption();
+ if (metadata.isFile())
+ return { command: candidate, commandSource: "path" };
+ } catch (cause: unknown) {
+ if (deadline.interruption !== undefined) throw lookupInterruption();
+ const code =
+ typeof cause === "object" && cause !== null
+ ? Reflect.get(cause, "code")
+ : undefined;
+ if (code === "ENOENT" || code === "ENOTDIR") continue;
+ throw new ApktoolConfigurationFailure(
+ "command_unresolved",
+ `The PATH-selected apktool launcher could not be inspected: ${candidate}`,
+ {
+ selected_command: "apktool",
+ command_source: "path",
+ candidate_path: candidate,
+ ...(typeof code === "string" ? { filesystem_error: code } : {}),
+ },
+ { cause },
+ );
+ }
+ }
+ }
+ } finally {
+ deadline.dispose();
+ }
+
+ throw new ApktoolConfigurationFailure(
+ "command_missing",
+ "The apktool launcher was not found on the selected Windows PATH/PATHEXT",
+ {
+ selected_command: "apktool",
+ command_source: "path",
+ remediation: `Set ${APKTOOL_COMMAND_ENV} to an absolute apktool launcher, or add its directory and extension to PATH and PATHEXT`,
+ },
+ );
};
export type ApktoolConfigurationFailureCode =
| "not_absolute"
| "command_missing"
+ | "command_unresolved"
| "launcher_unsupported"
| "version_unresolved";
@@ -121,7 +217,7 @@ export const inspectApktoolClient = async (
environment: Readonly> = process.env,
platform: NodeJS.Platform = process.platform,
): Promise => {
- const hostEnvironment = { ...process.env, ...environment };
+ const hostEnvironment = snapshotEnvironment(environment, platform);
let invocation: ApktoolInvocation;
try {
invocation = apktoolInvocation(
@@ -195,7 +291,9 @@ const rejectionCodeOf = (
): ProviderRejectionCode =>
code === "command_missing"
? "executable_missing"
- : code === "not_absolute" || code === "launcher_unsupported"
+ : code === "not_absolute" ||
+ code === "launcher_unsupported" ||
+ code === "command_unresolved"
? "not_configured"
: "version_unresolved";
diff --git a/src/apktool/ApktoolProvider.ts b/src/apktool/ApktoolProvider.ts
index 1e1f187c0..c58ea1f49 100644
--- a/src/apktool/ApktoolProvider.ts
+++ b/src/apktool/ApktoolProvider.ts
@@ -51,6 +51,7 @@ import type { ProviderProcessSupervisor } from "../process/ProviderProcess.js";
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
import { WindowsBatchCommandError } from "../process/WindowsBatchCommand.js";
import {
+ APKTOOL_COMMAND_ENV,
ApktoolConfigurationFailure,
apktoolConfigurationAvailability,
apktoolInvocation,
@@ -155,10 +156,9 @@ const runApktool = async (
): Promise> => {
if (signal?.aborted === true)
return err(new AnalysisCancelledError(operation));
- const hostEnvironment = { ...process.env, ...environment };
let invocation: ReturnType;
try {
- invocation = apktoolInvocation(selection, arguments_, hostEnvironment);
+ invocation = apktoolInvocation(selection, arguments_, environment);
} catch (cause) {
if (!(cause instanceof WindowsBatchCommandError)) throw cause;
const reason = `The apktool batch launcher cannot receive REA's decode paths through cmd.exe: ${cause.message}`;
@@ -178,7 +178,7 @@ const runApktool = async (
arguments: invocation.arguments,
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
runId: randomUUID(),
- hostEnvironment,
+ hostEnvironment: environment,
},
{
timeoutMs: limits.timeoutMs,
@@ -317,7 +317,9 @@ export interface ApktoolProviderOptions {
* (`--no-src`) because Java-source recovery belongs to the JADX family.
*/
export class ApktoolProvider {
- private readonly environment: Readonly>;
+ private readonly selectedEnvironment:
+ | Readonly>
+ | undefined;
private readonly workspaces = new Set();
private readonly removeWorkspace: (path: string) => Promise;
@@ -328,14 +330,25 @@ export class ApktoolProvider {
#closePromise: Promise | undefined;
constructor(options: ApktoolProviderOptions = {}) {
- this.environment = snapshotEnvironment(options.environment ?? process.env);
+ this.selectedEnvironment =
+ options.environment === undefined
+ ? undefined
+ : snapshotEnvironment(options.environment);
this.removeWorkspace =
options.removeWorkspace ??
((path) => rm(path, { recursive: true, force: true }));
}
- private selection(): ApktoolCommandSelection {
- return resolveApktoolCommand(this.environment);
+ private operationEnvironment(): Readonly {
+ const ambient = snapshotEnvironment(process.env);
+ if (this.selectedEnvironment === undefined) return ambient;
+ // Child inheritance is sampled per operation. An explicit map keeps its
+ // launcher choice authoritative, including an omitted command selecting PATH.
+ return snapshotEnvironment({
+ ...ambient,
+ ...this.selectedEnvironment,
+ [APKTOOL_COMMAND_ENV]: this.selectedEnvironment[APKTOOL_COMMAND_ENV],
+ });
}
/** Probe the selected apktool launcher; decodes nothing. */
@@ -354,9 +367,14 @@ export class ApktoolProvider {
async #inspectAvailability(
signal: AbortSignal,
): Promise {
+ const environment = this.operationEnvironment();
try {
- const selection = this.selection();
- await inspectApktoolClient(selection, signal, this.environment);
+ const selection = await resolveApktoolCommand(
+ environment,
+ process.platform,
+ signal,
+ );
+ await inspectApktoolClient(selection, signal, environment);
throwIfCancelled("inspect_apktool_client", signal);
return {
status: "available",
@@ -406,17 +424,22 @@ export class ApktoolProvider {
): Promise> {
if (signal?.aborted === true)
return err(new AnalysisCancelledError(request.operation));
+ const environment = this.operationEnvironment();
let selection: ApktoolCommandSelection;
let identity: ApktoolClientIdentity;
try {
- selection = this.selection();
- identity = await inspectApktoolClient(
- selection,
+ selection = await resolveApktoolCommand(
+ environment,
+ process.platform,
signal,
- this.environment,
);
+ identity = await inspectApktoolClient(selection, signal, environment);
} catch (cause) {
- if (cause instanceof AnalysisCancelledError) return err(cause);
+ if (
+ cause instanceof AnalysisCancelledError ||
+ cause instanceof AnalysisTimeoutError
+ )
+ return err(cause);
if (cause instanceof ApktoolConfigurationFailure) {
return err(
providerConfigurationRefusal(
@@ -449,6 +472,7 @@ export class ApktoolProvider {
return this.decodeResources(
request.operation,
selection,
+ environment,
client,
provider,
request.input,
@@ -500,6 +524,7 @@ export class ApktoolProvider {
private async decodeResources(
operation: "decode_android_resources",
selection: ApktoolCommandSelection,
+ environment: Readonly,
client: {
command: string;
command_source: string;
@@ -628,7 +653,7 @@ export class ApktoolProvider {
maxOutputBytes: DECODE_MAX_OUTPUT_BYTES,
},
signal,
- this.environment,
+ environment,
(owner) => this.#pendingCommandOwners.set(owner, root),
);
if (!decoded.ok) {
diff --git a/src/application/AppBundleExecutable.test.ts b/src/application/AppBundleExecutable.test.ts
new file mode 100644
index 000000000..942a00112
--- /dev/null
+++ b/src/application/AppBundleExecutable.test.ts
@@ -0,0 +1,34 @@
+import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+
+import { describe, expect, it } from "vitest";
+
+import { resolveAppBundleExecutable } from "./AppBundleExecutable.js";
+
+describe("app bundle Info.plist magic", () => {
+ it("does not decode a high-bit fold of bplist as a binary Info.plist", async () => {
+ const root = await mkdtemp(join(tmpdir(), "rea-bplist-fold-"));
+ try {
+ const bundle = join(root, "Fixture.app");
+ const plistPath = join(bundle, "Contents", "Info.plist");
+ await mkdir(join(bundle, "Contents"), { recursive: true });
+ const plist = Buffer.from("bplist00");
+ plist[0] = (plist[0] ?? 0) | 0x80;
+ await writeFile(plistPath, plist);
+ let decoded = false;
+ const result = await resolveAppBundleExecutable(bundle, {
+ readFile: async () => plist,
+ realpath: async (path) => path,
+ decodeBinaryPlist: async () => {
+ decoded = true;
+ return "Fixture";
+ },
+ });
+ expect(decoded).toBe(false);
+ expect(result.ok).toBe(false);
+ } finally {
+ await rm(root, { recursive: true, force: true });
+ }
+ });
+});
diff --git a/src/application/BinaryTargetResolver.ts b/src/application/BinaryTargetResolver.ts
index 6f1bc4366..b07364adf 100644
--- a/src/application/BinaryTargetResolver.ts
+++ b/src/application/BinaryTargetResolver.ts
@@ -162,6 +162,12 @@ export const parseBinaryTarget = async (
availableArchitectures: ["x86"],
});
}
+ // Probe WASM content before suffix-only database routing; an explicit
+ // database selection keeps the caller's chosen interpretation.
+ const artifactFormat =
+ targetKind === "database"
+ ? undefined
+ : await detectArtifactFormat(path, handle, signal);
if (
targetKind === "database" ||
(targetKind === undefined && path.toLowerCase().endsWith(".hop"))
@@ -173,7 +179,6 @@ export const parseBinaryTarget = async (
kind: "database",
format: "analysis-database",
});
- const artifactFormat = await detectArtifactFormat(path, handle, signal);
if (artifactFormat !== undefined) {
const identity = {
path,
diff --git a/src/application/BrowserEvidence.ts b/src/application/BrowserEvidence.ts
index d864fac9d..495f1f197 100644
--- a/src/application/BrowserEvidence.ts
+++ b/src/application/BrowserEvidence.ts
@@ -131,7 +131,7 @@ const browserParameters = (
}
const observedOrigin = browserResultTargetOrigin(result);
const scope = {
- cdp_endpoint: input.cdp_endpoint,
+ cdp_endpoint: input.cdp_endpoint.recorded,
allowed_origins:
input.allowed_origins.length > 0
? input.allowed_origins
diff --git a/src/application/ClaudeDesktopConfigLocation.test.ts b/src/application/ClaudeDesktopConfigLocation.test.ts
new file mode 100644
index 000000000..84d8ca8bc
--- /dev/null
+++ b/src/application/ClaudeDesktopConfigLocation.test.ts
@@ -0,0 +1,341 @@
+import { basename, dirname, join, sep } from "node:path";
+
+import { describe, expect, it } from "vitest";
+
+import {
+ CLAUDE_DESKTOP_PACKAGE_FAMILY,
+ resolveWindowsClaudeDesktopLocation,
+ type ClaudeDesktopLocationFileSystem,
+} from "./ClaudeDesktopConfigLocation.js";
+import {
+ packagePrivateLauncherBlocker,
+ packagePrivateStorageFamily,
+ redirectedWriteBlocker,
+ type MsixPackageView,
+} from "./MsixPackageStorage.js";
+
+const home = join("/profile", "a");
+const env = {
+ APPDATA: join(home, "AppData", "Roaming"),
+ LOCALAPPDATA: join(home, "AppData", "Local"),
+};
+const packages = join(env.LOCALAPPDATA, "Packages");
+const appDataConfig = join(env.APPDATA, "Claude", "claude_desktop_config.json");
+const privateConfig = join(
+ packages,
+ CLAUDE_DESKTOP_PACKAGE_FAMILY,
+ "LocalCache",
+ "Roaming",
+ "Claude",
+ "claude_desktop_config.json",
+);
+
+const failure = (code: string): Error =>
+ Object.assign(new Error(`${code}: injected`), { code });
+
+const file = { isFile: () => true };
+const privateDirectory = dirname(privateConfig);
+const privatePreferences = join(privateDirectory, "Preferences");
+const appDataDirectory = dirname(appDataConfig);
+
+/** Package storage holding `privateFiles`; realpath decides the process view. */
+const packagedFileSystem = (
+ realpath: ClaudeDesktopLocationFileSystem["realpath"],
+ privateFiles: readonly string[] = [privateConfig],
+): ClaudeDesktopLocationFileSystem => ({
+ readdir: (path) => {
+ if (path === packages) return [CLAUDE_DESKTOP_PACKAGE_FAMILY];
+ if (path === privateDirectory)
+ return privateFiles.map((name) => basename(name));
+ throw failure("ENOENT");
+ },
+ lstat: (path) => {
+ if (!privateFiles.includes(path)) throw failure("ENOENT");
+ return file;
+ },
+ realpath,
+});
+
+describe("Claude Desktop MSIX process view", () => {
+ it("recognizes a process that sees the private copy at the AppData path", () => {
+ const location = resolveWindowsClaudeDesktopLocation(
+ home,
+ env,
+ packagedFileSystem((path) =>
+ path === appDataConfig ? privateConfig : path,
+ ),
+ );
+ expect(location).toEqual({
+ ok: true,
+ value: expect.objectContaining({
+ configPath: privateConfig,
+ msixPackage: expect.objectContaining({ processView: "redirected" }),
+ }),
+ });
+ });
+
+ it("treats an absent AppData file as an unredirected process", () => {
+ const location = resolveWindowsClaudeDesktopLocation(
+ home,
+ env,
+ packagedFileSystem((path) => {
+ if (path === appDataConfig) throw failure("ENOENT");
+ return path;
+ }),
+ );
+ expect(location.ok && location.value.msixPackage?.processView).toBe(
+ "direct",
+ );
+ });
+
+ it("recognizes redirection through another private file before the configuration exists", () => {
+ const location = resolveWindowsClaudeDesktopLocation(
+ home,
+ env,
+ packagedFileSystem(
+ (path) =>
+ path === join(appDataDirectory, "Preferences")
+ ? privatePreferences
+ : path,
+ [privatePreferences],
+ ),
+ );
+ expect(location.ok && location.value.configPath).toBe(privateConfig);
+ expect(location.ok && location.value.msixPackage?.processView).toBe(
+ "redirected",
+ );
+ });
+
+ it("continues after a listed private file disappears", () => {
+ const fs = packagedFileSystem(
+ (path) =>
+ path === join(appDataDirectory, "Preferences")
+ ? privatePreferences
+ : path,
+ [privatePreferences],
+ );
+ const location = resolveWindowsClaudeDesktopLocation(home, env, {
+ ...fs,
+ readdir: (path) =>
+ path === privateDirectory
+ ? ["vanished", "Preferences"]
+ : fs.readdir(path),
+ });
+ expect(location.ok && location.value.msixPackage?.processView).toBe(
+ "redirected",
+ );
+ });
+
+ it("keeps the view unknown when the package has no private files", () => {
+ const location = resolveWindowsClaudeDesktopLocation(
+ home,
+ env,
+ packagedFileSystem((path) => path, []),
+ );
+ expect(location.ok && location.value.msixPackage?.processView).toBe(
+ "unknown",
+ );
+ });
+
+ it("keeps the view unknown when the AppData path cannot be resolved", () => {
+ const location = resolveWindowsClaudeDesktopLocation(
+ home,
+ env,
+ packagedFileSystem((path) => {
+ if (path === appDataConfig) throw failure("EACCES");
+ return path;
+ }),
+ );
+ expect(location.ok && location.value.msixPackage?.processView).toBe(
+ "unknown",
+ );
+ });
+});
+
+describe("unresolved Claude Desktop package identity", () => {
+ it("reports unreadable package storage instead of assuming AppData", () => {
+ const location = resolveWindowsClaudeDesktopLocation(home, env, {
+ readdir: () => {
+ throw failure("EACCES");
+ },
+ lstat: () => file,
+ realpath: (path) => path,
+ });
+ expect(location).toEqual({
+ ok: false,
+ error: {
+ path: packages,
+ message: expect.stringContaining("could not be inspected"),
+ },
+ });
+ });
+
+ it("reports an uninspectable private copy instead of choosing a file", () => {
+ const location = resolveWindowsClaudeDesktopLocation(home, env, {
+ readdir: () => [CLAUDE_DESKTOP_PACKAGE_FAMILY],
+ lstat: () => {
+ throw failure("EPERM");
+ },
+ realpath: (path) => path,
+ });
+ expect(location.ok).toBe(false);
+ expect(!location.ok && location.error.path).toBe(privateConfig);
+ });
+
+ it("matches the package family case-insensitively, as Windows does", () => {
+ const location = resolveWindowsClaudeDesktopLocation(home, env, {
+ readdir: () => [CLAUDE_DESKTOP_PACKAGE_FAMILY.toUpperCase()],
+ lstat: () => {
+ throw failure("ENOENT");
+ },
+ realpath: (path) => path,
+ });
+ expect(location.ok && location.value.msixPackage?.packageFamilyName).toBe(
+ CLAUDE_DESKTOP_PACKAGE_FAMILY.toUpperCase(),
+ );
+ });
+});
+
+describe("package-private REA launchers", () => {
+ const launcher = join(
+ env.APPDATA,
+ "npm",
+ "node_modules",
+ "rea-agents",
+ "scripts",
+ "rea.mjs",
+ );
+ const stored = join(
+ packages,
+ CLAUDE_DESKTOP_PACKAGE_FAMILY,
+ "LocalCache",
+ "Roaming",
+ "npm",
+ "node_modules",
+ "rea-agents",
+ "scripts",
+ "rea.mjs",
+ );
+
+ it("names the package that privately stores a path", () => {
+ expect(packagePrivateStorageFamily(stored, packages)).toBe(
+ CLAUDE_DESKTOP_PACKAGE_FAMILY,
+ );
+ expect(packagePrivateStorageFamily(launcher, packages)).toBeUndefined();
+ expect(
+ packagePrivateStorageFamily(
+ join(packages, CLAUDE_DESKTOP_PACKAGE_FAMILY, "LocalState", "x"),
+ packages,
+ ),
+ ).toBeUndefined();
+ });
+
+ it("blocks a launcher that only the package's processes can see", () => {
+ const blocker = packagePrivateLauncherBlocker(
+ [join("/Program Files", "nodejs", "node.exe"), launcher, "mcp"],
+ packages,
+ (path) => (path === launcher ? stored : path),
+ );
+ expect(blocker).toContain(stored);
+ expect(blocker).toContain(CLAUDE_DESKTOP_PACKAGE_FAMILY);
+ expect(blocker).toContain("npm install -g rea-agents");
+ });
+
+ it("compares the launcher with the canonical package storage path", () => {
+ const shortProfilePackages = join(
+ "/profile",
+ "A~1",
+ "AppData",
+ "Local",
+ "Packages",
+ );
+ const blocker = packagePrivateLauncherBlocker(
+ [launcher, "mcp"],
+ shortProfilePackages,
+ (path) =>
+ path === shortProfilePackages
+ ? packages
+ : path === launcher
+ ? stored
+ : path,
+ );
+ expect(blocker).toContain(CLAUDE_DESKTOP_PACKAGE_FAMILY);
+ });
+
+ it("allows ordinary and unresolvable launchers", () => {
+ expect(
+ packagePrivateLauncherBlocker(
+ ["cmd.exe", "/d", "/c", "npx", "-y", "rea-agents@6.4.0", "mcp"],
+ packages,
+ () => {
+ throw new Error("package runners are not resolved");
+ },
+ ),
+ ).toBeUndefined();
+ expect(
+ packagePrivateLauncherBlocker([launcher, "mcp"], packages, () => {
+ throw failure("ENOENT");
+ }),
+ ).toBeUndefined();
+ });
+});
+
+describe("writes from a redirected AppData view", () => {
+ const view: MsixPackageView = {
+ packageFamilyName: CLAUDE_DESKTOP_PACKAGE_FAMILY,
+ privateConfigPath: privateConfig,
+ appDataConfigPath: appDataConfig,
+ virtualizedRoots: [env.APPDATA, env.LOCALAPPDATA],
+ processView: "redirected",
+ };
+ const vscode = join(env.APPDATA, "Code", "User", "mcp.json");
+ const hermes = join(env.LOCALAPPDATA, "hermes", "config.yaml");
+ const codex = join(home, ".codex", "config.toml");
+
+ it("refuses only targets under the package's virtualized roots", () => {
+ const blocker = redirectedWriteBlocker(
+ view,
+ [vscode, codex, hermes],
+ "setup",
+ );
+ expect(blocker).toContain(`${vscode}, ${hermes}`);
+ expect(blocker).not.toContain(codex);
+ expect(blocker).toContain("Rerun setup from a terminal");
+ expect(redirectedWriteBlocker(view, [codex], "setup")).toBeUndefined();
+ });
+
+ it("compares aliases through the existing parent of a new destination", () => {
+ const alias = join(home, "skill-alias");
+ const target = join(alias, "skills", "reverse-engineer-anything");
+ const blocker = redirectedWriteBlocker(
+ view,
+ [target],
+ "uninstall",
+ (path) => {
+ if (path === alias) return join(env.APPDATA, "dsh");
+ if (path.startsWith(`${alias}${sep}`)) throw failure("ENOENT");
+ return path;
+ },
+ );
+ expect(blocker).toContain(target);
+ expect(blocker).toContain("Rerun uninstall");
+ });
+
+ it("keeps canonicalization failures visible instead of assuming a target is outside AppData", () => {
+ expect(
+ redirectedWriteBlocker(view, [codex], "setup", () => {
+ throw failure("EACCES");
+ }),
+ ).toContain("Cannot establish the MSIX write location");
+ });
+
+ it("allows every target without positive evidence of redirection", () => {
+ for (const processView of ["direct", "unknown"] as const)
+ expect(
+ redirectedWriteBlocker({ ...view, processView }, [vscode], "uninstall"),
+ ).toBeUndefined();
+ expect(
+ redirectedWriteBlocker(undefined, [vscode], "setup"),
+ ).toBeUndefined();
+ });
+});
diff --git a/src/application/ClaudeDesktopConfigLocation.ts b/src/application/ClaudeDesktopConfigLocation.ts
new file mode 100644
index 000000000..899aadbbb
--- /dev/null
+++ b/src/application/ClaudeDesktopConfigLocation.ts
@@ -0,0 +1,199 @@
+import { lstatSync, readdirSync, realpathSync } from "node:fs";
+import { join } from "node:path";
+
+import { err, ok, type Result } from "../domain/result.js";
+import {
+ windowsAppDataRoots,
+ type MsixPackageView,
+} from "./MsixPackageStorage.js";
+
+const CONFIG_FILE = "claude_desktop_config.json";
+
+/** Private files compared at most when looking for a redirected AppData view. */
+const PROCESS_VIEW_PROBES = 8;
+
+/**
+ * Claude Desktop's MSIX package family: package name `Claude` and the
+ * publisher ID Windows derives from `CN="Anthropic, PBC", O="Anthropic, PBC",
+ * L=San Francisco, S=California, C=US, …`.
+ */
+export const CLAUDE_DESKTOP_PACKAGE_FAMILY = "Claude_pzs8sxrjxfjjc";
+
+/** `Claude_` plus a 13-character Crockford base32 MSIX publisher ID. */
+const CLAUDE_PACKAGE_FAMILY = /^Claude_[0-9a-hjkmnp-tv-z]{13}$/iu;
+
+/** The configuration Claude Desktop reads and the evidence that it is installed. */
+export interface ClaudeDesktopLocation {
+ readonly configPath: string;
+ readonly markerPath: string;
+ readonly msixPackage?: MsixPackageView;
+}
+
+/** Unresolved Claude Desktop identity, reported instead of guessing a file. */
+export interface ClaudeDesktopLocationFailure {
+ readonly path: string;
+ readonly message: string;
+}
+
+/** Synchronous probes; each throws for failures other than absence. */
+export interface ClaudeDesktopLocationFileSystem {
+ readonly readdir: (path: string) => readonly string[];
+ readonly lstat: (path: string) => { isFile(): boolean };
+ readonly realpath: (path: string) => string;
+}
+
+const systemFileSystem: ClaudeDesktopLocationFileSystem = {
+ readdir: (path) => readdirSync(path),
+ lstat: (path) => lstatSync(path),
+ realpath: (path) => realpathSync.native(path),
+};
+
+/**
+ * Resolve the `claude_desktop_config.json` that Windows Claude Desktop reads.
+ * Without its MSIX package storage, that is the AppData file. With it, the
+ * package reads its private copy when one exists and the AppData file
+ * otherwise; a new registration goes to the private copy. A `Claude_*`
+ * package from another publisher leaves the reader unknown.
+ */
+export const resolveWindowsClaudeDesktopLocation = (
+ home: string,
+ env: Readonly,
+ fileSystem: ClaudeDesktopLocationFileSystem = systemFileSystem,
+): Result => {
+ const roots = windowsAppDataRoots(home, env);
+ const appDataDirectory = join(roots.roaming, "Claude");
+ const appDataConfigPath = join(appDataDirectory, CONFIG_FILE);
+ const packagesDirectory = join(roots.local, "Packages");
+ let families: readonly string[];
+ try {
+ families = fileSystem
+ .readdir(packagesDirectory)
+ .filter((name) => CLAUDE_PACKAGE_FAMILY.test(name));
+ } catch (cause: unknown) {
+ if (!isMissing(cause))
+ return err({
+ path: packagesDirectory,
+ message: `MSIX package storage ${packagesDirectory} could not be inspected, so REA cannot establish whether Claude Desktop reads ${appDataConfigPath} or a package-private copy: ${errorMessage(cause)}. Repair access to that directory, then rerun.`,
+ });
+ families = [];
+ }
+ const foreign = families.filter(
+ (name) =>
+ name.toLowerCase() !== CLAUDE_DESKTOP_PACKAGE_FAMILY.toLowerCase(),
+ );
+ if (foreign.length > 0)
+ return err({
+ path: join(packagesDirectory, foreign[0] ?? ""),
+ message: `MSIX package storage ${foreign.map((name) => join(packagesDirectory, name)).join(", ")} uses Claude Desktop's package name with an unrecognized publisher (Claude Desktop is ${CLAUDE_DESKTOP_PACKAGE_FAMILY}). REA cannot establish which ${CONFIG_FILE} that package reads and does not fall back to ${appDataConfigPath}. Remove the unrecognized package, or register REA in the configuration it reads.`,
+ });
+ const family = families[0];
+ if (family === undefined)
+ return ok({ configPath: appDataConfigPath, markerPath: appDataDirectory });
+ const container = join(packagesDirectory, family);
+ const privateDirectory = join(container, "LocalCache", "Roaming", "Claude");
+ const privateConfigPath = join(privateDirectory, CONFIG_FILE);
+ const privateCopy = present(fileSystem, privateConfigPath);
+ if (!privateCopy.ok) return privateCopy;
+ const appDataCopy = privateCopy.value
+ ? ok(false)
+ : present(fileSystem, appDataConfigPath);
+ if (!appDataCopy.ok) return appDataCopy;
+ return ok({
+ configPath:
+ privateCopy.value || !appDataCopy.value
+ ? privateConfigPath
+ : appDataConfigPath,
+ markerPath: container,
+ msixPackage: {
+ packageFamilyName: family,
+ privateConfigPath,
+ appDataConfigPath,
+ virtualizedRoots: [roots.roaming, roots.local],
+ processView: processView(
+ fileSystem,
+ privateDirectory,
+ appDataDirectory,
+ privateCopy.value,
+ ),
+ },
+ });
+};
+
+/**
+ * A process the package started sees the package's private files at their
+ * AppData paths; any other process sees the real AppData files or nothing.
+ * Claude Desktop saves its user data there while it runs, so the
+ * configuration, or any other private file, can show which view this is.
+ */
+const processView = (
+ fileSystem: ClaudeDesktopLocationFileSystem,
+ privateDirectory: string,
+ appDataDirectory: string,
+ privateConfig: boolean,
+): MsixPackageView["processView"] => {
+ let probes: readonly string[];
+ try {
+ probes = privateConfig
+ ? [CONFIG_FILE]
+ : fileSystem
+ .readdir(privateDirectory)
+ .filter((name) => {
+ try {
+ return fileSystem.lstat(join(privateDirectory, name)).isFile();
+ } catch (cause: unknown) {
+ // A candidate may vanish or become unreadable while listed.
+ void cause;
+ return false;
+ }
+ })
+ .slice(0, PROCESS_VIEW_PROBES);
+ } catch (cause: unknown) {
+ // No private file can be listed, so no view can be compared.
+ void cause;
+ return "unknown";
+ }
+ let view: MsixPackageView["processView"] = "unknown";
+ for (const name of probes) {
+ let privateTarget: string;
+ try {
+ privateTarget = fileSystem.realpath(join(privateDirectory, name));
+ } catch (cause: unknown) {
+ // A private file that vanished cannot be compared; try another.
+ void cause;
+ continue;
+ }
+ try {
+ if (fileSystem.realpath(join(appDataDirectory, name)) === privateTarget)
+ return "redirected";
+ view = "direct";
+ } catch (cause: unknown) {
+ if (!isMissing(cause)) return "unknown";
+ view = "direct";
+ }
+ }
+ return view;
+};
+
+const present = (
+ fileSystem: ClaudeDesktopLocationFileSystem,
+ path: string,
+): Result => {
+ try {
+ fileSystem.lstat(path);
+ return ok(true);
+ } catch (cause: unknown) {
+ if (isMissing(cause)) return ok(false);
+ return err({
+ path,
+ message: `${path} could not be inspected, so REA cannot establish which ${CONFIG_FILE} Claude Desktop's MSIX package reads: ${errorMessage(cause)}. Repair access to that path, then rerun.`,
+ });
+ }
+};
+
+const isMissing = (cause: unknown): boolean =>
+ cause instanceof Error &&
+ "code" in cause &&
+ (cause.code === "ENOENT" || cause.code === "ENOTDIR");
+
+const errorMessage = (cause: unknown): string =>
+ cause instanceof Error ? cause.message : String(cause);
diff --git a/src/application/ClientConfigurationDocument.ts b/src/application/ClientConfigurationDocument.ts
index 1b241a98b..a226f8cb6 100644
--- a/src/application/ClientConfigurationDocument.ts
+++ b/src/application/ClientConfigurationDocument.ts
@@ -110,9 +110,12 @@ const parseDocument = (
return requireObject(parseToml(text), "The configuration root");
// Pi loads `mcp.json` with a strict `JSON.parse` and no BOM tolerance, so
// comments, trailing commas, a BOM, or an empty existing file must be
- // rejected instead of registering into a document Pi cannot load.
- if (format === "pi")
+ // rejected instead of registering into a document the client cannot load.
+ // Kimi also reads mcp.json with JSON.parse and an empty-file fallback.
+ if (format === "pi" || format === "kimi") {
+ if (format === "kimi" && text.trim().length === 0) return {};
return requireObject(JSON.parse(text), "The configuration root");
+ }
// An empty file, which some clients create before any server is added,
// holds no settings to preserve.
if (isEmptyClientConfigurationText(text)) return {};
@@ -1446,6 +1449,13 @@ export const clientRegistrationEntry = (
? {}
: { environment: { ...environment } }),
};
+ case "kimi":
+ // Kimi infers stdio from command; it uses `transport`, not `type`.
+ return {
+ command: executable,
+ args,
+ ...(Object.keys(environment).length === 0 ? {} : { env: environment }),
+ };
case "opencode_v2":
// V2 connects servers unless `disabled` is true.
return {
diff --git a/src/application/ClientRegistrationStatus.ts b/src/application/ClientRegistrationStatus.ts
index caeba4fb6..56a3e81ba 100644
--- a/src/application/ClientRegistrationStatus.ts
+++ b/src/application/ClientRegistrationStatus.ts
@@ -66,6 +66,8 @@ const registrationSchema = z
command: z.string().min(1),
args: z.array(z.string()).default([]),
startup_timeout_sec: z.number().positive().optional(),
+ startupTimeoutMs: z.number().positive().optional(),
+ toolTimeoutMs: z.number().positive().optional(),
type: z.string().optional(),
transport: z.string().optional(),
serverName: z.string().optional(),
@@ -155,7 +157,7 @@ export const readClientRegistrationInspections = async (
}
const parsed = effectiveClientConfiguration(files.value);
if (parsed === undefined) {
- record(unavailableStatus(client.name, client.configPath, "missing"));
+ record(unavailableStatus(client, "missing"));
continue;
}
const policy = await readClientPolicyBlock(
@@ -191,7 +193,7 @@ export const readClientRegistrationInspections = async (
});
continue;
}
- record(unavailableStatus(client.name, client.configPath, "missing"));
+ record(unavailableStatus(client, "missing"));
continue;
}
const registration = parseRegistration(raw, client);
@@ -202,8 +204,7 @@ export const readClientRegistrationInspections = async (
const activation = registrationActivation(registration, client, parsed);
record(
configuredStatus(
- client.name,
- client.configPath,
+ client,
command,
activation === "enabled" &&
registrationAligned(
@@ -222,11 +223,7 @@ export const readClientRegistrationInspections = async (
);
} catch (cause: unknown) {
record(
- unavailableStatus(
- client.name,
- client.configPath,
- isMissing(cause) ? "missing" : "invalid",
- ),
+ unavailableStatus(client, isMissing(cause) ? "missing" : "invalid"),
);
}
}
@@ -263,6 +260,21 @@ const registrationAligned = (
MCP_STARTUP_POLICY.codexStartupTimeoutSeconds
)
return false;
+ if (
+ client.name === "kimi" &&
+ (registration.startupTimeoutMs !==
+ MCP_STARTUP_POLICY.kimiStartupTimeoutMs ||
+ registration.toolTimeoutMs !== MCP_STARTUP_POLICY.kimiToolTimeoutMs)
+ )
+ return false;
+ if (
+ (client.format === "kimi" ||
+ client.format === "omp" ||
+ client.format === "pi") &&
+ registration.type !== undefined &&
+ registration.type !== "stdio"
+ )
+ return false;
if (client.format === "vscode" && registration.type !== "stdio") return false;
// A Harness registration only connects as the rea stdio server; another
// server name or transport leaves the session without REA tools.
@@ -271,13 +283,6 @@ const registrationAligned = (
(registration.serverName !== "rea" || registration.transport !== "stdio")
)
return false;
- // OMP and Pi infer stdio for a command entry without an explicit type.
- if (
- (client.format === "omp" || client.format === "pi") &&
- registration.type !== undefined &&
- registration.type !== "stdio"
- )
- return false;
if (
client.format === "copilot_cli" &&
(registration.type !== "stdio" ||
@@ -336,6 +341,12 @@ const parseRegistration = (
});
}
const registration = registrationSchema.parse(value);
+ if (
+ client.format === "kimi" &&
+ registration.transport !== undefined &&
+ registration.transport !== "stdio"
+ )
+ throw new TypeError("Expected an stdio registration");
if (
(client.format === "vscode" || client.format === "copilot_cli") &&
registration.type !== "stdio"
@@ -344,7 +355,9 @@ const parseRegistration = (
if (client.format === "commandcode" && registration.transport !== "stdio")
throw new TypeError("Expected an stdio registration");
if (
- (client.format === "omp" || client.format === "pi") &&
+ (client.format === "kimi" ||
+ client.format === "omp" ||
+ client.format === "pi") &&
registration.type !== undefined &&
registration.type !== "stdio"
)
@@ -352,36 +365,45 @@ const parseRegistration = (
return registration;
};
-const remediation =
- "Run rea setup to refresh this registration, then restart the client.";
+/** A client that rewrites its file from memory must be quit before setup. */
+const remediation = (client: SetupClient): string => {
+ if (client.quitBeforeSetup !== true)
+ return "Run rea setup to refresh this registration, then restart the client.";
+ const name = client.displayName ?? client.name;
+ return `Quit ${name} fully, run rea setup to refresh this registration, then start ${name} again. A running ${name} can rewrite its configuration from memory and drop the registration.`;
+};
const configuredStatus = (
- client: string,
- configPath: string,
+ client: SetupClient,
command: RegistrationCommand,
state: "aligned" | "stale",
policyRemediation?: string,
): ClientRegistrationStatus =>
state === "aligned"
- ? { client, config_path: configPath, command, state, remediation: null }
- : {
- client,
- config_path: configPath,
+ ? {
+ client: client.name,
+ config_path: client.configPath,
command,
state,
- remediation: policyRemediation ?? remediation,
+ remediation: null,
+ }
+ : {
+ client: client.name,
+ config_path: client.configPath,
+ command,
+ state,
+ remediation: policyRemediation ?? remediation(client),
};
const unavailableStatus = (
- client: string,
- configPath: string,
+ client: SetupClient,
state: "missing" | "invalid",
): ClientRegistrationStatus => ({
- client,
- config_path: configPath,
+ client: client.name,
+ config_path: client.configPath,
command: [],
state,
- remediation,
+ remediation: remediation(client),
});
const clientEvidencePresent = async (client: SetupClient): Promise => {
diff --git a/src/application/DirectAnalysis.ts b/src/application/DirectAnalysis.ts
index 0bdb69864..8a7874598 100644
--- a/src/application/DirectAnalysis.ts
+++ b/src/application/DirectAnalysis.ts
@@ -60,6 +60,7 @@ type DirectAnalysisTool =
| "annotate_native_function"
| "inspect_native_load_image"
| "read_bytes"
+ | "search_bytes"
| "address_to_file_offset"
| "binary_overview"
| "procedure_pseudo_code"
diff --git a/src/application/Doctor.test.ts b/src/application/Doctor.test.ts
index 4251bbaf2..a39f81e35 100644
--- a/src/application/Doctor.test.ts
+++ b/src/application/Doctor.test.ts
@@ -74,7 +74,7 @@ describe("doctor", () => {
),
).toEqual({
node: `Install ${SUPPORTED_NODE_VERSION_PROSE}.`,
- host: "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, Nobara 44+, 64-bit Arch Linux, CachyOS, Omarchy, and the experimental Windows x64 Ghidra P0 boundary.",
+ host: "REA supports macOS 12+, Linux distributions with a recognized apt, dnf, or pacman package family, and the experimental Windows x64 Ghidra P0 boundary; automated Hopper setup additionally requires Ubuntu 24.04+, Fedora 41+, Nobara 44+, 64-bit Arch Linux, CachyOS, or Omarchy.",
hopper: "Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.",
target: "Supply a readable local app or program path.",
});
@@ -418,10 +418,9 @@ describe("doctor scoped readiness", () => {
platform: "linux",
linuxDistribution: () =>
Promise.resolve({
- id: "debian",
- versionId: "13",
- packageFamily: "deb",
- supported: false,
+ id: "nixos",
+ packageFamily: undefined,
+ hopperSetupSupported: false,
}),
clientRegistrations: () =>
Promise.resolve([
@@ -451,6 +450,31 @@ describe("doctor scoped readiness", () => {
);
});
+ it("accepts a Debian-family host that has no tested Hopper package", async () => {
+ const result = await runDoctor(
+ undefined,
+ host({
+ platform: "linux",
+ macosVersion: () => Promise.resolve(undefined),
+ linuxDistribution: () =>
+ Promise.resolve({
+ id: "kali",
+ versionId: "2026.3",
+ packageFamily: "deb",
+ hopperSetupSupported: false,
+ }),
+ }),
+ );
+
+ expect(result.checks).toContainEqual(
+ expect.objectContaining({
+ name: "host",
+ ok: true,
+ detail: "kali 2026.3",
+ }),
+ );
+ });
+
it("requires explicitly selected providers instead of any available provider", async () => {
const ghidra = {
id: "ghidra",
@@ -502,7 +526,7 @@ describe("doctor Linux and Hopper discovery", () => {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
- supported: true,
+ hopperSetupSupported: true,
}),
configuredHopperPath: path,
executable: (candidate) => Promise.resolve(candidate === path),
@@ -522,7 +546,7 @@ describe("doctor Linux and Hopper discovery", () => {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
- supported: true,
+ hopperSetupSupported: true,
}),
linuxDemoRuntimeCheck: () =>
Promise.resolve({
@@ -554,7 +578,7 @@ describe("doctor Linux and Hopper discovery", () => {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
- supported: true,
+ hopperSetupSupported: true,
}),
executable: (candidate) => Promise.resolve(candidate === path),
supportedLinuxHopper: () => Promise.resolve(false),
diff --git a/src/application/DoctorDiagnostics.ts b/src/application/DoctorDiagnostics.ts
index 354d08636..1d8a5e621 100644
--- a/src/application/DoctorDiagnostics.ts
+++ b/src/application/DoctorDiagnostics.ts
@@ -2,7 +2,7 @@ import {
SUPPORTED_NODE_VERSION_PROSE,
supportsNodeVersion,
} from "../domain/runtimeVersion.js";
-import { linuxHopperLauncherPath } from "./LinuxHopper.js";
+import { linuxHopperLauncherPath, supportsReaHost } from "./LinuxHopper.js";
import type {
DoctorCheck,
DoctorHost,
@@ -129,7 +129,7 @@ const hostCheck = async (host: DoctorHost): Promise => {
host.platform === "linux" ? await host.linuxDistribution() : undefined;
const supported =
(host.platform === "darwin" && parseMajor(macosVersion) >= 12) ||
- (host.platform === "linux" && linuxDistribution?.supported === true) ||
+ (host.platform === "linux" && supportsReaHost(linuxDistribution)) ||
(host.platform === "win32" && host.architecture === "x64");
const detail =
macosVersion ??
@@ -138,7 +138,7 @@ const hostCheck = async (host: DoctorHost): Promise => {
: `${linuxDistribution.id} ${linuxDistribution.versionId ?? "unknown"}`);
return check("host", supported, detail, {
remediation:
- "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, Nobara 44+, 64-bit Arch Linux, CachyOS, Omarchy, and the experimental Windows x64 Ghidra P0 boundary.",
+ "REA supports macOS 12+, Linux distributions with a recognized apt, dnf, or pacman package family, and the experimental Windows x64 Ghidra P0 boundary; automated Hopper setup additionally requires Ubuntu 24.04+, Fedora 41+, Nobara 44+, 64-bit Arch Linux, CachyOS, or Omarchy.",
classification: "unsupported_host",
});
};
diff --git a/src/application/DshCordisPatch.ts b/src/application/DshCordisPatch.ts
index 5e0fc10ec..0e34da23e 100644
--- a/src/application/DshCordisPatch.ts
+++ b/src/application/DshCordisPatch.ts
@@ -8,6 +8,7 @@ import { CordisExpression, DshCordisYaml } from "./DshCordisYaml.js";
export const DSH_ENTRY_ID = "rea-mcp";
export const DSH_CLIENT_PACKAGE = "@deepseek-ai/dsh-mcp-client";
const LAUNCHER_KEYS = new Set(["serverName", "transport", "command", "args"]);
+const SELECTOR_KEYS = new Set(["id", "name"]);
const isRecord = (value: unknown): value is Record =>
typeof value === "object" &&
value !== null &&
@@ -295,10 +296,15 @@ export class DshCordisDocument {
carried = true;
return [this.#entry(desired, invalidated)] as typeof inserted.items;
});
- replacements.set(
- op.source,
- inserted.items.length === 0 ? undefined : copy,
- );
+ if (inserted.items.length === 0) copy.delete("insert");
+ // An emptied operation keeps caller fields unless it would patch REA's row.
+ const kept =
+ inserted.items.length > 0 ||
+ (this.#id(op.map) !== DSH_ENTRY_ID &&
+ copy.items.some(
+ (pair) => !SELECTOR_KEYS.has(this.#yaml.value(pair.key) as string),
+ ));
+ replacements.set(op.source, kept ? copy : undefined);
}
const output = this.#yaml.document.clone();
const outputRoot = output.contents;
diff --git a/src/application/EvidenceBundleCommands.ts b/src/application/EvidenceBundleCommands.ts
index a72d17cb5..1255ea68f 100644
--- a/src/application/EvidenceBundleCommands.ts
+++ b/src/application/EvidenceBundleCommands.ts
@@ -11,6 +11,7 @@ import { compareBundles } from "../domain/bundleComparison.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import {
readEvidenceBundle,
+ readImmutableEvidenceBundle,
writeEvidenceBundle,
} from "./EvidenceBundleFiles.js";
@@ -33,7 +34,8 @@ export const exportEvidenceBundleCommand = async (
outputPath: string,
overwrite: boolean,
): Promise> => {
- const loaded = await readEvidenceBundle(sourcePath);
+ // The sealed snapshot lets the writer reuse this authentication.
+ const loaded = await readImmutableEvidenceBundle(sourcePath);
if (!loaded.ok) return loaded;
const resources = new ArtifactResourceScope();
const written = await writeEvidenceBundle(
@@ -69,9 +71,11 @@ export const compareEvidenceBundlesCommand = async (input: {
readonly leftPath: string;
readonly rightPath: string;
}): Promise> => {
+ // Sealed snapshots let comparison and its bundle digests reuse this
+ // authentication instead of re-validating every record twice more.
const [left, right] = await Promise.all([
- readEvidenceBundle(input.leftPath),
- readEvidenceBundle(input.rightPath),
+ readImmutableEvidenceBundle(input.leftPath),
+ readImmutableEvidenceBundle(input.rightPath),
]);
if (!left.ok) return left;
if (!right.ok) return right;
diff --git a/src/application/LinuxHopper.test.ts b/src/application/LinuxHopper.test.ts
index b5cf2d38f..1c37a03ea 100644
--- a/src/application/LinuxHopper.test.ts
+++ b/src/application/LinuxHopper.test.ts
@@ -7,6 +7,7 @@ import {
linuxPackageManagerCommands,
linuxSharedLibrariesAvailable,
parseLinuxDistribution,
+ supportsReaHost,
type LinuxDistribution,
type LinuxHopperDownload,
type LinuxHopperInstallHost,
@@ -19,7 +20,7 @@ class RecordingLinuxHost implements LinuxHopperInstallHost {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
- supported: true,
+ hopperSetupSupported: true,
};
archive = new Uint8Array([1, 2, 3]);
archiveOk = true;
@@ -66,7 +67,7 @@ describe("Linux Hopper host classification", () => {
(document, family) => {
expect(parseLinuxDistribution(document)).toMatchObject({
packageFamily: family,
- supported: true,
+ hopperSetupSupported: true,
});
},
);
@@ -99,8 +100,32 @@ describe("Linux Hopper host classification", () => {
"ID=garuda\nID_LIKE=arch\n",
"ID=omarchy\n",
"ID=omarchy\nID_LIKE=debian\n",
- ])("rejects unsupported vendor/version combinations", (document) => {
- expect(parseLinuxDistribution(document).supported).toBe(false);
+ ])(
+ "rejects vendor/version combinations without a tested Hopper installer",
+ (document) => {
+ expect(parseLinuxDistribution(document).hopperSetupSupported).toBe(false);
+ },
+ );
+
+ it.each([
+ 'ID=kali\nVERSION_ID="2026.3"\nID_LIKE=debian\n',
+ 'ID=debian\nVERSION_ID="13"\nID_LIKE=debian\n',
+ 'ID=linuxmint\nVERSION_ID="22.1"\nID_LIKE="ubuntu debian"\n',
+ "ID=manjaro\nID_LIKE=arch\n",
+ "ID=derivative\nID_LIKE='debian'\n",
+ "ID='debian'\nVERSION_ID='13'\n",
+ ])(
+ "hosts REA on a distribution Hopper has no tested package for: %s",
+ (document) => {
+ const distribution = parseLinuxDistribution(document);
+ expect(distribution.hopperSetupSupported).toBe(false);
+ expect(supportsReaHost(distribution)).toBe(true);
+ },
+ );
+
+ it("withholds host support from a distribution with no package family", () => {
+ expect(supportsReaHost(parseLinuxDistribution("ID=nixos\n"))).toBe(false);
+ expect(supportsReaHost(undefined)).toBe(false);
});
});
@@ -166,7 +191,7 @@ describe("Linux Hopper installation", () => {
host.distributionValue = {
id: family,
packageFamily: family,
- supported: true,
+ hopperSetupSupported: true,
};
const result = await installFixture(host);
expect(result).toEqual({
@@ -194,7 +219,7 @@ describe("Linux Hopper installation", () => {
host.distributionValue = {
id: "debian",
packageFamily: "deb",
- supported: false,
+ hopperSetupSupported: false,
};
if (scenario === "package request failure") host.archiveOk = false;
if (scenario === "checksum mismatch") host.archive = new Uint8Array([9]);
diff --git a/src/application/LinuxHopper.ts b/src/application/LinuxHopper.ts
index 3a46106bb..765f31d29 100644
--- a/src/application/LinuxHopper.ts
+++ b/src/application/LinuxHopper.ts
@@ -76,19 +76,35 @@ interface LinuxDistributionIdentity {
readonly versionId?: string;
}
-/** Parsed Linux distribution information used for support and package selection. */
+/**
+ * Parsed Linux distribution information used for package selection.
+ *
+ * `hopperSetupSupported` is the automated Hopper installer's tested list. It is
+ * narrower than the hosts where REA itself runs, which {@link supportsReaHost}
+ * answers from the package family.
+ */
export type LinuxDistribution = LinuxDistributionIdentity &
(
| {
readonly packageFamily: LinuxPackageFamily;
- readonly supported: true;
+ readonly hopperSetupSupported: true;
}
| {
readonly packageFamily: LinuxPackageFamily | undefined;
- readonly supported: false;
+ readonly hopperSetupSupported: false;
}
);
+/**
+ * Whether REA's analysis providers run on this distribution. A recognized
+ * package family means a glibc host with a known package layout, which is what
+ * the providers need; downloading and installing the official Hopper package
+ * additionally requires a distribution on its tested list.
+ */
+export const supportsReaHost = (
+ distribution: LinuxDistribution | undefined,
+): boolean => distribution?.packageFamily !== undefined;
+
/** Download response kept independent of Node's global fetch implementation. */
export interface LinuxHopperDownload {
readonly ok: boolean;
@@ -141,16 +157,19 @@ export const parseLinuxDistribution = (text: string): LinuxDistribution => {
const key = match[1];
const encoded = match[2];
if (key === undefined || encoded === undefined) continue;
- const value =
- encoded.startsWith('"') && encoded.endsWith('"')
- ? encoded.slice(1, -1).replaceAll('\\"', '"').replaceAll("\\\\", "\\")
- : encoded;
+ const quote = encoded[0];
+ const quoted = (quote === "'" || quote === '"') && encoded.endsWith(quote);
+ const value = !quoted
+ ? encoded
+ : quote === "'"
+ ? encoded.slice(1, -1)
+ : encoded.slice(1, -1).replace(/\\(["\\$`])/gu, "$1");
fields.set(key, value);
}
const id = (fields.get("ID") ?? "unknown").toLowerCase();
const versionId = fields.get("VERSION_ID");
const packageFamily = packageFamilyFor(id, fields.get("ID_LIKE"));
- const supported =
+ const hopperSetupSupported =
packageFamily !== undefined &&
(id === "arch" ||
id === "cachyos" ||
@@ -162,9 +181,9 @@ export const parseLinuxDistribution = (text: string): LinuxDistribution => {
id,
...(versionId === undefined ? {} : { versionId }),
};
- return supported
- ? { ...identity, packageFamily, supported: true }
- : { ...identity, packageFamily, supported: false };
+ return hopperSetupSupported
+ ? { ...identity, packageFamily, hopperSetupSupported: true }
+ : { ...identity, packageFamily, hopperSetupSupported: false };
};
/** Read and classify the current Linux distribution. */
@@ -193,7 +212,7 @@ export const installLinuxHopper = async (
if (options.signal?.aborted === true)
return { status: "failed", reason: "cancelled" };
const distribution = await host.distribution();
- if (distribution?.supported !== true)
+ if (distribution?.hopperSetupSupported !== true)
return { status: "failed", reason: "unsupported_host" };
let temporary: string | undefined;
try {
diff --git a/src/application/MsixPackageStorage.ts b/src/application/MsixPackageStorage.ts
new file mode 100644
index 000000000..098b2f1db
--- /dev/null
+++ b/src/application/MsixPackageStorage.ts
@@ -0,0 +1,187 @@
+import { lstat } from "node:fs/promises";
+import { realpathSync } from "node:fs";
+import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
+import { err, ok, type Result } from "../domain/result.js";
+
+/**
+ * How one MSIX-packaged client reads its AppData configuration. A package
+ * with write virtualization saves AppData files to its private
+ * `LocalCache\Roaming` copy, then reads that copy whenever it exists and the
+ * ordinary AppData file otherwise.
+ */
+export interface MsixPackageView {
+ readonly packageFamilyName: string;
+ /** The package-private copy, which shadows the AppData file once it exists. */
+ readonly privateConfigPath: string;
+ /** The AppData path that unpackaged processes read and write. */
+ readonly appDataConfigPath: string;
+ /** Roots whose writes the package redirects for processes it starts. */
+ readonly virtualizedRoots: readonly string[];
+ /**
+ * `redirected`: this process sees one of the package's private files at its
+ * AppData path, so the running package started it. `direct`: it does not.
+ * `unknown`: no private file could be compared.
+ */
+ readonly processView: "redirected" | "direct" | "unknown";
+}
+
+/** Windows' roaming and local application data roots; empty means unset. */
+export const windowsAppDataRoots = (
+ home: string,
+ env: Readonly,
+): { readonly roaming: string; readonly local: string } => ({
+ roaming: env.APPDATA?.trim() || join(home, "AppData", "Roaming"),
+ local: env.LOCALAPPDATA?.trim() || join(home, "AppData", "Local"),
+});
+
+/** The per-user root of MSIX package data containers. */
+export const msixPackagesDirectory = (
+ home: string,
+ env: Readonly,
+): string => join(windowsAppDataRoots(home, env).local, "Packages");
+
+/** Whether `path` is `root` or inside it, using the host's path comparison. */
+const pathWithin = (path: string, root: string): boolean => {
+ const inner = relative(root, path);
+ return (
+ inner === "" ||
+ (inner !== ".." && !inner.startsWith(`..${sep}`) && !isAbsolute(inner))
+ );
+};
+
+/**
+ * Name the package whose private `LocalCache` holds a canonical path. Its
+ * processes see those files at virtual AppData locations; other processes do
+ * not, and Windows removes them with the package.
+ */
+export const packagePrivateStorageFamily = (
+ canonicalPath: string,
+ packagesDirectory: string,
+): string | undefined => {
+ if (!pathWithin(canonicalPath, packagesDirectory)) return undefined;
+ const [family, storage] = relative(packagesDirectory, canonicalPath).split(
+ /[\\/]/u,
+ );
+ return family !== undefined &&
+ family !== "" &&
+ storage?.toLowerCase() === "localcache"
+ ? family
+ : undefined;
+};
+
+/**
+ * Explain why setup must not register a launcher that exists only in one
+ * package's private storage. Other clients could not start it, and doctor
+ * outside that package could not verify it.
+ */
+export const packagePrivateLauncherBlocker = (
+ command: readonly string[],
+ packagesDirectory: string,
+ canonicalPath: (path: string) => string = realpathSync.native,
+): string | undefined => {
+ // Compare canonical forms: a short-name, junctioned or substituted profile
+ // spells the same directory differently.
+ let packages = packagesDirectory;
+ try {
+ packages = canonicalPath(packagesDirectory);
+ } catch (cause: unknown) {
+ // Without package storage, the spelled path is the only comparison.
+ void cause;
+ }
+ for (const part of command) {
+ if (!isAbsolute(part)) continue;
+ let resolved: string;
+ try {
+ resolved = canonicalPath(part);
+ } catch (cause: unknown) {
+ // A launcher that cannot be resolved is not evidence of package storage.
+ void cause;
+ continue;
+ }
+ const family = packagePrivateStorageFamily(resolved, packages);
+ if (family !== undefined)
+ return `The REA launcher ${part} is stored at ${resolved}, inside the private storage of MSIX package ${family}. Only processes started by that app can see it at ${part}, and Windows removes it with the package, so other clients could not start this registration. Install REA from a terminal that the app did not start (for example with \`npm install -g rea-agents\`), then rerun setup from that terminal.`;
+ }
+ return undefined;
+};
+
+/**
+ * Explain why this process must not write `targets`: the running package
+ * started it, so writes under its virtualized roots land in that package's
+ * private storage, which the targets' owners never read.
+ */
+const canonicalDestination = (
+ path: string,
+ canonicalPath: (path: string) => string,
+): Result => {
+ let ancestor = path;
+ const missing: string[] = [];
+ while (true) {
+ try {
+ return ok(join(canonicalPath(ancestor), ...missing));
+ } catch (cause: unknown) {
+ if (
+ cause instanceof Error &&
+ "code" in cause &&
+ (cause.code === "ENOENT" || cause.code === "ENOTDIR") &&
+ dirname(ancestor) !== ancestor
+ ) {
+ missing.unshift(basename(ancestor));
+ ancestor = dirname(ancestor);
+ continue;
+ }
+ return err({
+ path,
+ detail: cause instanceof Error ? cause.message : String(cause),
+ });
+ }
+ }
+};
+
+export const redirectedWriteBlocker = (
+ view: MsixPackageView | undefined,
+ targets: readonly string[],
+ operation: "setup" | "uninstall",
+ canonicalPath: (path: string) => string = realpathSync.native,
+): string | undefined => {
+ if (view?.processView !== "redirected" || targets.length === 0)
+ return undefined;
+ const roots: string[] = [];
+ const redirected: string[] = [];
+ for (const root of view.virtualizedRoots) {
+ const resolved = canonicalDestination(root, canonicalPath);
+ if (!resolved.ok)
+ return `Cannot establish the MSIX write location for ${resolved.error.path}: ${resolved.error.detail}. Rerun ${operation} from a terminal that this app did not start.`;
+ roots.push(resolved.value);
+ }
+ for (const target of targets) {
+ const resolved = canonicalDestination(target, canonicalPath);
+ if (!resolved.ok)
+ return `Cannot establish the MSIX write location for ${resolved.error.path}: ${resolved.error.detail}. Rerun ${operation} from a terminal that this app did not start.`;
+ if (roots.some((root) => pathWithin(resolved.value, root)))
+ redirected.push(target);
+ }
+ if (redirected.length === 0) return undefined;
+ return `This process was started by MSIX package ${view.packageFamilyName}, which redirects its writes under ${view.virtualizedRoots.join(" and ")} into the package's private storage. Changes to ${redirected.join(", ")} would not reach the files other apps read. Rerun ${operation} from a terminal that this app did not start.`;
+};
+
+/**
+ * Whether the package began reading its private copy after the AppData file
+ * was chosen, for example by saving its settings when it quit.
+ */
+export const packageReadTargetChanged = async (
+ view: MsixPackageView,
+ configPath: string,
+): Promise => {
+ if (configPath !== view.appDataConfigPath) return false;
+ try {
+ await lstat(view.privateConfigPath);
+ return true;
+ } catch (cause: unknown) {
+ return !(
+ cause instanceof Error &&
+ "code" in cause &&
+ (cause.code === "ENOENT" || cause.code === "ENOTDIR")
+ );
+ }
+};
diff --git a/src/application/ProviderOperationExecution.ts b/src/application/ProviderOperationExecution.ts
index bf4ad4b0e..966a43f29 100644
--- a/src/application/ProviderOperationExecution.ts
+++ b/src/application/ProviderOperationExecution.ts
@@ -43,7 +43,7 @@ export const executeProviderOperation = async <
provider: ProviderOperationPort,
confidence: "observed" | "derived",
): Promise> => {
- if (options?.signal?.aborted === true)
+ if (options?.signal?.aborted)
return err(new AnalysisCancelledError(operation));
const request = schema.safeParse({ operation, input });
if (!request.success)
@@ -51,8 +51,10 @@ export const executeProviderOperation = async <
const executed = await provider.execute(request.data, options);
if (!executed.ok) return executed;
const execution = executed.value;
- return ok(
- createEvidence(execution.subject ?? undefined, execution.provider, {
+ const evidence = createEvidence(
+ execution.subject ?? undefined,
+ execution.provider,
+ {
operation,
parameters: jsonObjectSchema.parse(request.data.input),
result: execution.result,
@@ -60,6 +62,16 @@ export const executeProviderOperation = async <
limitations: execution.limitations,
locations: execution.locations,
confidence,
- }),
+ },
);
+ // A provider that cannot observe the signal may finish its work after the
+ // caller cancelled, so re-check rather than reporting a completed operation
+ // the caller no longer wants. The observation it completed is still this
+ // provider's Evidence, so the cancellation carries it as the partial
+ // observation instead of discarding a finished result.
+ if (options?.signal?.aborted)
+ return err(
+ new AnalysisCancelledError(operation, { partialObservation: evidence }),
+ );
+ return ok(evidence);
};
diff --git a/src/application/Setup.configuration.test.ts b/src/application/Setup.configuration.test.ts
index 4ba11f622..a3f46ed6f 100644
--- a/src/application/Setup.configuration.test.ts
+++ b/src/application/Setup.configuration.test.ts
@@ -1,9 +1,43 @@
import { describe, expect, it } from "vitest";
-import { resolve } from "node:path";
+import { join, resolve } from "node:path";
import { FakeSetupHost, options } from "./Setup.fixture.js";
import { runSetup } from "./Setup.js";
import { setupRegistrationCommand } from "./SetupHost.js";
+import type { MsixPackageView } from "./MsixPackageStorage.js";
+import type { SetupClient } from "./SupportedClients.js";
+
+// Host joins keep containment checks meaningful on POSIX test hosts too.
+const PROFILE = String.raw`C:\Users\a`;
+const ROAMING = join(PROFILE, "AppData", "Roaming");
+const LOCAL = join(PROFILE, "AppData", "Local");
+const PRIVATE_CONFIG = join(
+ LOCAL,
+ "Packages",
+ "Claude_pzs8sxrjxfjjc",
+ "LocalCache",
+ "Roaming",
+ "Claude",
+ "claude_desktop_config.json",
+);
+const APPDATA_CONFIG = join(ROAMING, "Claude", "claude_desktop_config.json");
+
+/** Claude Desktop as setup resolves it from its MSIX package storage. */
+const msixClaudeDesktop = (
+ processView: MsixPackageView["processView"],
+): SetupClient => ({
+ name: "claude_desktop",
+ displayName: "Claude Desktop",
+ configPath: PRIVATE_CONFIG,
+ quitBeforeSetup: true,
+ msixPackage: {
+ packageFamilyName: "Claude_pzs8sxrjxfjjc",
+ privateConfigPath: PRIVATE_CONFIG,
+ appDataConfigPath: APPDATA_CONFIG,
+ virtualizedRoots: [ROAMING, LOCAL],
+ processView,
+ },
+});
describe("setup workflow", () => {
it("omits an aligned managed skill from an otherwise empty plan", async () => {
@@ -23,7 +57,7 @@ describe("setup workflow", () => {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
- supported: true,
+ hopperSetupSupported: true,
};
host.hopper = "/opt/hopper/bin/Hopper";
host.linuxDemoRuntimeMissing = true;
@@ -45,7 +79,7 @@ describe("setup workflow", () => {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
- supported: true,
+ hopperSetupSupported: true,
};
host.hopper = "/custom/Hopper";
host.unsupportedHopperVersion = true;
@@ -293,3 +327,85 @@ describe("setup workflow action selection", () => {
expect(result.remediation).not.toContain("internal_client_key");
});
});
+
+describe("MSIX Claude Desktop setup", () => {
+ it("discloses which file MSIX Claude Desktop reads and that it must be quit", async () => {
+ const host = new FakeSetupHost("win32");
+ host.hopper = join(PROFILE, "Hopper");
+ host.skill = "unchanged";
+ host.clients = [msixClaudeDesktop("direct")];
+
+ const result = await runSetup(
+ { ...options(false), clientIds: ["claude_desktop"], dryRun: true },
+ host,
+ );
+
+ expect(result.status).toBe("planned");
+ const [action] = result.plannedActions;
+ expect(action?.target).toBe(PRIVATE_CONFIG);
+ expect(action?.detail).toContain(
+ `Claude Desktop is installed as MSIX package Claude_pzs8sxrjxfjjc, which reads this package-private file instead of ${APPDATA_CONFIG}.`,
+ );
+ expect(action?.detail).toContain(
+ "Quit Claude Desktop fully before approving; closing its window is not enough.",
+ );
+ });
+
+ it("refuses AppData writes from a process the MSIX package started", async () => {
+ const host = new FakeSetupHost("win32");
+ host.hopper = join(PROFILE, "Hopper");
+ host.skill = "unchanged";
+ const vscode = join(ROAMING, "Code", "User", "mcp.json");
+ host.clients = [
+ msixClaudeDesktop("redirected"),
+ { name: "vscode", displayName: "VS Code", configPath: vscode },
+ {
+ name: "codex",
+ displayName: "Codex",
+ configPath: join(PROFILE, ".codex", "config.toml"),
+ },
+ ];
+
+ const blocked = await runSetup(
+ { ...options(true), clientIds: ["vscode", "codex"] },
+ host,
+ );
+ expect(blocked.status).toBe("needs_human");
+ expect(blocked.remediation).toContain(
+ `Changes to ${vscode} would not reach the files other apps read.`,
+ );
+ expect(host.configurations).toBe(0);
+
+ const homeOnly = await runSetup(
+ { ...options(true), clientIds: ["codex"] },
+ host,
+ );
+ expect(homeOnly.status).toBe("ready");
+ expect(host.configurations).toBe(1);
+ });
+
+ it("refuses registrations whose launcher only one MSIX package can see", async () => {
+ const host = new FakeSetupHost("win32");
+ host.hopper = join(PROFILE, "Hopper");
+ host.registrationCommandBlocker = "Install REA outside the package.";
+ host.clients = [
+ { name: "codex", displayName: "Codex", configPath: "/codex.toml" },
+ ];
+
+ const blocked = await runSetup(
+ { ...options(true), clientIds: ["codex"] },
+ host,
+ );
+ expect(blocked.status).toBe("needs_human");
+ expect(blocked.remediation).toBe("Install REA outside the package.");
+ expect(host.configurations).toBe(0);
+ expect(host.skillInstalls).toBe(0);
+
+ const skillOnly = await runSetup(
+ { ...options(true), clientIds: [], installSkill: true },
+ host,
+ );
+ expect(skillOnly.status).toBe("ready");
+ expect(host.skillInstalls).toBe(1);
+ });
+});
diff --git a/src/application/Setup.fixture.ts b/src/application/Setup.fixture.ts
index a99897494..3c7c21d35 100644
--- a/src/application/Setup.fixture.ts
+++ b/src/application/Setup.fixture.ts
@@ -25,6 +25,7 @@ export class FakeSetupHost implements SetupHost {
return skillDestinations(this.homeDirectory, clientIds);
}
readonly registrationCommand: readonly string[];
+ registrationCommandBlocker?: string;
nodeVersion = "24.18.0";
version: string | undefined = "14.5";
distribution: LinuxDistribution | undefined;
diff --git a/src/application/Setup.planning.test.ts b/src/application/Setup.planning.test.ts
index d1ca6baea..2548772c9 100644
--- a/src/application/Setup.planning.test.ts
+++ b/src/application/Setup.planning.test.ts
@@ -70,7 +70,7 @@ describe("setup workflow", () => {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
- supported: true,
+ hopperSetupSupported: true,
};
const result = await runSetup(
{ ...options(false), structured: false },
@@ -162,7 +162,7 @@ describe("setup workflow", () => {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
- supported: true,
+ hopperSetupSupported: true,
};
const result = await runSetup(options(true, true), host);
expect(result.appliedActions).toEqual(["installed_hopper"]);
@@ -236,7 +236,7 @@ describe("setup workflow lifecycle and provider planning", () => {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
- supported: true,
+ hopperSetupSupported: true,
};
host.ghidra = "/opt/ghidra_12.1.4_PUBLIC";
host.javaHome = "/usr/lib/jvm/jdk-21";
diff --git a/src/application/Setup.recovery.test.ts b/src/application/Setup.recovery.test.ts
index f29c9d375..244aee26b 100644
--- a/src/application/Setup.recovery.test.ts
+++ b/src/application/Setup.recovery.test.ts
@@ -310,7 +310,7 @@ describe("setup scoped readiness", () => {
id: "debian",
versionId: "13",
packageFamily: "deb",
- supported: false,
+ hopperSetupSupported: false,
};
const result = await runSetup(options(true, true), host);
expect(result.status).toBe("needs_human");
@@ -326,7 +326,7 @@ describe("setup scoped readiness", () => {
id: "debian",
versionId: "13",
packageFamily: "deb",
- supported: false,
+ hopperSetupSupported: false,
};
host.ghidra = "/opt/ghidra_12.1.4_PUBLIC";
host.javaHome = "/usr/lib/jvm/jdk-21";
diff --git a/src/application/SetupClientConfiguration.ts b/src/application/SetupClientConfiguration.ts
index 981b829a4..90f4bf4ca 100644
--- a/src/application/SetupClientConfiguration.ts
+++ b/src/application/SetupClientConfiguration.ts
@@ -29,6 +29,7 @@ import { PRODUCT_IDENTITY } from "../identity.js";
import { npxRegistrationCommand } from "./ClientRegistrationIdentity.js";
import { MCP_STARTUP_POLICY } from "../mcpStartupPolicy.js";
import { resolveClientConfigTransactionPath } from "./ClientConfigPath.js";
+import { packageReadTargetChanged } from "./MsixPackageStorage.js";
import {
readRegularFile,
readRegularFileText,
@@ -40,7 +41,10 @@ import type {
ClientConfigurationResult,
SetupProviderEnvironment,
} from "./SetupTypes.js";
-import type { SetupClient } from "./SupportedClients.js";
+import {
+ registrationWriteBlocker,
+ type SetupClient,
+} from "./SupportedClients.js";
export class ClientConfigurationCleanupRollbackFailure extends RegularFileCleanupFailure {
constructor(
@@ -187,6 +191,13 @@ const persistClientConfiguration = async ({
desired,
fallbackServers,
}: ClientConfigurationUpdate): Promise => {
+ // The plan chose this file; an MSIX package may have switched to its
+ // private copy since, for example when it quit.
+ if (
+ client.msixPackage !== undefined &&
+ (await packageReadTargetChanged(client.msixPackage, client.configPath))
+ )
+ return { status: "failed", reason: "target_changed" };
const backupPath =
original === undefined ? undefined : `${client.configPath}.rea.backup`;
let recoveryBackupPath: string | undefined;
@@ -358,6 +369,25 @@ export const inspectClientConfiguration = async (
client: SetupClient,
providerEnvironment: SetupProviderEnvironment,
command: readonly string[],
+): Promise => {
+ const inspection = await inspectClientDocument(
+ client,
+ providerEnvironment,
+ command,
+ );
+ if (inspection.status !== "create" && inspection.status !== "update")
+ return inspection;
+ // A change the running client would overwrite is not a persisted registration.
+ const blocker = registrationWriteBlocker(client, "setup");
+ return blocker === undefined
+ ? inspection
+ : { status: "invalid", remediation: blocker };
+};
+
+const inspectClientDocument = async (
+ client: SetupClient,
+ providerEnvironment: SetupProviderEnvironment,
+ command: readonly string[],
): Promise => {
if (client.configPathError !== undefined)
return { status: "invalid", remediation: client.configPathError };
@@ -674,6 +704,14 @@ const clientConfigurationDesired = (
launcher,
environment.value,
);
+ const previous = parsed?.servers[PRODUCT_IDENTITY.mcpServerKey];
+ if (
+ client.name === "kimi" &&
+ typeof previous === "object" &&
+ previous !== null &&
+ "enabledTools" in previous
+ )
+ registration.enabledTools = previous.enabledTools;
return {
...registration,
...(client.name === "codex" || client.name === "grok_build"
@@ -681,5 +719,11 @@ const clientConfigurationDesired = (
startup_timeout_sec: MCP_STARTUP_POLICY.codexStartupTimeoutSeconds,
}
: {}),
+ ...(client.name === "kimi"
+ ? {
+ startupTimeoutMs: MCP_STARTUP_POLICY.kimiStartupTimeoutMs,
+ toolTimeoutMs: MCP_STARTUP_POLICY.kimiToolTimeoutMs,
+ }
+ : {}),
};
};
diff --git a/src/application/SetupClients.ts b/src/application/SetupClients.ts
index d86d04b9e..5ceccb21a 100644
--- a/src/application/SetupClients.ts
+++ b/src/application/SetupClients.ts
@@ -9,6 +9,8 @@ import type { SetupClient } from "./SupportedClients.js";
export const failedConfigurationMessage = (
result: Extract,
): string => {
+ if (result.reason === "target_changed")
+ return "The client began reading a different configuration file after the plan was shown, so no change was made. Rerun setup to review the new target.";
if (result.reason === "path")
return "Agent configuration path could not be safely verified. Check its permissions and, if it is a symbolic link, verify that the link resolves to a regular file owned by the current user, then rerun setup.";
if (result.reason === "backup")
diff --git a/src/application/SetupHost.ts b/src/application/SetupHost.ts
index 7ac6828fd..84d41229e 100644
--- a/src/application/SetupHost.ts
+++ b/src/application/SetupHost.ts
@@ -44,6 +44,10 @@ import {
retryRegularFileCleanup,
} from "./RegularFileRead.js";
import { providerRegistrationEnvironment } from "./SetupRegistrationEnvironment.js";
+import {
+ msixPackagesDirectory,
+ packagePrivateLauncherBlocker,
+} from "./MsixPackageStorage.js";
import type {
SetupHost,
SetupInitialState,
@@ -66,6 +70,31 @@ export const setupRegistrationCommand = (
]
: [resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), "mcp"];
+/**
+ * The launcher setup registers, and why it must not be registered when it
+ * exists only inside one MSIX package's private storage.
+ */
+const setupRegistration = (
+ platform: NodeJS.Platform,
+ homeDirectory: string,
+ environment: Readonly,
+): Pick => {
+ const registrationCommand = setupRegistrationCommand(
+ platform,
+ invokingPackageRunner(environment, platform),
+ );
+ const registrationCommandBlocker =
+ platform === "win32"
+ ? packagePrivateLauncherBlocker(
+ registrationCommand,
+ msixPackagesDirectory(homeDirectory, environment),
+ )
+ : undefined;
+ return registrationCommandBlocker === undefined
+ ? { registrationCommand }
+ : { registrationCommand, registrationCommandBlocker };
+};
+
export const filterClientsNeedingConfigure = async (
host: SetupHost,
detectedClients: readonly SetupClient[],
@@ -95,7 +124,8 @@ export const hostRemediation = async (
? "Upgrade to macOS 12 or newer."
: undefined;
}
- if ((await host.linuxDistribution())?.supported === true) return undefined;
+ if ((await host.linuxDistribution())?.hopperSetupSupported === true)
+ return undefined;
return "Automated Hopper setup supports Ubuntu 24.04+, Fedora 41+, Nobara 44+, 64-bit Arch Linux, CachyOS, and Omarchy; configure an existing supported provider instead.";
};
@@ -117,10 +147,7 @@ export const systemSetupHost = (
homeDirectory,
skillDestinations: (clientIds) =>
skillDestinations(homeDirectory, clientIds, environment, platform),
- registrationCommand: setupRegistrationCommand(
- platform,
- invokingPackageRunner(environment, platform),
- ),
+ ...setupRegistration(platform, homeDirectory, environment),
nodeVersion: process.versions.node,
macosVersion: () => doctorHost.macosVersion(),
linuxDistribution: readLinuxDistribution,
diff --git a/src/application/SetupPlan.ts b/src/application/SetupPlan.ts
index a78697c80..f428ecade 100644
--- a/src/application/SetupPlan.ts
+++ b/src/application/SetupPlan.ts
@@ -8,12 +8,18 @@ import {
import type { SkillDestination } from "./SetupSkill.js";
import { macHopperInstallDisclosure } from "./MacHopper.js";
import type {
+ ClientConfigurationInspection,
SetupAction,
SetupClientState,
SetupHost,
SetupProviderEnvironment,
} from "./SetupTypes.js";
-import type { SetupClient } from "./SupportedClients.js";
+import {
+ appDataRedirection,
+ registrationPersistenceDetail,
+ type SetupClient,
+} from "./SupportedClients.js";
+import { redirectedWriteBlocker } from "./MsixPackageStorage.js";
import type { DoctorReport } from "./Doctor.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
@@ -50,7 +56,8 @@ export const discoverSetupState = async (input: {
const hopperInstallSupported =
(input.host.platform === "darwin" &&
majorVersion((await input.host.macosVersion()) ?? "0") >= 12) ||
- (input.host.platform === "linux" && linuxDistribution?.supported === true);
+ (input.host.platform === "linux" &&
+ linuxDistribution?.hopperSetupSupported === true);
const installHopper =
input.forceHopperInstall ||
(hopperInstallSupported &&
@@ -161,12 +168,58 @@ export const planSetupActions = async (input: {
blocker: `${invalid.client.displayName ?? invalid.client.name}: ${invalid.inspection.remediation}`,
};
}
- const clientPlans = inspections.flatMap(({ client, inspection }) =>
+ const clientPlans = plannedClientWrites(
+ inspections,
+ input.discovery.installHopper,
+ );
+ const plannedActions = setupPlan({
+ platform: input.host.platform,
+ homeDirectory: input.host.homeDirectory,
+ installHopper: input.discovery.installHopper,
+ installSkill: input.installSkill,
+ skillClientIds: input.skillClientIds,
+ skillDestinations: input.host.skillDestinations(input.skillClientIds),
+ clients: clientPlans,
+ providerEnvironment: input.providerEnvironment,
+ ...(input.discovery.linuxPackageFamily === undefined
+ ? {}
+ : { linuxPackageFamily: input.discovery.linuxPackageFamily }),
+ command: input.command,
+ });
+ const blocker =
+ (clientPlans.length > 0
+ ? input.host.registrationCommandBlocker
+ : undefined) ??
+ redirectedWriteBlocker(
+ appDataRedirection(input.discovery.clients),
+ plannedActions
+ .filter(({ kind }) => kind !== "install_hopper")
+ .map(({ target }) => target),
+ "setup",
+ );
+ return {
+ plannedActions,
+ selectedClients,
+ clientsToConfigure: selectedClients.filter((client) =>
+ clientPlans.some(({ client: planned }) => planned.name === client.name),
+ ),
+ ...(blocker === undefined ? {} : { blocker }),
+ };
+};
+
+/** Selected clients whose registration setup writes, with the disclosed operation. */
+const plannedClientWrites = (
+ inspections: readonly {
+ readonly client: SetupClient;
+ readonly inspection: ClientConfigurationInspection;
+ }[],
+ installHopper: boolean,
+) =>
+ inspections.flatMap(({ client, inspection }) =>
client.format !== "unsupported" &&
(inspection.status === "create" ||
inspection.status === "update" ||
- (input.discovery.installHopper &&
- inspection.status === "already_current"))
+ (installHopper && inspection.status === "already_current"))
? [
{
client,
@@ -182,27 +235,6 @@ export const planSetupActions = async (input: {
]
: [],
);
- return {
- plannedActions: setupPlan({
- platform: input.host.platform,
- homeDirectory: input.host.homeDirectory,
- installHopper: input.discovery.installHopper,
- installSkill: input.installSkill,
- skillClientIds: input.skillClientIds,
- skillDestinations: input.host.skillDestinations(input.skillClientIds),
- clients: clientPlans,
- providerEnvironment: input.providerEnvironment,
- ...(input.discovery.linuxPackageFamily === undefined
- ? {}
- : { linuxPackageFamily: input.discovery.linuxPackageFamily }),
- command: input.command,
- }),
- selectedClients,
- clientsToConfigure: selectedClients.filter((client) =>
- clientPlans.some(({ client: planned }) => planned.name === client.name),
- ),
- };
-};
/** Describe every selected setup mutation before user approval. */
const setupPlan = (input: {
@@ -259,7 +291,7 @@ const setupPlan = (input: {
label: client.displayName ?? client.name,
target: client.configPath,
detail: clientConfigurationDetail(
- client.displayName ?? client.name,
+ client,
input.command,
input.providerEnvironment,
input.installHopper,
@@ -303,7 +335,7 @@ const majorVersion = (version: string): number =>
Number.parseInt(version.split(".")[0] ?? "0", 10);
const clientConfigurationDetail = (
- client: string,
+ client: SetupClient,
command: readonly string[],
environment: SetupProviderEnvironment,
installHopper: boolean,
@@ -320,5 +352,6 @@ const clientConfigurationDetail = (
? " If this plan installs Hopper, REA will add HOPPER_LAUNCHER_PATH using the launcher path reported by that installation."
: " The HOPPER_LAUNCHER_PATH value shown above is the current fallback; if this plan installs Hopper, REA will replace it with the launcher path reported by that installation."
: "";
- return `Add the REA MCP registration for ${client}; preserve unrelated configuration. The backup path reflects the configuration inspected for this plan; if that file changes before execution, setup will select a recovery path for its current contents. Command: ${command.map((part) => JSON.stringify(part)).join(" ")}.${environmentDetail}${hopperDetail}`;
+ const persistence = registrationPersistenceDetail(client);
+ return `Add the REA MCP registration for ${client.displayName ?? client.name}; preserve unrelated configuration. The backup path reflects the configuration inspected for this plan; if that file changes before execution, setup will select a recovery path for its current contents. Command: ${command.map((part) => JSON.stringify(part)).join(" ")}.${environmentDetail}${hopperDetail}${persistence === undefined ? "" : ` ${persistence}`}`;
};
diff --git a/src/application/SetupSkill.ts b/src/application/SetupSkill.ts
index 83ba96c5a..794202b48 100644
--- a/src/application/SetupSkill.ts
+++ b/src/application/SetupSkill.ts
@@ -72,6 +72,15 @@ interface CanonicalSkillFile {
readonly original: SkillFileState | undefined;
}
+const canonicalSkillContent = (relativePath: string): Promise =>
+ readFile(
+ new URL(
+ `../../skills/${PRODUCT_IDENTITY.skillName}/${relativePath}`,
+ import.meta.url,
+ ),
+ "utf8",
+ );
+
export interface SkillInstallFileOperations {
readonly mkdir: (path: string) => Promise;
readonly backup: (source: string, destination: string) => Promise;
@@ -244,19 +253,36 @@ const canonicalSkillFiles = async (
const destination = join(root, relativePath);
return {
destination,
- content: await readFile(
- new URL(
- `../../skills/${PRODUCT_IDENTITY.skillName}/${relativePath}`,
- import.meta.url,
- ),
- "utf8",
- ),
+ content: await canonicalSkillContent(relativePath),
original: await readOptionalSkillState(destination, resources),
};
}),
),
);
+const canonicalSkillNeedsInstallByReading = async (
+ home: string,
+ clientIds: readonly string[],
+ environment: Readonly,
+ platform: NodeJS.Platform,
+ resources?: ArtifactResourceScope,
+): Promise => {
+ for (const { path: root } of skillDestinations(
+ home,
+ clientIds,
+ environment,
+ platform,
+ )) {
+ for (const relativePath of MANAGED_SKILL_FILES) {
+ const destination = join(root, relativePath);
+ const content = await canonicalSkillContent(relativePath);
+ const original = await readOptionalSkillState(destination, resources);
+ if (original?.content !== content) return true;
+ }
+ }
+ return false;
+};
+
/** Report whether setup would change any file in the managed REA skill bundle. */
export const canonicalSkillNeedsInstall = async (
home: string,
@@ -266,15 +292,13 @@ export const canonicalSkillNeedsInstall = async (
resources?: ArtifactResourceScope,
): Promise => {
try {
- return (
- await canonicalSkillFiles(
- home,
- clientIds,
- environment,
- platform,
- resources,
- )
- ).some(({ content, original }) => original?.content !== content);
+ return await canonicalSkillNeedsInstallByReading(
+ home,
+ clientIds,
+ environment,
+ platform,
+ resources,
+ );
} catch (cause: unknown) {
if (
cause instanceof RegularFileCleanupFailure ||
diff --git a/src/application/SetupTypes.ts b/src/application/SetupTypes.ts
index 241e0f6dd..f0cf61c59 100644
--- a/src/application/SetupTypes.ts
+++ b/src/application/SetupTypes.ts
@@ -25,7 +25,12 @@ export type ClientConfigurationResult =
}
| {
readonly status: "failed";
- readonly reason: "path" | "backup" | "write" | "readback";
+ readonly reason:
+ | "path"
+ | "target_changed"
+ | "backup"
+ | "write"
+ | "readback";
}
| {
readonly status: "failed";
@@ -55,6 +60,8 @@ export interface SetupHost {
readonly homeDirectory: string;
skillDestinations(clientIds: readonly string[]): readonly SkillDestination[];
readonly registrationCommand: readonly string[];
+ /** Why no client may be registered with `registrationCommand` from this host. */
+ readonly registrationCommandBlocker?: string;
readonly nodeVersion: string;
macosVersion(): Promise;
linuxDistribution(): Promise;
diff --git a/src/application/SupportedClients.ts b/src/application/SupportedClients.ts
index 0a7352627..55a651a52 100644
--- a/src/application/SupportedClients.ts
+++ b/src/application/SupportedClients.ts
@@ -7,6 +7,8 @@ import {
resolveHermesClientHome,
} from "./HermesClientHome.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
+import { resolveWindowsClaudeDesktopLocation } from "./ClaudeDesktopConfigLocation.js";
+import type { MsixPackageView } from "./MsixPackageStorage.js";
/** One supported client configuration location. */
export interface SetupClient {
@@ -29,6 +31,10 @@ export interface SetupClient {
/** Same-scope fallback ZCode stops loading after its first native server. */
readonly zcodeFallbackPath?: string;
readonly markerPath?: string;
+ /** MSIX package storage that decides which configuration the client reads. */
+ readonly msixPackage?: MsixPackageView;
+ /** A running client can rewrite its configuration from memory. */
+ readonly quitBeforeSetup?: boolean;
readonly format?:
| "json"
| "toml"
@@ -39,6 +45,7 @@ export interface SetupClient {
| "grok"
| "omp"
| "pi"
+ | "kimi"
| "hermes"
| "zcode"
| "dsh"
@@ -61,12 +68,35 @@ interface ClientPathContext {
readonly env: Readonly;
}
-interface ClientDefinition {
+/** One resolved configuration location and its detection evidence. */
+interface ClientLocation {
+ readonly configPath: string;
+ readonly markerPath: string;
+ readonly msixPackage?: MsixPackageView;
+}
+
+type ClientDefinition = ClientDefinitionBase &
+ (
+ | {
+ readonly configPath: ClientPath;
+ readonly markerPath: ClientPath;
+ readonly location?: undefined;
+ }
+ | {
+ /** Resolve configuration and marker together when they share probes. */
+ readonly location: (
+ context: ClientPathContext,
+ ) => Result;
+ readonly configPath?: undefined;
+ readonly markerPath?: undefined;
+ }
+ );
+
+interface ClientDefinitionBase {
readonly name: string;
readonly displayName: string;
- readonly configPath: ClientPath;
- readonly markerPath: ClientPath;
readonly format: NonNullable;
+ readonly quitBeforeSetup?: boolean;
readonly zcodeFallbackPath?: (context: ClientPathContext) => string;
readonly skillPath?: ClientPath;
readonly configPaths?: (context: ClientPathContext) => readonly string[];
@@ -111,16 +141,20 @@ const traeUserDirectory = ({
return join(xdgConfigHome({ home, platform, env }), "Trae", "User");
};
-const claudeDesktopDirectory = ({
- home,
- platform,
- env,
-}: ClientPathContext): string => {
- if (platform === "win32")
- return join(env.APPDATA ?? join(home, "AppData", "Roaming"), "Claude");
- if (platform === "linux")
- return join(xdgConfigHome({ home, platform, env }), "Claude");
- return join(home, "Library", "Application Support", "Claude");
+/** Windows packaging decides which file Claude Desktop reads; see its resolver. */
+const claudeDesktopLocation = (
+ context: ClientPathContext,
+): Result => {
+ if (context.platform === "win32")
+ return resolveWindowsClaudeDesktopLocation(context.home, context.env);
+ const directory =
+ context.platform === "linux"
+ ? join(xdgConfigHome(context), "Claude")
+ : join(context.home, "Library", "Application Support", "Claude");
+ return ok({
+ configPath: join(directory, "claude_desktop_config.json"),
+ markerPath: directory,
+ });
};
const claudeCodeConfigDirectory = ({ home, env }: ClientPathContext): string =>
@@ -132,6 +166,10 @@ const claudeCodeMarkerDirectory = ({ home, env }: ClientPathContext): string =>
const codexDirectory = ({ home, env }: ClientPathContext): string =>
env.CODEX_HOME || join(home, ".codex");
+/** Kimi Code stores MCP declarations beside the rest of its home data. */
+const kimiDirectory = ({ home, env }: ClientPathContext): string =>
+ env.KIMI_CODE_HOME || join(home, ".kimi-code");
+
const qoderDirectory = ({ home, env }: ClientPathContext): string =>
env.QODER_CONFIG_DIR || join(home, ".qoder");
@@ -211,6 +249,55 @@ export const manualRegistrationRemediation = (
? GROK_BOT_MANUAL_REGISTRATION_REMEDIATION
: undefined;
+/**
+ * Facts a setup plan must disclose so an approved registration persists:
+ * which file an MSIX package reads, and that a running client can overwrite it.
+ */
+export const registrationPersistenceDetail = (
+ client: SetupClient,
+): string | undefined => {
+ const name = client.displayName ?? client.name;
+ const msix = client.msixPackage;
+ const packageDetail =
+ msix === undefined
+ ? undefined
+ : client.configPath === msix.privateConfigPath
+ ? `${name} is installed as MSIX package ${msix.packageFamilyName}, which reads this package-private file instead of ${msix.appDataConfigPath}.`
+ : `${name} is installed as MSIX package ${msix.packageFamilyName}, which reads this file until it saves its own copy at ${msix.privateConfigPath}.`;
+ const quitDetail =
+ client.quitBeforeSetup === true
+ ? `Quit ${name} fully before approving; closing its window is not enough. A running ${name} can rewrite this file from memory and drop the registration. Start it again after setup.`
+ : undefined;
+ const details = [packageDetail, quitDetail].filter(
+ (detail): detail is string => detail !== undefined,
+ );
+ return details.length === 0 ? undefined : details.join(" ");
+};
+
+/**
+ * Why this process must not change an MSIX client's configuration. A process
+ * the running app started sees its redirected AppData, and the app would
+ * overwrite the change from memory; an unknown view cannot rule that out.
+ */
+export const registrationWriteBlocker = (
+ client: SetupClient,
+ operation: "setup" | "uninstall",
+): string | undefined => {
+ const msix = client.msixPackage;
+ if (msix === undefined || msix.processView === "direct") return undefined;
+ const name = client.displayName ?? client.name;
+ return msix.processView === "redirected"
+ ? `This process was started from ${name}'s MSIX package ${msix.packageFamilyName}: its AppData is redirected into that package, so ${name} is running and would overwrite ${msix.privateConfigPath} from memory. Quit ${name} fully, then rerun ${operation} from a terminal that ${name} did not start.`
+ : `REA cannot tell whether ${name}'s MSIX package ${msix.packageFamilyName} started this process, because none of its files under ${dirname(msix.privateConfigPath)} could be compared with ${dirname(msix.appDataConfigPath)}. A running ${name} would overwrite the change. Open ${name} once if it has never run, quit it fully, then rerun ${operation} from a terminal that ${name} did not start.`;
+};
+
+/** The MSIX package view, if any, that redirects this process's AppData writes. */
+export const appDataRedirection = (
+ clients: readonly SetupClient[],
+): MsixPackageView | undefined =>
+ clients.find(({ msixPackage }) => msixPackage?.processView === "redirected")
+ ?.msixPackage;
+
const OMP_PROFILE_NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
const OMP_RESERVED_PROFILE =
/^(?:CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(?:\..*)?$/iu;
@@ -408,9 +495,8 @@ export const SUPPORTED_CLIENT_DEFINITIONS = [
{
name: "claude_desktop",
displayName: "Claude Desktop",
- configPath: (context: ClientPathContext) =>
- join(claudeDesktopDirectory(context), "claude_desktop_config.json"),
- markerPath: claudeDesktopDirectory,
+ location: claudeDesktopLocation,
+ quitBeforeSetup: true,
format: "json",
},
{
@@ -495,6 +581,14 @@ export const SUPPORTED_CLIENT_DEFINITIONS = [
markerPath: qwenCodeDirectory,
format: "json",
},
+ {
+ name: "kimi",
+ displayName: "Kimi Code",
+ configPath: (context: ClientPathContext) =>
+ join(kimiDirectory(context), "mcp.json"),
+ markerPath: kimiDirectory,
+ format: "kimi",
+ },
{
name: "vscode",
displayName: "VS Code",
@@ -627,6 +721,19 @@ const resolvePath = (
return typeof resolved === "string" ? ok(resolved) : resolved;
};
+const resolveLocation = (
+ definition: ClientDefinition,
+ context: ClientPathContext,
+): Result => {
+ if (definition.location !== undefined) return definition.location(context);
+ const config = resolvePath(definition.configPath, context);
+ if (!config.ok) return config;
+ const marker = resolvePath(definition.markerPath, context);
+ return marker.ok
+ ? ok({ configPath: config.value, markerPath: marker.value })
+ : marker;
+};
+
/** Describe every client location that setup, doctor, or uninstall may inspect. */
export const supportedClients = (
home: string,
@@ -637,27 +744,16 @@ export const supportedClients = (
const definitions: readonly ClientDefinition[] = SUPPORTED_CLIENT_DEFINITIONS;
return definitions.map((definition) => {
try {
- const config = resolvePath(definition.configPath, context);
- const marker = config.ok
- ? resolvePath(definition.markerPath, context)
- : config;
- if (!config.ok)
+ const location = resolveLocation(definition, context);
+ if (!location.ok)
return {
name: definition.name,
displayName: definition.displayName,
format: definition.format,
- configPath: config.error.path,
- configPathError: config.error.message,
+ configPath: location.error.path,
+ configPathError: location.error.message,
};
- if (!marker.ok)
- return {
- name: definition.name,
- displayName: definition.displayName,
- format: definition.format,
- configPath: marker.error.path,
- configPathError: marker.error.message,
- };
- const configPath = config.value;
+ const { configPath, markerPath, msixPackage } = location.value;
return {
name: definition.name,
displayName: definition.displayName,
@@ -675,7 +771,11 @@ export const supportedClients = (
...(definition.zcodeFallbackPath === undefined
? {}
: { zcodeFallbackPath: definition.zcodeFallbackPath(context) }),
- markerPath: marker.value,
+ markerPath,
+ ...(msixPackage === undefined ? {} : { msixPackage }),
+ ...(definition.quitBeforeSetup === true
+ ? { quitBeforeSetup: true }
+ : {}),
format: definition.format,
};
} catch (cause: unknown) {
diff --git a/src/application/Uninstall.ts b/src/application/Uninstall.ts
index a7f051a49..eac36c73d 100644
--- a/src/application/Uninstall.ts
+++ b/src/application/Uninstall.ts
@@ -32,10 +32,16 @@ import {
} from "./RegularFileRead.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import {
+ appDataRedirection,
manualRegistrationRemediation,
+ registrationWriteBlocker,
supportedClients,
type SetupClient,
} from "./SupportedClients.js";
+import {
+ redirectedWriteBlocker,
+ type MsixPackageView,
+} from "./MsixPackageStorage.js";
export { isUninstallFailure } from "./UninstallFailure.js";
interface ManagedPathStats {
@@ -93,6 +99,7 @@ export interface UninstallHost {
/** Report a configuration that cannot be read safely, before any removal. */
inspectClient(client: SetupClient): Promise;
removeClient(client: SetupClient): Promise;
+ inspectSkill?(): Promise;
removeSkill(): Promise;
purgeData(): Promise;
close?(): Promise;
@@ -118,6 +125,8 @@ export const runUninstall = async (
const blocking = await host.inspectClient(client);
if (blocking !== undefined) blocked.push(blocking);
}
+ const skillBlocker = await host.inspectSkill?.();
+ if (skillBlocker !== undefined) blocked.push(skillBlocker);
const items: UninstallItem[] =
blocked.length > 0
? [
@@ -209,27 +218,70 @@ export const systemUninstallHost = (
selectedHome ?? homeDirectoryFromEnvironment(environment, platform);
const resources = new ArtifactResourceScope();
const fileSystem = selectedFileSystem ?? systemFileSystem(resources);
+ // Set with the client list: the package view that redirects this
+ // process's AppData writes, if any.
+ let redirection: MsixPackageView | undefined;
+ const clientWriteBlocker = (client: SetupClient): string | undefined =>
+ registrationWriteBlocker(client, "uninstall") ??
+ redirectedWriteBlocker(redirection, [client.configPath], "uninstall");
+ const skillWriteBlocker = (path: string): string | undefined =>
+ redirectedWriteBlocker(redirection, [path], "uninstall");
return {
- clients: () =>
- Promise.resolve(
- supportedClients(home, platform, environment).flatMap((client) =>
- client.configPaths === undefined
- ? [client]
- : client.configPaths.map((configPath) => ({
- ...client,
- configPath,
- })),
+ clients: () => {
+ const clients = supportedClients(home, platform, environment);
+ redirection = appDataRedirection(clients);
+ return Promise.resolve(
+ clients.flatMap((client) =>
+ uninstallConfigPaths(client).map((configPath) => ({
+ ...client,
+ configPath,
+ })),
),
- ),
+ );
+ },
inspectClient: async (client) => {
const read = await readClientConfiguration(client, fileSystem);
- return read.kind === "item" && read.item.status === "failed"
- ? read.item
- : undefined;
+ if (read.kind === "item")
+ return read.item.status === "failed" ? read.item : undefined;
+ const removal = clientRegistrationRemoval(read.parsed, client);
+ if (!removal.removeNative && removal.legacyPath === undefined)
+ return undefined;
+ // A removal the running package would revert, or redirect into its
+ // private storage, would leave the registration in place.
+ const blocker = clientWriteBlocker(client);
+ return blocker === undefined
+ ? undefined
+ : item(client.name, "failed", blocker);
+ },
+ removeClient: (client) =>
+ removeClient(client, fileSystem, () => clientWriteBlocker(client)),
+ inspectSkill: async () => {
+ for (const destination of skillDestinations(
+ home,
+ undefined,
+ environment,
+ platform,
+ )) {
+ const inspected = await inspectManagedSkill(
+ destination.path,
+ destination.client,
+ fileSystem,
+ );
+ if (inspected?.status === "failed") return inspected;
+ if (inspected !== undefined) continue;
+ const blocker = skillWriteBlocker(destination.path);
+ if (blocker !== undefined) return item("skill", "failed", blocker);
+ }
+ return undefined;
},
- removeClient: (client) => removeClient(client, fileSystem),
removeSkill: () =>
- removeManagedSkills(home, fileSystem, environment, platform),
+ removeManagedSkills(
+ home,
+ fileSystem,
+ environment,
+ platform,
+ skillWriteBlocker,
+ ),
purgeData: () => purgeManagedData(join(home, ".rea"), fileSystem),
close: async () => {
try {
@@ -242,6 +294,36 @@ export const systemUninstallHost = (
};
};
+/**
+ * Every user file that can hold REA's registration for one client. An MSIX
+ * package can read either its private copy or the AppData file, and earlier
+ * setups wrote the AppData file, so both lose REA's entry.
+ */
+const uninstallConfigPaths = (client: SetupClient): readonly string[] => [
+ ...new Set([
+ ...(client.configPaths ?? [client.configPath]),
+ ...(client.msixPackage === undefined
+ ? []
+ : [
+ client.msixPackage.privateConfigPath,
+ client.msixPackage.appDataConfigPath,
+ ]),
+ ]),
+];
+
+/** Whether removal would change this configuration. */
+const registrationPresent = (parsed: ClientConfigurationDocument): boolean => {
+ const name = PRODUCT_IDENTITY.mcpServerKey;
+ return (
+ Object.hasOwn(parsed.servers, name) ||
+ // Beside a native OpenCode V2 table, OpenCode also loads a V1 `mcp.rea`.
+ legacyClientServerPath(parsed, name) !== undefined ||
+ // Harness ownership is the stable entry id. A stale or malformed launcher
+ // still belongs to that id, and so does a direct patch that projects no row.
+ (parsed.dialect === "dsh" && dshDocument(parsed.document).carriesEntry)
+ );
+};
+
type ClientConfigurationRead =
| { readonly kind: "item"; readonly item: UninstallItem }
| {
@@ -308,21 +390,47 @@ const itemRead = (result: UninstallItem): ClientConfigurationRead => ({
item: result,
});
+/**
+ * Share the exact ownership and applicability decision with preflight. A
+ * removable legacy entry carries its document path, so removal edits exactly
+ * the entry this decision admitted.
+ */
+const clientRegistrationRemoval = (
+ parsed: ClientConfigurationDocument,
+ client: SetupClient,
+): {
+ readonly removeNative: boolean;
+ readonly legacyPath: readonly string[] | undefined;
+} => {
+ const name = PRODUCT_IDENTITY.mcpServerKey;
+ if (
+ parsed.dialect === "dsh" &&
+ dshDocument(parsed.document).hasUnknownStructure
+ )
+ return { removeNative: false, legacyPath: undefined };
+ const legacyPath = legacyClientServerPath(parsed, name);
+ return {
+ removeNative:
+ (parsed.dialect === "dsh" && dshDocument(parsed.document).carriesEntry) ||
+ (Object.hasOwn(parsed.servers, name) &&
+ isOwnedRegistration(parsed.servers[name], client)),
+ legacyPath:
+ legacyPath !== undefined &&
+ isOwnedRegistration(parsed.legacyServers[name], client)
+ ? legacyPath
+ : undefined,
+ };
+};
+
const removeClient = async (
client: SetupClient,
fileSystem: UninstallFileSystem,
+ writeBlocker?: () => string | undefined,
): Promise => {
const read = await readClientConfiguration(client, fileSystem);
if (read.kind === "item") return read.item;
const { transactionPath, original, parsed } = read;
const name = PRODUCT_IDENTITY.mcpServerKey;
- // Beside a native OpenCode V2 table, OpenCode also loads a V1 `mcp.rea`.
- const legacyPath = legacyClientServerPath(parsed, name);
- const hasNative = Object.hasOwn(parsed.servers, name);
- // Harness ownership is the stable entry id. A stale or malformed launcher
- // still belongs to that id, and so does a direct patch that projects no row.
- const carriesDshEntry =
- parsed.dialect === "dsh" && dshDocument(parsed.document).carriesEntry;
if (
parsed.dialect === "dsh" &&
dshDocument(parsed.document).hasUnknownStructure
@@ -332,20 +440,21 @@ const removeClient = async (
"retained",
"A Cordis expression may produce registration rows, so the patch was retained without changes.",
);
- if (!hasNative && legacyPath === undefined && !carriesDshEntry)
+ if (!registrationPresent(parsed))
return item(client.name, "skipped", "REA registration is absent.");
- const removeNative =
- carriesDshEntry ||
- (hasNative && isOwnedRegistration(parsed.servers[name], client));
- const removeLegacy =
- legacyPath !== undefined &&
- isOwnedRegistration(parsed.legacyServers[name], client);
+ const { removeNative, legacyPath } = clientRegistrationRemoval(
+ parsed,
+ client,
+ );
+ const removeLegacy = legacyPath !== undefined;
if (!removeNative && !removeLegacy)
return item(
client.name,
"retained",
"The rea-named registration is not REA-owned.",
);
+ const blocker = writeBlocker?.();
+ if (blocker !== undefined) return item(client.name, "failed", blocker);
const remaining = { ...parsed.servers };
if (removeNative) delete remaining[name];
const document = withClientServers(
@@ -355,7 +464,7 @@ const removeClient = async (
);
const editedPaths = [
...(removeNative ? [clientServerPath(parsed, name)] : []),
- ...(removeLegacy ? [legacyPath] : []),
+ ...(legacyPath === undefined ? [] : [legacyPath]),
];
const backupPath = `${client.configPath}.rea.backup`;
try {
@@ -516,12 +625,18 @@ const removeManagedSkills = async (
fileSystem: UninstallFileSystem,
environment: Readonly,
platform: NodeJS.Platform,
+ writeBlocker?: (path: string) => string | undefined,
): Promise => {
- // Every destination shares one summary item, so each result is labeled by
- // the directory it refers to.
+ // Every destination shares one summary item; preserve both client and path.
const results = await Promise.all(
- skillDestinations(home, undefined, environment, platform).map(({ path }) =>
- removeManagedSkill(path, path, fileSystem),
+ skillDestinations(home, undefined, environment, platform).map(
+ ({ client, path }) =>
+ removeManagedSkill(
+ path,
+ `${client} (${path})`,
+ fileSystem,
+ writeBlocker,
+ ),
),
);
const failed = results.find(({ status }) => status === "failed");
@@ -552,11 +667,11 @@ const removeManagedSkills = async (
);
};
-const removeManagedSkill = async (
+const inspectManagedSkill = async (
path: string,
name: string,
fileSystem: UninstallFileSystem,
-): Promise => {
+): Promise => {
let root: ManagedPathStats;
try {
root = await fileSystem.stat(path);
@@ -594,6 +709,20 @@ const removeManagedSkill = async (
if (!isManagedSkillManifest(content))
return item(name, "retained", "Skill manifest is not REA-owned.");
+ return undefined;
+};
+
+const removeManagedSkill = async (
+ path: string,
+ name: string,
+ fileSystem: UninstallFileSystem,
+ writeBlocker?: (path: string) => string | undefined,
+): Promise => {
+ const inspection = await inspectManagedSkill(path, name, fileSystem);
+ if (inspection !== undefined) return inspection;
+ const blocker = writeBlocker?.(path);
+ if (blocker !== undefined) return item(name, "failed", blocker);
+
const manifestName = "SKILL.md";
const otherFiles = MANAGED_SKILL_FILES.filter(
(relativePath) => relativePath !== manifestName,
diff --git a/src/application/apktool/ApktoolResourceAnalysisService.test.ts b/src/application/apktool/ApktoolResourceAnalysisService.test.ts
index 6242bb54d..2220ce9e6 100644
--- a/src/application/apktool/ApktoolResourceAnalysisService.test.ts
+++ b/src/application/apktool/ApktoolResourceAnalysisService.test.ts
@@ -83,3 +83,37 @@ it("preserves cancellation before execution", async () => {
if (result.ok) return;
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
});
+
+it("preserves a cancellation that races a completed observation", async () => {
+ const controller = new AbortController();
+ const racingPort: ApktoolResourceAnalysisPort = {
+ async inspectAvailability() {
+ return { status: "available", code: null, reason: null, diagnostics: {} };
+ },
+ async close() {},
+ async execute() {
+ // The provider may finish its work before the abort is delivered, so a
+ // successful result is not by itself proof the caller still wants it.
+ controller.abort();
+ return ok(execution);
+ },
+ };
+ const result = await new ApktoolResourceAnalysisService(racingPort).execute(
+ "decode_android_resources",
+ { path: "/targets/app.apk", include_strings: true },
+ { signal: controller.signal },
+ );
+ expect(result.ok).toBe(false);
+ if (result.ok) return;
+ expect(result.error).toBeInstanceOf(AnalysisCancelledError);
+ // The observation the provider completed is retained on the cancellation, so
+ // a session still records the Evidence instead of losing finished work.
+ const retained = parseEvidence(result.error.partialObservation);
+ expect(retained.operation).toBe("decode_android_resources");
+ expect(retained.provider).toEqual(identity);
+ expect(retained.parameters).toEqual({
+ path: "/targets/app.apk",
+ include_strings: true,
+ });
+ expect(retained.confidence).toBe("observed");
+});
diff --git a/src/application/binary/AnalysisProviderEvaluation.ts b/src/application/binary/AnalysisProviderEvaluation.ts
index 1654ab783..27292ed31 100644
--- a/src/application/binary/AnalysisProviderEvaluation.ts
+++ b/src/application/binary/AnalysisProviderEvaluation.ts
@@ -12,7 +12,7 @@ import { jsonObjectSchema, type JsonValue } from "../../domain/jsonValue.js";
import { err, ok, type Result } from "../../domain/result.js";
import type { AnalysisProviderCandidate } from "../AnalysisProvider.js";
import type { AnalysisProviderCandidateStatus } from "./AnalysisProviderRegistry.js";
-import { ABORTED, waitForAbortable } from "./AbortablePromise.js";
+import { ABORTED, waitForAbortable } from "../../process/AbortablePromise.js";
/** Profile-resolution truth for one discovered provider candidate. */
export interface AnalysisProviderCandidateEvaluation {
diff --git a/src/application/binary/BinarySession.ts b/src/application/binary/BinarySession.ts
index 3da571832..b151958e7 100644
--- a/src/application/binary/BinarySession.ts
+++ b/src/application/binary/BinarySession.ts
@@ -283,7 +283,9 @@ export class BinarySession
previous.route.identity.id,
);
if (!closed.ok) {
- this.clearSessionRecords();
+ // A failed open retires the provider cache, not the investigation.
+ // Preserve Evidence and Unknown history for export and recovery.
+ this.clearSnapshot();
this.#clearRuntimeAvailability();
return closed;
}
diff --git a/src/application/binary/LazyAnalysisProvider.ts b/src/application/binary/LazyAnalysisProvider.ts
index a87e77445..095acf5ed 100644
--- a/src/application/binary/LazyAnalysisProvider.ts
+++ b/src/application/binary/LazyAnalysisProvider.ts
@@ -5,7 +5,7 @@ import { AnalysisError } from "../../domain/analysisErrorBase.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import type { ProviderOperationHealth } from "../../domain/providerOperationHealth.js";
import { err, ok } from "../../domain/result.js";
-import { ABORTED, waitForAbortable } from "./AbortablePromise.js";
+import { ABORTED, waitForAbortable } from "../../process/AbortablePromise.js";
import type {
AnalysisClient,
AnalysisClientContext,
diff --git a/src/application/binaryDiagnostics/RecordedCrashService.test.ts b/src/application/binaryDiagnostics/RecordedCrashService.test.ts
index cdf7cbd8f..8d8f8e94c 100644
--- a/src/application/binaryDiagnostics/RecordedCrashService.test.ts
+++ b/src/application/binaryDiagnostics/RecordedCrashService.test.ts
@@ -30,6 +30,29 @@ it("labels an envelope containing debugger-derived maps as derived", async () =>
expect(evidence.raw_result).toBeNull();
});
+it.each([
+ ["x86_64-little-endian", "x86_64"],
+ ["aarch64-little-endian", "arm64"],
+] as const)(
+ "names the CPU family carried by a %s core on created Evidence",
+ async (architecture, expected) => {
+ const fixture = recordedCrashFixture("/selected.core", architecture);
+ const service = new RecordedCrashService({
+ inspectAndClose(input, options) {
+ return this.inspect(input, options);
+ },
+ identity: RECORDED_CRASH_TEST_PROVIDER,
+ inspect: () => Promise.resolve(ok(fixture)),
+ });
+ const result = await service.inspect({
+ path: fixture.artifact.path,
+ include_debugger_context: false,
+ });
+ if (!result.ok) throw result.error;
+ expect(parseEvidence(result.value).subject?.architecture).toBe(expected);
+ },
+);
+
it.each([
{ path: "relative.elf" },
{ path: "/selected.core", approval: true },
diff --git a/src/application/binaryDiagnostics/RecordedCrashService.ts b/src/application/binaryDiagnostics/RecordedCrashService.ts
index df137f83e..67a57ca9a 100644
--- a/src/application/binaryDiagnostics/RecordedCrashService.ts
+++ b/src/application/binaryDiagnostics/RecordedCrashService.ts
@@ -10,6 +10,7 @@ import {
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import {
inspectRecordedCrashInputSchema,
+ RECORDED_LINUX_CORE_PROFILES,
recordedCrashSchema,
} from "../../domain/native/recordedCrash.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
@@ -101,7 +102,9 @@ export class RecordedCrashService {
{
path: value.artifact.path,
format: "elf",
- architecture: "x86_64",
+ architecture:
+ RECORDED_LINUX_CORE_PROFILES[value.architecture]
+ .evidenceArchitecture,
sha256: value.artifact.sha256,
},
this.provider.identity,
diff --git a/src/application/javascript/ElectronBoundaryGraphCallPaths.ts b/src/application/javascript/ElectronBoundaryGraphCallPaths.ts
index 4447d59db..b815e82bf 100644
--- a/src/application/javascript/ElectronBoundaryGraphCallPaths.ts
+++ b/src/application/javascript/ElectronBoundaryGraphCallPaths.ts
@@ -51,6 +51,9 @@ export const addElectronCallPaths = (
properties: {
path_id: pathId,
entry: path.entry,
+ ...(path.entry_path === undefined
+ ? {}
+ : { entry_path: [...path.entry_path] }),
entry_kind: path.entry_kind,
channel: path.channel,
terminal: path.terminal,
@@ -97,6 +100,9 @@ export const addElectronCallPaths = (
properties: {
path_id: pathId,
entry: path.entry,
+ ...(path.entry_path === undefined
+ ? {}
+ : { entry_path: [...path.entry_path] }),
resolution: "candidate",
},
});
@@ -119,6 +125,9 @@ export const addElectronCallPaths = (
properties: {
path_id: pathId,
entry: path.entry,
+ ...(path.entry_path === undefined
+ ? {}
+ : { entry_path: [...path.entry_path] }),
channel: path.channel,
resolution: "candidate",
conditions: path.conditions,
diff --git a/src/application/javascript/JavaScriptApplicationEvidence.ts b/src/application/javascript/JavaScriptApplicationEvidence.ts
index b812f237e..ca20b813f 100644
--- a/src/application/javascript/JavaScriptApplicationEvidence.ts
+++ b/src/application/javascript/JavaScriptApplicationEvidence.ts
@@ -100,4 +100,10 @@ const parameters = (
...(input.source_paths === undefined
? {}
: { source_paths: input.source_paths }),
+ ...(input.max_heap_mb === undefined
+ ? {}
+ : { max_heap_mb: input.max_heap_mb }),
+ ...(input.analysis_timeout_ms === undefined
+ ? {}
+ : { analysis_timeout_ms: input.analysis_timeout_ms }),
});
diff --git a/src/application/javascript/JavaScriptApplicationService.ts b/src/application/javascript/JavaScriptApplicationService.ts
index ea56ce591..fa66a5521 100644
--- a/src/application/javascript/JavaScriptApplicationService.ts
+++ b/src/application/javascript/JavaScriptApplicationService.ts
@@ -62,26 +62,7 @@ export const analyzeJavaScriptApplicationValidated = async (
});
await checkpointJavaScriptAnalysis(options.signal);
reconstructed = await reconstructJavaScriptArtifact(
- {
- input_path: input.input_path,
- format: input.format,
- integrity_policy: input.integrity_policy,
- ...(input.max_semantic_nodes === undefined
- ? {}
- : { max_semantic_nodes: input.max_semantic_nodes }),
- ...(input.max_semantic_nodes_per_file === undefined
- ? {}
- : { max_semantic_nodes_per_file: input.max_semantic_nodes_per_file }),
- ...(input.source_paths === undefined
- ? {}
- : { source_paths: input.source_paths }),
- ...(input.max_heap_mb === undefined
- ? {}
- : { max_heap_mb: input.max_heap_mb }),
- ...(input.analysis_timeout_ms === undefined
- ? {}
- : { analysis_timeout_ms: input.analysis_timeout_ms }),
- },
+ input,
scope,
options.signal,
options.progress,
diff --git a/src/application/javascript/JavaScriptArtifactAnalysis.ts b/src/application/javascript/JavaScriptArtifactAnalysis.ts
index e61d3ae94..6b48b448c 100644
--- a/src/application/javascript/JavaScriptArtifactAnalysis.ts
+++ b/src/application/javascript/JavaScriptArtifactAnalysis.ts
@@ -553,10 +553,10 @@ const collectSourceMapOriginals = (
if (!Array.isArray(names))
return invalidSourceMap(file, "Source map has no sources array.");
const contents = map.sourcesContent;
- if (!hasValidSourceMapContents(names.length, contents))
+ if (!hasValidSourceMapContents(contents))
return invalidSourceMap(
file,
- "Source map sourcesContent must contain one string or null entry per source.",
+ "Source map sourcesContent must be an array of strings or null entries.",
);
if (map.sourceRoot != null && typeof map.sourceRoot !== "string")
return invalidSourceMap(
diff --git a/src/application/javascript/JavaScriptArtifactPathResolution.ts b/src/application/javascript/JavaScriptArtifactPathResolution.ts
index cb29ab2a9..88aa1508f 100644
--- a/src/application/javascript/JavaScriptArtifactPathResolution.ts
+++ b/src/application/javascript/JavaScriptArtifactPathResolution.ts
@@ -457,9 +457,8 @@ const bareModuleCandidate = (
dependencyDirectory = posix.dirname(confined);
}
const source = input.files.get(input.sourcePath);
- let directory = posix.dirname(input.sourcePath);
let nearestNotFound: CandidateResolution | null = null;
- while (true) {
+ for (const directory of packageLookupDirectories(input)) {
const candidate = posix.join(directory, dependencyDirectory);
const metadata = readPackageMetadata(input, candidate);
const continueCommonJsLookup =
@@ -487,8 +486,6 @@ const bareModuleCandidate = (
nearestNotFound ??= packageResolution;
} else return packageResolution;
}
- if (directory === "." || directory === "") break;
- directory = posix.dirname(directory);
}
if (nearestNotFound !== null) return nearestNotFound;
return unresolvedCandidate("external", [
@@ -496,6 +493,21 @@ const bareModuleCandidate = (
]);
};
+function* packageLookupDirectories(
+ input: ResolveArtifactPathInput,
+): Generator {
+ let directory = posix.dirname(input.sourcePath);
+ while (true) {
+ if (
+ input.moduleKind !== "require" ||
+ posix.basename(directory) !== "node_modules"
+ )
+ yield directory;
+ if (directory === "." || directory === "") return;
+ directory = posix.dirname(directory);
+ }
+}
+
/**
* Nearest same-container package.json, stopping at a `node_modules` directory
* so a nested package cannot inherit an ancestor's map.
@@ -960,15 +972,29 @@ const resolveCandidate = (
candidate: string,
packageChain: ReadonlySet,
emittedSourceCandidates: readonly TypeScriptEmittedSourceCandidate[] = [],
-): CandidateResolution =>
- resolveFileCandidates(input, fileCandidates(candidate, input.moduleKind)) ??
- resolveRuntimeSourceCandidates(input, candidate, emittedSourceCandidates) ??
- resolveDirectory(
+): CandidateResolution => {
+ if (!commonJsDirectoryRequest(input)) {
+ const file =
+ resolveFileCandidates(
+ input,
+ fileCandidates(candidate, input.moduleKind),
+ ) ??
+ resolveRuntimeSourceCandidates(input, candidate, emittedSourceCandidates);
+ if (file !== null) return file;
+ }
+ return resolveDirectory(
input,
candidate,
readPackageMetadata(input, candidate),
packageChain,
);
+};
+
+const commonJsDirectoryRequest = (input: ResolveArtifactPathInput): boolean =>
+ input.context === "module-specifier" &&
+ input.moduleKind === "require" &&
+ (input.declaredPath.endsWith("/") ||
+ /(?:^|\/)\.{1,2}$/u.test(input.declaredPath));
const resolveDirectory = (
input: ResolveArtifactPathInput,
diff --git a/src/application/javascript/JavaScriptArtifactReconstruction.ts b/src/application/javascript/JavaScriptArtifactReconstruction.ts
index bd95415c6..a5528ee10 100644
--- a/src/application/javascript/JavaScriptArtifactReconstruction.ts
+++ b/src/application/javascript/JavaScriptArtifactReconstruction.ts
@@ -277,15 +277,9 @@ export const reconstructJavaScriptArtifact = async (
limitations: completed.graph.limitations,
}),
);
- const { max_heap_mb, analysis_timeout_ms, ...coreInput } = input;
- const parameters = {
- ...coreInput,
- ...(max_heap_mb === undefined ? {} : { max_heap_mb }),
- ...(analysis_timeout_ms === undefined ? {} : { analysis_timeout_ms }),
- };
const evidence =
await createOwnedJavaScriptApplicationEvidenceCooperatively(
- parameters,
+ input,
partial,
);
throw interruption.retainPartialObservation(evidence);
diff --git a/src/application/javascript/JavaScriptSemanticGraphElectronProjection.ts b/src/application/javascript/JavaScriptSemanticGraphElectronProjection.ts
index 53b5bdce6..c10e8f632 100644
--- a/src/application/javascript/JavaScriptSemanticGraphElectronProjection.ts
+++ b/src/application/javascript/JavaScriptSemanticGraphElectronProjection.ts
@@ -34,6 +34,9 @@ export function* projectElectronSemanticCallPaths(
properties: {
path_id: pathId,
entry: path.entry,
+ ...(path.entry_path === undefined
+ ? {}
+ : { entry_path: [...path.entry_path] }),
channel: path.channel,
terminal: path.terminal,
step_kind: step.kind,
@@ -62,6 +65,9 @@ export function* projectElectronSemanticCallPaths(
properties: {
path_id: pathId,
entry: path.entry,
+ ...(path.entry_path === undefined
+ ? {}
+ : { entry_path: [...path.entry_path] }),
channel: path.channel,
basis: "lexical-wrapper-substitution",
},
diff --git a/src/application/native/EnhancedNativeInvestigation.test.ts b/src/application/native/EnhancedNativeInvestigation.test.ts
index 087b5981f..1e1b517b6 100644
--- a/src/application/native/EnhancedNativeInvestigation.test.ts
+++ b/src/application/native/EnhancedNativeInvestigation.test.ts
@@ -23,6 +23,20 @@ import type { Result } from "../../domain/result.js";
const target = "a".repeat(64);
const provider = { id: "fixture", name: "Fixture", version: "1" };
+const methodName = "-[BuildViewController buildTapped:]";
+const methodSymbol = {
+ primary: true,
+ dynamic: false,
+ external: false,
+ type: "function",
+ source: "user_defined",
+};
+const methodInventoryEntry = {
+ address: "0x1000",
+ value: methodName,
+ symbol: methodSymbol,
+};
+
const evidence = [
{
kind: "interface_builder_resource" as const,
@@ -199,6 +213,49 @@ const testAnalysis = (
});
describe("native UI action trace", () => {
+ it("retains Hopper targetless call evidence alongside fallback callee routes", async () => {
+ const result = await new EnhancedTools(
+ testAnalysis({
+ procedure_address: () => execution("procedure_address", "0x1000"),
+ procedure_references: (parameters) =>
+ ok(
+ createAnalysisExecution(
+ {
+ reference_kinds_available: false,
+ references: [],
+ unresolved_calls:
+ parameters.procedure === "0x1000"
+ ? [{ address: "0x1008", reason: "No static target" }]
+ : [],
+ },
+ { id: "hopper", name: "Hopper", version: "fixture" },
+ ),
+ ),
+ }),
+ ).execute("trace_native_ui_action", { action: "0x1000" });
+ if (!result.ok) throw result.error;
+ const trace = nativeInvestigationTraceSchema.parse(result.value);
+ expect(trace.edges).toContainEqual(
+ expect.objectContaining({
+ relation: "provider_call",
+ to: "native:function:0x2000",
+ }),
+ );
+ expect(trace.unresolved).toContainEqual(
+ expect.objectContaining({
+ from: "native:function:0x1000",
+ to: null,
+ reason: "No static target",
+ evidence: [
+ expect.objectContaining({
+ description: "Hopper reported an unresolved call: No static target",
+ location: { address: "0x1008", file_offset: null },
+ artifact_sha256: target,
+ }),
+ ],
+ }),
+ );
+ });
it("accepts an explicit native seed without an authored UI connection", async () => {
const result = await new EnhancedTools(
testAnalysis({
@@ -215,8 +272,12 @@ describe("native UI action trace", () => {
},
});
});
- it("builds the evidence path from the active app and native provider", async () => {
- const tools = new EnhancedTools(testAnalysis());
+ it("builds the evidence path from provider names with declared symbol facts", async () => {
+ const tools = new EnhancedTools(
+ testAnalysis({
+ list_names: () => execution("list_names", [methodInventoryEntry]),
+ }),
+ );
const result = await tools.execute("trace_native_ui_action", {
action: "buildTapped:",
});
@@ -349,18 +410,117 @@ describe("bounded native UI tracing", () => {
error: { _tag: "AnalysisInputError" },
});
});
+});
- it("binds dispatch metadata to the active target digest", async () => {
- const result = await new EnhancedTools(testAnalysis()).execute(
- "inspect_native_dispatch_metadata",
- { max_records: 10 },
- );
+describe("native dispatch inventory", () => {
+ it.each([
+ { label: "plain value", entry: { address: "0x1000", value: methodName } },
+ { label: "value with symbol facts", entry: methodInventoryEntry },
+ {
+ label: "legacy name with symbol facts",
+ entry: { address: "0x1000", name: methodName, symbol: methodSymbol },
+ },
+ ])(
+ "binds $label inventory to the target without overstating dispatch coverage",
+ async ({ entry }) => {
+ const result = await new EnhancedTools(
+ testAnalysis({
+ list_names: () => execution("list_names", [entry]),
+ }),
+ ).execute("inspect_native_dispatch_metadata", { max_records: 10 });
- expect(result).toMatchObject({
- ok: true,
- value: { target_sha256: target },
- });
- });
+ expect(result).toMatchObject({
+ ok: true,
+ value: {
+ target_sha256: target,
+ provider,
+ result: {
+ objc_dispatch_implementations: [
+ {
+ class_name: "BuildViewController",
+ selector: "buildTapped:",
+ implementation_address: "0x1000",
+ decode: { status: "partial", reason: "symbol_name_only" },
+ },
+ ],
+ coverage: expect.arrayContaining([
+ expect.objectContaining({
+ facet: "objc_dispatch_implementations",
+ status: "partial",
+ }),
+ expect.objectContaining({
+ facet: "swift_dispatch_tables",
+ status: "unsupported",
+ }),
+ expect.objectContaining({
+ facet: "relative_pointers",
+ status: "unsupported",
+ }),
+ ]),
+ },
+ },
+ });
+ },
+ );
+
+ it.each([
+ {
+ label: "malformed symbol flag",
+ entry: {
+ ...methodInventoryEntry,
+ symbol: { ...methodSymbol, primary: "true" },
+ },
+ },
+ {
+ label: "unknown symbol source",
+ entry: {
+ ...methodInventoryEntry,
+ symbol: { ...methodSymbol, source: "unknown" },
+ },
+ },
+ {
+ label: "incomplete symbol",
+ entry: { ...methodInventoryEntry, symbol: { primary: true } },
+ },
+ {
+ label: "malformed legacy symbol",
+ entry: { address: "0x1000", name: methodName, symbol: null },
+ },
+ {
+ label: "malformed address",
+ entry: { ...methodInventoryEntry, address: 4096 },
+ },
+ {
+ label: "malformed value with a legacy name",
+ entry: { ...methodInventoryEntry, value: 42, name: methodName },
+ },
+ {
+ label: "undeclared field",
+ entry: { address: "0x1000", value: methodName, unexpected: true },
+ },
+ ])(
+ "rejects $label inventory in both dispatch consumers",
+ async ({ entry }) => {
+ const tools = new EnhancedTools(
+ testAnalysis({
+ list_names: () => execution("list_names", [entry]),
+ }),
+ );
+ for (const [operation, parameters] of [
+ ["inspect_native_dispatch_metadata", { max_records: 10 }],
+ ["trace_native_ui_action", { action: "buildTapped:" }],
+ ] as const) {
+ expect(await tools.execute(operation, parameters)).toMatchObject({
+ ok: false,
+ error: {
+ _tag: "AnalysisOutputError",
+ operation: "list_names",
+ reason: "provider returned an invalid inventory",
+ },
+ });
+ }
+ },
+ );
});
describe("native UI query outcomes", () => {
diff --git a/src/application/native/NativeCallRoutes.test.ts b/src/application/native/NativeCallRoutes.test.ts
index e54a55dc5..135807c3e 100644
--- a/src/application/native/NativeCallRoutes.test.ts
+++ b/src/application/native/NativeCallRoutes.test.ts
@@ -7,6 +7,59 @@ import { err, ok } from "../../domain/result.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
const provider = { id: "fixture", name: "Fixture", version: "1" };
+describe("untyped native call route fallback", () => {
+ it.each([{ callees: [] }, { callees: ["0x2000"] }])(
+ "preserves targetless calls and their provider when fallback callees are $callees",
+ async ({ callees }) => {
+ const calls: string[] = [];
+ const referenceProvider = { ...provider, id: "hopper", name: "Hopper" };
+ const fallbackProvider = { ...provider, id: "fallback" };
+ const signal = new AbortController().signal;
+ const unknown = { address: "0x1008", reason: "No static target" };
+ const result = await readNativeCallRoutes(
+ {
+ execute: async (operation, parameters, options) => {
+ calls.push(operation);
+ expect(parameters.procedure).toBe("0x1000");
+ expect(options).toEqual({ signal, selectorOrigin: "provider" });
+ return ok(
+ createAnalysisExecution(
+ operation === "procedure_references"
+ ? {
+ reference_kinds_available: false,
+ references: [],
+ unresolved_calls: [unknown],
+ }
+ : callees,
+ operation === "procedure_references"
+ ? referenceProvider
+ : fallbackProvider,
+ ),
+ );
+ },
+ },
+ "0x1000",
+ signal,
+ "provider",
+ );
+ expect(calls).toEqual(["procedure_references", "procedure_callees"]);
+ expect(result).toEqual({
+ ok: true,
+ value: {
+ provider: fallbackProvider,
+ routes: callees.map((address) => ({
+ address,
+ site: "0x1000",
+ relation: "provider_call",
+ resolution: "inferred",
+ })),
+ unknowns: [{ ...unknown, provider: referenceProvider }],
+ },
+ });
+ },
+ );
+});
+
describe("typed native call routes", () => {
it("preserves computed targets, ambiguity and targetless call sites", async () => {
const reference = jsonValueSchema.parse(ghidraReferenceEdge());
diff --git a/src/application/native/NativeCallRoutes.ts b/src/application/native/NativeCallRoutes.ts
index fe77f61e9..9848ab86d 100644
--- a/src/application/native/NativeCallRoutes.ts
+++ b/src/application/native/NativeCallRoutes.ts
@@ -2,12 +2,12 @@ import { z } from "zod";
import type {
AnalysisOperationPort,
ExecutionOptions,
+ ProviderIdentity,
} from "../AnalysisProvider.js";
import {
parseRelatedAddresses,
referenceKindSchema,
} from "../../domain/hopperValues.js";
-import { err, ok } from "../../domain/result.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
const referencesSchema = z.object({
@@ -40,15 +40,29 @@ export const readNativeCallRoutes = async (
{ procedure, direction: "outgoing" },
options,
);
+ let unknowns: Array<{
+ address: string;
+ reason: string;
+ provider: ProviderIdentity;
+ }> = [];
if (execution.ok) {
const parsed = referencesSchema.safeParse(execution.value.result);
if (!parsed.success)
- return err(
- new AnalysisOutputError(
+ return {
+ ok: false as const,
+ error: new AnalysisOutputError(
"procedure_references",
"Invalid typed call references",
),
- );
+ unknowns,
+ };
+ // Missing detailed reference flags do not invalidate observed targetless
+ // calls. Keep their producing provider even if another provider supplies
+ // the fallback callee inventory.
+ unknowns = parsed.data.unresolved_calls.map((call) => ({
+ ...call,
+ provider: execution.value.provider,
+ }));
const references = parsed.data.references;
if (
parsed.data.reference_kinds_available === true ||
@@ -64,45 +78,52 @@ export const readNativeCallRoutes = async (
targets.add(call.target_address);
countBySite.set(call.source_address, targets);
}
- return ok({
- provider: execution.value.provider,
- routes: calls.map((call) => ({
- address: call.target_procedure?.address ?? call.target_address,
- site: call.source_address,
- relation:
- call.kind.available && (call.kind.computed || call.kind.indirect)
- ? ("indirect_call" as const)
- : ("direct_call" as const),
- resolution:
- (countBySite.get(call.source_address)?.size ?? 0) > 1
- ? ("ambiguous" as const)
- : call.kind.available &&
- (call.kind.computed || call.kind.indirect)
- ? ("resolved" as const)
- : ("observed" as const),
- })),
- unknowns: parsed.data.unresolved_calls,
- });
+ return {
+ ok: true as const,
+ value: {
+ provider: execution.value.provider,
+ routes: calls.map((call) => ({
+ address: call.target_procedure?.address ?? call.target_address,
+ site: call.source_address,
+ relation:
+ call.kind.available && (call.kind.computed || call.kind.indirect)
+ ? ("indirect_call" as const)
+ : ("direct_call" as const),
+ resolution:
+ (countBySite.get(call.source_address)?.size ?? 0) > 1
+ ? ("ambiguous" as const)
+ : call.kind.available &&
+ (call.kind.computed || call.kind.indirect)
+ ? ("resolved" as const)
+ : ("observed" as const),
+ })),
+ unknowns,
+ },
+ };
}
} else if (execution.error._tag !== "AnalysisCapabilityUnavailableError")
- return err(execution.error);
+ return { ok: false as const, error: execution.error, unknowns };
const callees = await analysis.execute(
"procedure_callees",
{ procedure },
options,
);
- if (!callees.ok) return callees;
+ if (!callees.ok)
+ return { ok: false as const, error: callees.error, unknowns };
const parsed = parseRelatedAddresses(callees.value.result, "callees");
return parsed.ok
- ? ok({
- provider: callees.value.provider,
- routes: parsed.value.map((address) => ({
- address,
- site: procedure,
- relation: "provider_call" as const,
- resolution: "inferred" as const,
- })),
- unknowns: [],
- })
- : parsed;
+ ? {
+ ok: true as const,
+ value: {
+ provider: callees.value.provider,
+ routes: parsed.value.map((address) => ({
+ address,
+ site: procedure,
+ relation: "provider_call" as const,
+ resolution: "inferred" as const,
+ })),
+ unknowns,
+ },
+ }
+ : { ok: false as const, error: parsed.error, unknowns };
};
diff --git a/src/application/native/NativeDispatchMetadataInspection.ts b/src/application/native/NativeDispatchMetadataInspection.ts
index 85c1a69c3..e72d17dbf 100644
--- a/src/application/native/NativeDispatchMetadataInspection.ts
+++ b/src/application/native/NativeDispatchMetadataInspection.ts
@@ -1,4 +1,5 @@
import { z } from "zod";
+import { addressedValue } from "../../contracts/toolOutputSchemaPrimitives.js";
import {
nativeDispatchMetadataResultSchema,
inspectNativeDispatchMetadata,
@@ -36,8 +37,11 @@ export const inspectNativeDispatch = async (
const names = z
.array(
z.union([
- z.strictObject({ address: z.string(), value: z.string() }),
- z.strictObject({ address: z.string(), name: z.string() }),
+ addressedValue.strict(),
+ addressedValue
+ .omit({ value: true })
+ .extend({ name: z.string() })
+ .strict(),
]),
)
.safeParse(execution.value.result);
diff --git a/src/application/native/NativeUiActionTrace.ts b/src/application/native/NativeUiActionTrace.ts
index 141b31bc0..8a6affdf7 100644
--- a/src/application/native/NativeUiActionTrace.ts
+++ b/src/application/native/NativeUiActionTrace.ts
@@ -414,9 +414,44 @@ const addDirectCallees = async (input: {
"provider",
);
examined += 1;
+ if (!execution.ok && execution.error._tag === "AnalysisCancelledError")
+ return { error: execution.error };
+ const unknowns = execution.ok
+ ? execution.value.unknowns
+ : execution.unknowns;
+ for (const [index, unknown] of unknowns.entries()) {
+ if (edges.size >= input.input.max_edges) {
+ truncated = true;
+ omitted += unknowns.length - index;
+ break;
+ }
+ const id = `unknown-dispatch:${current.address}:${unknown.address}`;
+ edges.set(id, {
+ id,
+ from: current.node.id,
+ to: null,
+ relation: "indirect_call",
+ resolution: "unresolved",
+ reason: unknown.reason,
+ evidence: [
+ {
+ kind: "reference",
+ provider: unknown.provider,
+ description: `${unknown.provider.name} reported an unresolved call: ${unknown.reason}`,
+ location: { address: unknown.address, file_offset: null },
+ artifact_path: null,
+ artifact_sha256: input.targetSha256,
+ },
+ ],
+ limitations: [],
+ });
+ }
if (!execution.ok) {
- if (execution.error._tag === "AnalysisCancelledError")
- return { error: execution.error };
+ if (edges.size >= input.input.max_edges) {
+ truncated = true;
+ omitted += 1;
+ continue;
+ }
const id = `unknown-callees:${current.address}`;
edges.set(id, {
id,
@@ -432,32 +467,6 @@ const addDirectCallees = async (input: {
});
continue;
}
- for (const unknown of execution.value.unknowns) {
- if (edges.size >= input.input.max_edges) {
- truncated = true;
- omitted++;
- break;
- }
- const id = `unknown-dispatch:${current.address}:${unknown.address}`;
- edges.set(id, {
- id,
- from: current.node.id,
- to: null,
- relation: "indirect_call",
- resolution: "unresolved",
- reason: unknown.reason,
- evidence: [
- {
- kind: "reference",
- description: unknown.reason,
- location: { address: unknown.address, file_offset: null },
- artifact_path: null,
- artifact_sha256: input.targetSha256,
- },
- ],
- limitations: [],
- });
- }
for (const [index, route] of execution.value.routes.entries()) {
const address = route.address;
if (edges.size >= input.input.max_edges) {
@@ -469,6 +478,7 @@ const addDirectCallees = async (input: {
const evidence = [
{
kind: "reference" as const,
+ provider: execution.value.provider,
description: `${execution.value.provider.name} reported ${route.relation} from ${current.address} to ${address}`,
location: { address: route.site, file_offset: null },
artifact_path: null,
diff --git a/src/application/wasm/WasmInterfaceComparisonService.test.ts b/src/application/wasm/WasmInterfaceComparisonService.test.ts
new file mode 100644
index 000000000..9936c7530
--- /dev/null
+++ b/src/application/wasm/WasmInterfaceComparisonService.test.ts
@@ -0,0 +1,198 @@
+import { expect, it } from "vitest";
+import { createEvidence } from "../../domain/evidence.js";
+import { wasmInterfaceComparisonSchema } from "../../contracts/wasm/wasmComparisonContracts.js";
+import { compareWasmInterfaces } from "./WasmInterfaceComparisonService.js";
+import type { WasmInterface } from "../../domain/wasm/wasmInterface.js";
+import { wasmArtifactFixture } from "../../../tests/fixtures/wasm/artifact.js";
+
+const facts = (
+ name: string,
+ parameter: "i32" | "i64" = "i32",
+): WasmInterface => ({
+ state: "complete",
+ issues: [],
+ entries: [
+ {
+ direction: "export",
+ module: null,
+ name,
+ kind: "func",
+ index: 0,
+ signature: { kind: "func", params: [parameter], results: [] },
+ wat_form: `(export "${name}" (func 0))`,
+ issue: null,
+ },
+ ],
+});
+const evidence = (interfaceFacts?: WasmInterface) => {
+ const report = wasmArtifactFixture();
+ if (interfaceFacts !== undefined) report.interface = interfaceFacts;
+ return createEvidence(
+ {
+ path: report.artifact.path,
+ sha256: report.artifact.sha256,
+ format: "file",
+ },
+ { id: "fixture", name: "typed fixture", version: "1" },
+ {
+ operation: "inspect_wasm_artifact",
+ parameters: { path: report.artifact.path },
+ result: report,
+ confidence: "observed",
+ },
+ );
+};
+const compare = async (
+ left: ReturnType,
+ right: ReturnType,
+) => {
+ const result = await compareWasmInterfaces({ left, right, scope: "both" });
+ if (!result.ok) throw result.error;
+ return wasmInterfaceComparisonSchema.parse(result.value.normalized_result);
+};
+
+it("reports all five comparison states, including duplicate ambiguity and explicit selected scope", async () => {
+ const left = facts("changed");
+ const right = facts("changed", "i64");
+ left.entries.push(
+ ...facts("unchanged").entries,
+ ...facts("removed").entries,
+ ...facts("dup").entries,
+ ...facts("dup").entries,
+ );
+ right.entries.push(
+ ...facts("unchanged").entries,
+ ...facts("added").entries,
+ ...facts("dup").entries,
+ );
+ const report = await compare(evidence(left), evidence(right));
+ expect(report.summary).toEqual({
+ changed: 1,
+ unchanged: 1,
+ removed: 1,
+ added: 1,
+ unknown: 1,
+ });
+ expect(report.changes.find((c) => c.name === "dup")).toMatchObject({
+ state: "unknown",
+ left: [expect.anything(), expect.anything()],
+ right: [expect.anything()],
+ });
+ const selected = await compareWasmInterfaces({
+ left: evidence(left),
+ right: evidence(right),
+ scope: "imports",
+ });
+ if (!selected.ok) throw selected.error;
+ expect(selected.value.normalized_result).toMatchObject({
+ changes: [],
+ coverage: "complete-within-inputs",
+ });
+});
+
+it("withholds absence claims for historical or partial inventories and unsupported signatures", async () => {
+ const known = facts("f");
+ const absent = await compare(evidence(), evidence(known));
+ expect(absent).toMatchObject({
+ coverage: "partial",
+ changes: [{ state: "unknown" }],
+ });
+ const partial: WasmInterface = {
+ state: "partial",
+ issues: ["unread import table"],
+ entries: [],
+ };
+ expect(await compare(evidence(partial), evidence(known))).toMatchObject({
+ changes: [{ state: "unknown" }],
+ });
+ const unsupported = facts("f");
+ unsupported.entries[0]!.signature = null;
+ expect(await compare(evidence(known), evidence(unsupported))).toMatchObject({
+ changes: [{ state: "unknown" }],
+ });
+ const partialMatch = facts("f");
+ partialMatch.state = "partial";
+ partialMatch.issues = ["Later imports were not examined"];
+ expect(await compare(evidence(partialMatch), evidence(known))).toMatchObject({
+ changes: [{ state: "unknown" }],
+ });
+ expect(await compare(evidence(), evidence())).toMatchObject({
+ coverage: "unknown",
+ changes: [],
+ });
+});
+
+it("authenticates semantic Evidence and binds display subject and selection identity", async () => {
+ const left = JSON.parse(JSON.stringify(evidence(facts("f"))));
+ left.normalized_result.interface.entries[0].name = "tampered";
+ expect(
+ await compareWasmInterfaces({
+ left,
+ right: evidence(facts("f")),
+ scope: "both",
+ }),
+ ).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
+ const rebound = JSON.parse(JSON.stringify(evidence(facts("f"))));
+ rebound.subject.local_path = "/other.wasm";
+ expect(
+ await compareWasmInterfaces({
+ left: rebound,
+ right: evidence(facts("f")),
+ scope: "both",
+ }),
+ ).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
+});
+
+it("resolves exact retained references and permits cancellation between comparison batches", async () => {
+ const left = evidence(facts("f"));
+ const right = evidence(facts("f", "i64"));
+ const references = {
+ left: { kind: "retained-evidence", evidence_id: left.evidence_id },
+ right: { kind: "retained-evidence", evidence_id: right.evidence_id },
+ scope: "both",
+ };
+ expect(await compareWasmInterfaces(references)).toMatchObject({
+ ok: false,
+ error: { _tag: "EvidenceIntegrityError" },
+ });
+ const result = await compareWasmInterfaces(references, {}, (id) =>
+ id === left.evidence_id ? left : right,
+ );
+ if (!result.ok) throw result.error;
+ expect(result.value.evidence_links).toEqual([
+ left.evidence_id,
+ right.evidence_id,
+ ]);
+ const large = facts("f");
+ large.entries.push(
+ ...Array.from({ length: 2000 }, (_, i) => facts(`f${i}`).entries[0]!),
+ );
+ const controller = new AbortController();
+ const pending = compareWasmInterfaces(
+ { left: evidence(large), right: evidence(large), scope: "both" },
+ { signal: controller.signal },
+ );
+ setImmediate(() => controller.abort());
+ expect(await pending).toMatchObject({
+ ok: false,
+ error: { _tag: "AnalysisCancelledError" },
+ });
+});
+
+it.each([false, true])(
+ "keeps a one-sided unsupported interface unknown with reverse=%s",
+ async (reverse) => {
+ const empty: WasmInterface = { state: "complete", issues: [], entries: [] };
+ const unsupported = facts("exception");
+ unsupported.entries[0]!.signature = null;
+ unsupported.entries[0]!.issue = "Unsupported exnref global type";
+ const sides = reverse ? [unsupported, empty] : [empty, unsupported];
+ expect(await compare(evidence(sides[0]), evidence(sides[1]))).toMatchObject(
+ {
+ coverage: "partial",
+ summary: { added: 0, removed: 0, unknown: 1 },
+ changes: [{ state: "unknown" }],
+ },
+ );
+ },
+);
diff --git a/src/application/wasm/WasmInterfaceComparisonService.ts b/src/application/wasm/WasmInterfaceComparisonService.ts
new file mode 100644
index 000000000..f920a5dea
--- /dev/null
+++ b/src/application/wasm/WasmInterfaceComparisonService.ts
@@ -0,0 +1,250 @@
+import {
+ WASM_INTERFACE_REPRESENTATION_BYTES,
+ wasmInterfaceEntryRepresentationBytes,
+} from "../../domain/wasm/wasmInterface.js";
+import { setImmediate } from "node:timers/promises";
+import {
+ compareWasmInterfacesInputSchema,
+ wasmInterfaceComparisonSchema,
+} from "../../contracts/wasm/wasmComparisonContracts.js";
+import {
+ createEvidence,
+ parseEvidence,
+ type Evidence,
+} from "../../domain/evidence.js";
+import {
+ wasmArtifactSchema,
+ type WasmArtifact,
+} from "../../domain/wasm/wasmArtifact.js";
+import type { WasmInterfaceEntry } from "../../domain/wasm/wasmInterface.js";
+import {
+ AnalysisCancelledError,
+ AnalysisInputError,
+ AnalysisResourceConstraintError,
+} from "../../domain/analysisErrorCore.js";
+import { AnalysisError } from "../../domain/analysisErrorBase.js";
+import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
+import { err, ok, type Result } from "../../domain/result.js";
+import { ARTIFACT_COMPARISON_PROVIDER } from "../InvestigationProviders.js";
+import {
+ resolvePairedEvidenceRequest,
+ type EvidenceLookup,
+} from "../EvidenceInputResolver.js";
+import type { ExecutionOptions } from "../AnalysisProvider.js";
+import type { z } from "zod";
+
+const OPERATION = "compare_wasm_interfaces";
+const parseInspection = (
+ raw: unknown,
+): { evidence: Evidence; result: WasmArtifact } => {
+ const evidence = parseEvidence(raw);
+ if (evidence.operation !== "inspect_wasm_artifact")
+ throw new TypeError("Expected complete inspect_wasm_artifact Evidence");
+ const result = wasmArtifactSchema.parse(evidence.normalized_result);
+ if (
+ evidence.subject?.digest.sha256 !== result.artifact.sha256 ||
+ evidence.subject.local_path !== result.artifact.path ||
+ evidence.parameters.path !== result.artifact.path
+ )
+ throw new TypeError(
+ "WASM Evidence subject/selection does not match normalized artifact identity",
+ );
+ return { evidence, result };
+};
+
+/** Compare only producer-owned typed facts; no raw WAT or original file is reread. */
+export const compareWasmInterfaces = async (
+ rawInput: unknown,
+ options?: ExecutionOptions,
+ lookup?: EvidenceLookup,
+): Promise> => {
+ if (options?.signal?.aborted)
+ return err(new AnalysisCancelledError(OPERATION));
+ const parsed = compareWasmInterfacesInputSchema.safeParse(rawInput);
+ if (!parsed.success)
+ return err(
+ analysisInputErrorFromIssues(OPERATION, parsed.error.issues, rawInput),
+ );
+ const resolved = resolvePairedEvidenceRequest(parsed.data, lookup);
+ if (!resolved.ok) return resolved;
+ try {
+ await setImmediate();
+ options?.signal?.throwIfAborted();
+ const left = parseInspection(resolved.value.left);
+ const right = parseInspection(resolved.value.right);
+ const scope = parsed.data.scope;
+ const selected = (entry: WasmInterfaceEntry): boolean =>
+ scope === "both" ||
+ entry.direction === (scope === "imports" ? "import" : "export");
+ const groups = new Map<
+ string,
+ {
+ left: WasmInterfaceEntry[];
+ right: WasmInterfaceEntry[];
+ entry: WasmInterfaceEntry;
+ }
+ >();
+ let work = 0;
+ for (const [side, inspection] of [
+ ["left", left],
+ ["right", right],
+ ] as const) {
+ for (const entry of inspection.result.interface?.entries ?? []) {
+ if (++work % 256 === 0) {
+ await setImmediate();
+ options?.signal?.throwIfAborted();
+ }
+ if (!selected(entry)) continue;
+ const key = JSON.stringify([entry.direction, entry.module, entry.name]);
+ let group = groups.get(key);
+ if (group === undefined) {
+ group = { left: [], right: [], entry };
+ groups.set(key, group);
+ }
+ group[side].push(entry);
+ }
+ }
+ const knownInventory = (inspection: typeof left): boolean =>
+ inspection.result.interface?.state === "complete";
+ const changes: z.infer["changes"] =
+ [];
+ const summary = {
+ added: 0,
+ removed: 0,
+ changed: 0,
+ unchanged: 0,
+ unknown: 0,
+ };
+ let remaining = WASM_INTERFACE_REPRESENTATION_BYTES;
+ for (const group of groups.values()) {
+ remaining -=
+ 512 +
+ group.left.reduce(
+ (sum, entry) => sum + wasmInterfaceEntryRepresentationBytes(entry),
+ 0,
+ ) +
+ group.right.reduce(
+ (sum, entry) => sum + wasmInterfaceEntryRepresentationBytes(entry),
+ 0,
+ );
+ if (remaining < 0)
+ throw new AnalysisResourceConstraintError(
+ OPERATION,
+ "capacity",
+ "WASM comparison exceeds typed interface representation capacity",
+ { maximum_bytes: WASM_INTERFACE_REPRESENTATION_BYTES },
+ );
+ if (++work % 256 === 0) {
+ await setImmediate();
+ options?.signal?.throwIfAborted();
+ }
+ let state: keyof typeof summary;
+ let reason: string | null = null;
+ const l = group.left[0];
+ const r = group.right[0];
+ if (group.left.length > 1 || group.right.length > 1) {
+ state = "unknown";
+ reason =
+ "Duplicate interface names are ambiguous; all occurrences retained";
+ } else if (!knownInventory(left) || !knownInventory(right)) {
+ state = "unknown";
+ reason =
+ "Incomplete interface inventories cannot establish per-key uniqueness or absence";
+ } else if (
+ [l, r].some(
+ (entry) =>
+ entry !== undefined &&
+ (entry.signature === null ||
+ entry.issue !== null ||
+ entry.kind === "unknown"),
+ )
+ ) {
+ state = "unknown";
+ reason =
+ "Signature/type semantics were not admitted for every present entry";
+ } else if (l === undefined) state = "added";
+ else if (r === undefined) state = "removed";
+ else if (l.kind !== r.kind) state = "changed";
+ else
+ state =
+ JSON.stringify(l.signature) === JSON.stringify(r.signature)
+ ? "unchanged"
+ : "changed";
+ summary[state]++;
+ changes.push({
+ direction: group.entry.direction,
+ module: group.entry.module,
+ name: group.entry.name,
+ state,
+ left: group.left,
+ right: group.right,
+ reason,
+ });
+ }
+ const compareText = (a: string, b: string): number =>
+ a < b ? -1 : a > b ? 1 : 0;
+ changes.sort(
+ (a, b) =>
+ compareText(a.direction, b.direction) ||
+ compareText(a.module ?? "", b.module ?? "") ||
+ compareText(a.name, b.name),
+ );
+ const coverage =
+ left.result.interface === undefined &&
+ right.result.interface === undefined
+ ? "unknown"
+ : !knownInventory(left) || !knownInventory(right) || summary.unknown > 0
+ ? "partial"
+ : "complete-within-inputs";
+ const limitations = [
+ "Exact declared interface equality is not runtime/ABI compatibility, successful linking or original-source identity.",
+ "Typed facts come from the selected validated WABT decoding; unsupported signature semantics and duplicate names remain unknown.",
+ ];
+ const report = wasmInterfaceComparisonSchema.parse({
+ left: {
+ evidence_id: left.evidence.evidence_id,
+ artifact: left.result.artifact,
+ },
+ right: {
+ evidence_id: right.evidence.evidence_id,
+ artifact: right.result.artifact,
+ },
+ scope,
+ coverage,
+ changes,
+ summary,
+ limitations,
+ });
+ await setImmediate();
+ options?.signal?.throwIfAborted();
+ return ok(
+ createEvidence(undefined, ARTIFACT_COMPARISON_PROVIDER, {
+ operation: OPERATION,
+ predicateType: "rea.wasm-interface-comparison",
+ parameters: {
+ left_evidence_id: left.evidence.evidence_id,
+ right_evidence_id: right.evidence.evidence_id,
+ scope,
+ },
+ result: report,
+ confidence: "derived",
+ authority: "analyst-inference",
+ limitations,
+ evidenceLinks: [left.evidence.evidence_id, right.evidence.evidence_id],
+ }),
+ );
+ } catch (cause: unknown) {
+ if (options?.signal?.aborted)
+ return err(new AnalysisCancelledError(OPERATION));
+ if (cause instanceof AnalysisError) return err(cause);
+ return err(
+ new AnalysisInputError(OPERATION, { cause }, [
+ {
+ path: [],
+ reason: "invalid_value",
+ message: cause instanceof Error ? cause.message : String(cause),
+ },
+ ]),
+ );
+ }
+};
diff --git a/src/artifacts/AsarArtifactReader.test.ts b/src/artifacts/AsarArtifactReader.test.ts
deleted file mode 100644
index 54eb78cd4..000000000
--- a/src/artifacts/AsarArtifactReader.test.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { describe, expect, it } from "vitest";
-
-import { orderAsarListing } from "./AsarArtifactReader.js";
-
-describe("orderAsarListing", () => {
- it("orders host-spelled listings by their portable archive path", () => {
- const posix = orderAsarListing(["/a0", "/a", "/a/b"], "/");
- const windows = orderAsarListing(["\\a0", "\\a", "\\a\\b"], "\\");
-
- // "/" (0x2F) precedes "0" (0x30); "\" (0x5C) would follow it.
- expect(posix.map(({ path }) => path)).toEqual(["a", "a/b", "a0"]);
- expect(windows.map(({ path }) => path)).toEqual(["a", "a/b", "a0"]);
- expect(windows.map(({ listed }) => listed)).toEqual([
- "\\a",
- "\\a\\b",
- "\\a0",
- ]);
- });
-
- it("keeps POSIX backslashes as literal filename characters", () => {
- expect(orderAsarListing(["/a\\b"], "/")).toEqual([
- { listed: "/a\\b", path: "a\\b" },
- ]);
- });
-});
diff --git a/src/artifacts/AsarArtifactReader.ts b/src/artifacts/AsarArtifactReader.ts
index b7a235f4b..c988ba056 100644
--- a/src/artifacts/AsarArtifactReader.ts
+++ b/src/artifacts/AsarArtifactReader.ts
@@ -13,7 +13,7 @@ import { isAbsolute, join, relative, sep } from "node:path";
import { Readable, Transform } from "node:stream";
import { pipeline } from "node:stream/promises";
-import { listPackage, statFile, uncache } from "@electron/asar";
+import { getRawHeader, uncache } from "@electron/asar";
import {
ArtifactReaderFailure,
@@ -26,7 +26,11 @@ import { closeAsarHandle, readValidatedAsarEntry } from "./AsarEntryStream.js";
import { admitAsarHeader } from "./AsarHeader.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import { readFileHandleChunks } from "../filesystem/readFileHandleChunks.js";
-import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
+import {
+ readAsarListing,
+ type AsarListedEntry,
+ type AsarMetadata,
+} from "./AsarListing.js";
/**
* Official Electron ASAR adapter with range-streamed member reads.
@@ -51,7 +55,7 @@ export class AsarArtifactReader implements ArtifactReader {
) {}
async *entries(signal?: AbortSignal): AsyncIterable {
- let paths: readonly AsarListedPath[];
+ let paths: readonly AsarListedEntry[];
try {
await this.prepareContainer(undefined, signal);
paths = await this.#withSnapshotOperation(async () => {
@@ -62,8 +66,9 @@ export class AsarArtifactReader implements ArtifactReader {
this.#archiveSize = admitted.archiveBytes;
this.#headerSize = admitted.headerBytes;
abortIfNeeded(signal);
- const ordered = orderAsarListing(
- listPackage(snapshotPath, { isPack: false }),
+ const ordered = await readAsarListing(
+ getRawHeader(snapshotPath).header,
+ signal,
);
const archiveMetadata = await lstat(snapshotPath);
if (!archiveMetadata.isFile() || archiveMetadata.isSymbolicLink())
@@ -77,34 +82,25 @@ export class AsarArtifactReader implements ArtifactReader {
} catch (cause: unknown) {
throw asarFailure(this.path, "inventory", cause);
}
- for (const { listed, path } of paths) {
+ for (const { metadata, path } of paths) {
abortIfNeeded(signal);
- if (path.length === 0) continue;
- const providerPath = listed.startsWith(sep)
- ? listed.slice(sep.length)
- : listed;
- let metadata: ReturnType;
- try {
- metadata = statFile(this.#snapshotPath(), providerPath, false);
- } catch (cause: unknown) {
- throw asarFailure(this.path, `stat ${path}`, cause);
- }
+ const providerPath = path.split("/").join(sep);
const kind =
- "files" in metadata
+ metadata.files !== undefined
? "directory"
- : "link" in metadata
+ : metadata.link !== undefined
? "symlink"
: "file";
const entry: ArtifactEntry = {
path,
kind,
- declaredSize: "size" in metadata ? metadata.size : null,
+ declaredSize: metadata.size ?? null,
compressedSize: null,
- executable: "executable" in metadata && metadata.executable,
+ executable: metadata.executable === true,
encrypted: false,
byteOffset: null,
declaredSha256:
- "integrity" in metadata &&
+ metadata.integrity !== undefined &&
metadata.integrity.algorithm === "SHA256" &&
/^[a-f0-9]{64}$/u.test(metadata.integrity.hash)
? metadata.integrity.hash
@@ -603,7 +599,7 @@ export class AsarArtifactReader implements ArtifactReader {
}
}
-type AsarFileMetadata = Extract, { size: number }>;
+type AsarFileMetadata = AsarMetadata & { readonly size: number };
type AsarEntryState = {
readonly metadata: AsarFileMetadata;
readonly entry: ArtifactEntry;
@@ -652,12 +648,11 @@ const asarUnpackedEntryFailure = (
};
const isAsarFileMetadata = (
- metadata: ReturnType,
+ metadata: AsarMetadata,
): metadata is AsarFileMetadata =>
- "size" in metadata &&
typeof metadata.size === "number" &&
- !("files" in metadata) &&
- !("link" in metadata);
+ metadata.files === undefined &&
+ metadata.link === undefined;
const parseArchiveOffset = (value: string): number | undefined => {
if (!/^(?:0|[1-9][0-9]*)$/u.test(value)) return undefined;
@@ -699,33 +694,6 @@ const waitForOperation = async (
});
};
-interface AsarListedPath {
- readonly listed: string;
- readonly path: string;
-}
-
-/**
- * Pair each host-spelled listing with its portable archive path, ordered by
- * the portable path so Windows and POSIX hosts agree.
- *
- * @electron/asar returns paths assembled with the host path module. Keep that
- * spelling for its API calls, but expose archive paths with portable
- * separators. On POSIX, backslashes can be literal filename characters and
- * must remain untouched.
- */
-export const orderAsarListing = (
- listed: readonly string[],
- separator: string = sep,
-): AsarListedPath[] =>
- listed
- .map((entry) => ({ listed: entry, path: toArtifactPath(entry, separator) }))
- .sort((left, right) => compareUnicodeCodePoints(left.path, right.path));
-
-const toArtifactPath = (listed: string, separator: string): string => {
- const portable = separator === "\\" ? listed.replaceAll("\\", "/") : listed;
- return portable.replace(/^\/+|\/+$/gu, "");
-};
-
const asarFailure = (
path: string,
operation: string,
diff --git a/src/artifacts/AsarListing.ts b/src/artifacts/AsarListing.ts
new file mode 100644
index 000000000..60875e93b
--- /dev/null
+++ b/src/artifacts/AsarListing.ts
@@ -0,0 +1,110 @@
+import { setImmediate } from "node:timers/promises";
+import { z } from "zod";
+
+import { ArtifactReaderFailure } from "./ArtifactReader.js";
+import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
+
+/** Capacity for expanded paths and listing records, independent of packed bytes. */
+const MAX_LISTING_BYTES = 64 * 1024 * 1024;
+const nodeSchema = z.looseObject({
+ files: z
+ .custom>(
+ (value) =>
+ value !== null && typeof value === "object" && !Array.isArray(value),
+ )
+ .optional(),
+ link: z.string().optional(),
+ size: z.number().nonnegative().safe().optional(),
+ offset: z.string().optional(),
+ unpacked: z.boolean().optional(),
+ executable: z.boolean().optional(),
+ integrity: z
+ .looseObject({ algorithm: z.string(), hash: z.string() })
+ .optional(),
+});
+export type AsarMetadata = z.infer;
+export interface AsarListedEntry {
+ readonly path: string;
+ readonly metadata: AsarMetadata;
+}
+
+function* ownChildren(files: Record) {
+ for (const name in files)
+ if (Object.hasOwn(files, name)) yield [name, files[name]] as const;
+}
+
+/** Decode each node once; admit expanded paths before concatenation and retain no recursive lookup. */
+export const readAsarListing = async (
+ header: unknown,
+ signal?: AbortSignal,
+): Promise => {
+ const root = nodeSchema.parse(header);
+ if (root.files === undefined)
+ throw new ArtifactReaderFailure("format", "ASAR root has no files table");
+ const entries: AsarListedEntry[] = [];
+ const pending = [{ parent: "", children: ownChildren(root.files) }];
+ let retainedBytes = 0;
+ let examined = 0;
+ while (pending.length > 0) {
+ if (examined++ % 128 === 0) {
+ await setImmediate();
+ if (signal?.aborted)
+ throw new ArtifactReaderFailure(
+ "cancelled",
+ "ASAR operation cancelled",
+ );
+ }
+ const frame = pending.at(-1);
+ if (frame === undefined) break;
+ const next = frame.children.next();
+ if (next.done) {
+ pending.pop();
+ continue;
+ }
+ const [name, raw] = next.value;
+ // A header member is one component, not a host path to normalize. POSIX
+ // backslashes remain literal; Windows cannot represent them as components.
+ if (
+ name.length === 0 ||
+ name === "." ||
+ name === ".." ||
+ name.includes("/") ||
+ name.includes("\0") ||
+ (process.platform === "win32" && name.includes("\\"))
+ )
+ throw new ArtifactReaderFailure(
+ "path",
+ "ASAR header has an unsafe path component",
+ );
+ const units =
+ frame.parent.length + name.length + (frame.parent.length === 0 ? 0 : 1);
+ // UTF-16 storage, plus records and traversal bookkeeping. Charge every
+ // occurrence even when the engine can share a string prefix.
+ retainedBytes += units * 2 + 256;
+ if (retainedBytes > MAX_LISTING_BYTES)
+ throw new ArtifactReaderFailure(
+ "limit",
+ "ASAR expanded listing exceeds its 64 MiB representation budget",
+ );
+ const path = frame.parent.length === 0 ? name : `${frame.parent}/${name}`;
+ const metadata = nodeSchema.parse(raw);
+ if (
+ metadata.files === undefined &&
+ metadata.link === undefined &&
+ metadata.size === undefined
+ )
+ throw new ArtifactReaderFailure(
+ "format",
+ "ASAR entry has no directory, link or file metadata",
+ );
+ entries.push({ path, metadata });
+ if (metadata.files !== undefined)
+ pending.push({
+ parent: path,
+ children: ownChildren(metadata.files),
+ });
+ }
+ return entries.sort((left, right) =>
+ compareUnicodeCodePoints(left.path, right.path),
+ );
+};
diff --git a/src/artifacts/RegularFileAdmission.test.ts b/src/artifacts/RegularFileAdmission.test.ts
index 17926799a..97f61fc34 100644
--- a/src/artifacts/RegularFileAdmission.test.ts
+++ b/src/artifacts/RegularFileAdmission.test.ts
@@ -1,5 +1,6 @@
import type { BigIntStats, Stats } from "node:fs";
import type { FileHandle } from "node:fs/promises";
+import { tmpdir } from "node:os";
import { afterEach, expect, it, vi } from "vitest";
import {
@@ -88,3 +89,36 @@ it("compares available nanosecond timestamps without rounding away source change
sameRegularFileState(before, { ...before, ctimeNs: before.ctimeNs + 1n }),
).toBe(false);
});
+
+it.each([
+ ["EACCES", -13, "keeps"],
+ ["ENXIO", -6, "reclassifies"],
+] as const)(
+ "%s from opening a non-regular path: admission %s the failure",
+ async (code, errno, outcome) => {
+ const failure = Object.assign(new Error(`open ${code}`), { code, errno });
+ mockOpen.mockRejectedValue(failure);
+ // The selected path itself is a directory that stat can still describe.
+ const cause = await openRegularFile(tmpdir(), { symlinks: "follow" }).catch(
+ (rejected: unknown) => rejected,
+ );
+ if (outcome === "keeps") expect(cause).toBe(failure);
+ else expect(cause).toBeInstanceOf(NonRegularFileReadError);
+ },
+);
+
+it("keeps cancellation that arrives during the non-regular metadata probe", async () => {
+ const controller = new AbortController();
+ const cancellation = new Error("cancelled during probe");
+ mockOpen.mockImplementation(async () => {
+ // The probe stats the path after this rejection; abort before it settles.
+ queueMicrotask(() => controller.abort(cancellation));
+ throw Object.assign(new Error("open ENXIO"), { code: "ENXIO", errno: -6 });
+ });
+ await expect(
+ openRegularFile(tmpdir(), {
+ symlinks: "follow",
+ signal: controller.signal,
+ }),
+ ).rejects.toBe(cancellation);
+});
diff --git a/src/artifacts/apple/NibArchive.test.ts b/src/artifacts/apple/NibArchive.test.ts
index 10579d011..665617d28 100644
--- a/src/artifacts/apple/NibArchive.test.ts
+++ b/src/artifacts/apple/NibArchive.test.ts
@@ -36,6 +36,22 @@ const archiveWithOverlappingObjectRanges = (objectCount: number): Buffer => {
};
describe("NIBArchive decoder", () => {
+ it("keeps leading U+FEFF in length-prefixed keys and class names", () => {
+ const archive = encodeArchive({
+ classes: ["\uFEFFWidget"],
+ objects: [{ classIndex: 0, values: { count: 1, "\uFEFFcount": 2 } }],
+ });
+ expect(decodeNibArchive(archive).objects).toEqual([
+ {
+ id: 0,
+ class_name: "\uFEFFWidget",
+ values: { count: 1, "\uFEFFcount": 2 },
+ },
+ ]);
+ const invalid = Buffer.from(archive);
+ invalid[invalid.indexOf(Buffer.from("\uFEFFcount"))] = 0xff;
+ expect(() => decodeNibArchive(invalid)).toThrow(/not valid UTF-8/u);
+ });
it("decodes bounded object, key, class, and reference tables", () => {
const archive = encodeArchive({
classes: ["NSView\0", "NSString\0"],
diff --git a/src/browser/BrowserProcessStartup.ts b/src/browser/BrowserProcessStartup.ts
index 85eae0553..48367c432 100644
--- a/src/browser/BrowserProcessStartup.ts
+++ b/src/browser/BrowserProcessStartup.ts
@@ -14,6 +14,7 @@ export class BrowserStartupError extends Error {
readonly failure: BrowserStartupFailure;
readonly executable: string;
readonly elapsedMs: number;
+ readonly startupTimeoutMs: number;
readonly exitCode: number | null;
readonly signalCode: NodeJS.Signals | null;
readonly stderr: string;
@@ -23,6 +24,7 @@ export class BrowserStartupError extends Error {
readonly failure: BrowserStartupFailure;
readonly executable: string;
readonly elapsedMs: number;
+ readonly startupTimeoutMs: number;
readonly exitCode: number | null;
readonly signalCode: NodeJS.Signals | null;
readonly stderr: string;
@@ -33,12 +35,22 @@ export class BrowserStartupError extends Error {
this.failure = diagnostics.failure;
this.executable = diagnostics.executable;
this.elapsedMs = diagnostics.elapsedMs;
+ this.startupTimeoutMs = diagnostics.startupTimeoutMs;
this.exitCode = diagnostics.exitCode;
this.signalCode = diagnostics.signalCode;
this.stderr = diagnostics.stderr;
}
}
+/**
+ * Single owner of the wait for Chrome to publish its DevToolsActivePort.
+ *
+ * Callers that need a different budget pass `timeoutMs`; nobody else should
+ * restate this number, because a per-caller literal silently changes how long
+ * a cold Chrome launch is tolerated before the failure names its ceiling.
+ */
+export const BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS = 60_000;
+
/** Owned child process and startup policy for Chrome CDP discovery. */
export interface BrowserStartupOptions {
readonly child: ChildProcess;
@@ -53,23 +65,23 @@ export interface BrowserStartupOptions {
export const waitForBrowserDevtoolsPort = async (
options: BrowserStartupOptions,
): Promise => {
- const timeoutMs = options.timeoutMs ?? 15_000;
+ const timeoutMs = options.timeoutMs ?? BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS;
const pollIntervalMs = options.pollIntervalMs ?? 25;
const startedAt = performance.now();
- const terminal = childTermination(options, startedAt);
+ const terminal = childTermination(options, startedAt, timeoutMs);
const deadline = startedAt + timeoutMs;
while (true) {
const terminalState = currentTerminalState(options.child);
if (terminalState !== null)
- throw startupError(options, startedAt, terminalState);
+ throw startupError(options, startedAt, timeoutMs, terminalState);
const port = await readDevtoolsPort(options.activePortPath);
if (port !== null) return port;
const remainingMs = deadline - performance.now();
if (remainingMs <= 0)
- throw startupError(options, startedAt, { failure: "timeout" });
+ throw startupError(options, startedAt, timeoutMs, { failure: "timeout" });
const outcome = await Promise.race([
terminal,
@@ -88,11 +100,12 @@ type TerminalState =
const childTermination = (
options: BrowserStartupOptions,
startedAt: number,
+ startupTimeoutMs: number,
): Promise =>
new Promise((resolve) => {
options.child.once("error", (cause: unknown) => {
resolve(
- startupError(options, startedAt, {
+ startupError(options, startedAt, startupTimeoutMs, {
failure: "spawn-error",
cause,
}),
@@ -103,6 +116,7 @@ const childTermination = (
startupError(
options,
startedAt,
+ startupTimeoutMs,
options.child.signalCode === null
? { failure: "exited" }
: { failure: "signalled" },
@@ -120,6 +134,7 @@ const currentTerminalState = (child: ChildProcess): TerminalState | null => {
const startupError = (
options: BrowserStartupOptions,
startedAt: number,
+ startupTimeoutMs: number,
state: TerminalState,
): BrowserStartupError =>
new BrowserStartupError(
@@ -127,6 +142,7 @@ const startupError = (
failure: state.failure,
executable: options.executable,
elapsedMs: Math.max(0, Math.round(performance.now() - startedAt)),
+ startupTimeoutMs,
exitCode: options.child.exitCode,
signalCode: options.child.signalCode,
stderr: options.stderr(),
@@ -156,9 +172,10 @@ const browserStartupMessage = (
diagnostics: {
readonly executable: string;
readonly elapsedMs: number;
+ readonly startupTimeoutMs: number;
readonly exitCode: number | null;
readonly signalCode: NodeJS.Signals | null;
readonly stderr: string;
},
): string =>
- `Chrome CDP startup ${failure}; executable=${diagnostics.executable}; elapsed_ms=${String(diagnostics.elapsedMs)}; exit_code=${diagnostics.exitCode === null ? "null" : String(diagnostics.exitCode)}; signal=${diagnostics.signalCode ?? "null"}; stderr=${diagnostics.stderr.length === 0 ? "" : diagnostics.stderr}`;
+ `Chrome CDP startup ${failure}; executable=${diagnostics.executable}; elapsed_ms=${String(diagnostics.elapsedMs)}; startup_timeout_ms=${String(diagnostics.startupTimeoutMs)}; exit_code=${diagnostics.exitCode === null ? "null" : String(diagnostics.exitCode)}; signal=${diagnostics.signalCode ?? "null"}; stderr=${diagnostics.stderr.length === 0 ? "" : diagnostics.stderr}`;
diff --git a/src/browser/CdpBrowserProvider.ts b/src/browser/CdpBrowserProvider.ts
index f4d898a68..744a82983 100644
--- a/src/browser/CdpBrowserProvider.ts
+++ b/src/browser/CdpBrowserProvider.ts
@@ -49,13 +49,14 @@ import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import type { BrowserObservationOperation } from "../domain/browserObservationErrors.js";
import { err, ok, type Result } from "../domain/result.js";
import {
- discoverCdpEndpoint,
hasCdpTargetWebSocket,
type CdpEndpointDiscovery,
type CdpEndpointTarget,
} from "./CdpEndpoint.js";
import {
+ attachCdpTargetSession,
closeCdpTargetSession,
+ openCdpEndpoint,
openCdpTargetSession,
type CdpTargetSession,
} from "./CdpTargetSession.js";
@@ -82,11 +83,13 @@ export class CdpBrowserProvider implements BrowserObservationPort {
options: ExecutionOptions = {},
): Promise> {
try {
- const discovery = await discoverCdpEndpoint(
+ const endpoint = await openCdpEndpoint(
input.cdp_endpoint,
"list_browser_targets",
options.signal,
);
+ await endpoint.connection?.close();
+ const { discovery } = endpoint;
const selected = selectTargets(
discovery,
input.allowed_origins.length === 0
@@ -169,45 +172,19 @@ export class CdpBrowserProvider implements BrowserObservationPort {
input: ObserveWebSessionInput,
options: ExecutionOptions = {},
): Promise> {
- let targetSession: CdpTargetSession | undefined;
try {
- const discovery = await discoverCdpEndpoint(
- input.cdp_endpoint,
- "observe_web_session",
- options.signal,
- );
- const target = authorizeTarget(discovery, input);
- const scopedInput = scopeToTargetOrigin(input, target);
- targetSession = await openCdpTargetSession(
- discovery,
- target,
- "observe_web_session",
- options.signal,
- );
return ok(
webObservationSessionSchema.parse(
- await observeCdpSession({
- connection: targetSession.connection,
- sessionId: targetSession.sessionId,
- discovery,
- target,
- input: scopedInput,
- ...(options.signal === undefined ? {} : { signal: options.signal }),
- ...(options.progress === undefined
- ? {}
- : { progress: options.progress }),
- }),
+ await withTargetSession(
+ input,
+ "observe_web_session",
+ options,
+ observeCdpSession,
+ ),
),
);
} catch (cause: unknown) {
return err(providerError(cause, "observe_web_session"));
- } finally {
- if (targetSession !== undefined)
- await closeCdpTargetSession(
- targetSession,
- CLEANUP_DOMAINS,
- options.signal,
- );
}
}
@@ -215,45 +192,19 @@ export class CdpBrowserProvider implements BrowserObservationPort {
input: DiscoverWebMcpToolsInput,
options: ExecutionOptions = {},
): Promise> {
- let targetSession: CdpTargetSession | undefined;
try {
- const discovery = await discoverCdpEndpoint(
- input.cdp_endpoint,
- "discover_webmcp_tools",
- options.signal,
- );
- const target = authorizeTarget(discovery, input);
- const scopedInput = scopeToTargetOrigin(input, target);
- targetSession = await openCdpTargetSession(
- discovery,
- target,
- "discover_webmcp_tools",
- options.signal,
- );
return ok(
webMcpDiscoverySchema.parse(
- await discoverWebMcp({
- connection: targetSession.connection,
- sessionId: targetSession.sessionId,
- discovery,
- target,
- input: scopedInput,
- ...(options.signal === undefined ? {} : { signal: options.signal }),
- ...(options.progress === undefined
- ? {}
- : { progress: options.progress }),
- }),
+ await withTargetSession(
+ input,
+ "discover_webmcp_tools",
+ options,
+ discoverWebMcp,
+ ),
),
);
} catch (cause: unknown) {
return err(providerError(cause, "discover_webmcp_tools"));
- } finally {
- if (targetSession !== undefined)
- await closeCdpTargetSession(
- targetSession,
- CLEANUP_DOMAINS,
- options.signal,
- );
}
}
@@ -273,45 +224,19 @@ export class CdpBrowserProvider implements BrowserObservationPort {
input: CaptureWebScreenshotInput,
options: ExecutionOptions = {},
): Promise> {
- let targetSession: CdpTargetSession | undefined;
try {
- const discovery = await discoverCdpEndpoint(
- input.cdp_endpoint,
- "capture_web_screenshot",
- options.signal,
- );
- const target = authorizeTarget(discovery, input);
- const scopedInput = scopeToTargetOrigin(input, target);
- targetSession = await openCdpTargetSession(
- discovery,
- target,
- "capture_web_screenshot",
- options.signal,
- );
return ok(
webScreenshotSchema.parse(
- await captureCdpScreenshot({
- connection: targetSession.connection,
- sessionId: targetSession.sessionId,
- discovery,
- target,
- input: scopedInput,
- ...(options.signal === undefined ? {} : { signal: options.signal }),
- ...(options.progress === undefined
- ? {}
- : { progress: options.progress }),
- }),
+ await withTargetSession(
+ input,
+ "capture_web_screenshot",
+ options,
+ captureCdpScreenshot,
+ ),
),
);
} catch (cause: unknown) {
return err(providerError(cause, "capture_web_screenshot"));
- } finally {
- if (targetSession !== undefined)
- await closeCdpTargetSession(
- targetSession,
- CLEANUP_DOMAINS,
- options.signal,
- );
}
}
@@ -336,44 +261,83 @@ export class CdpBrowserProvider implements BrowserObservationPort {
options: ExecutionOptions,
operation: "inspect_web_page" | "analyze_web_bundle",
): Promise {
- let targetSession: CdpTargetSession | undefined;
- try {
- const discovery = await discoverCdpEndpoint(
- input.cdp_endpoint,
- operation,
- options.signal,
- );
- const target = authorizeTarget(discovery, input);
- const scopedInput = scopeToTargetOrigin(input, target);
- targetSession = await openCdpTargetSession(
- discovery,
- target,
- operation,
- options.signal,
- );
- return await capturePage({
- connection: targetSession.connection,
- sessionId: targetSession.sessionId,
- operation,
- discovery,
- target,
- input: scopedInput,
- ...(options.signal === undefined ? {} : { signal: options.signal }),
- ...(options.progress === undefined
- ? {}
- : { progress: options.progress }),
- });
- } finally {
- if (targetSession !== undefined)
- await closeCdpTargetSession(
- targetSession,
- CLEANUP_DOMAINS,
- options.signal,
- );
- }
+ return await withTargetSession(
+ input,
+ operation,
+ options,
+ async (session) => await capturePage({ ...session, operation }),
+ );
}
}
+/** Facts and transport handed to one target operation. */
+interface CdpTargetOperationSession {
+ readonly connection: CdpTargetSession["connection"];
+ readonly sessionId: CdpTargetSession["sessionId"];
+ readonly discovery: CdpEndpointDiscovery;
+ readonly target: CdpEndpointTarget;
+ readonly input: Input;
+ readonly signal?: AbortSignal;
+ readonly progress?: NonNullable;
+}
+
+/** Authorize one discovered target, run work in its session, and close REA's socket. */
+const withTargetSession = async <
+ Input extends Pick<
+ InspectWebPageInput,
+ "cdp_endpoint" | "target_id" | "allowed_origins"
+ >,
+ Output,
+>(
+ input: Input,
+ operation: BrowserObservationOperation,
+ options: ExecutionOptions,
+ work: (session: CdpTargetOperationSession ) => Promise,
+): Promise => {
+ const endpoint = await openCdpEndpoint(
+ input.cdp_endpoint,
+ operation,
+ options.signal,
+ );
+ let targetSession: CdpTargetSession | undefined;
+ try {
+ const target = authorizeTarget(endpoint.discovery, input);
+ const scopedInput = scopeToTargetOrigin(input, target);
+ targetSession =
+ endpoint.connection === undefined
+ ? await openCdpTargetSession(
+ endpoint.discovery,
+ target,
+ operation,
+ options.signal,
+ )
+ : await attachCdpTargetSession(
+ endpoint.connection,
+ target,
+ operation,
+ options.signal,
+ );
+ return await work({
+ connection: targetSession.connection,
+ sessionId: targetSession.sessionId,
+ discovery: endpoint.discovery,
+ target,
+ input: scopedInput,
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
+ ...(options.progress === undefined ? {} : { progress: options.progress }),
+ });
+ } finally {
+ if (targetSession !== undefined)
+ await closeCdpTargetSession(
+ targetSession,
+ CLEANUP_DOMAINS,
+ options.signal,
+ );
+ // An unadopted browser socket is closed here; closing is idempotent.
+ await endpoint.connection?.close();
+ }
+};
+
const selectTargets = (
discovery: CdpEndpointDiscovery,
allowedOrigins: ReadonlySet | undefined,
diff --git a/src/browser/CdpCaptureStorage.test.ts b/src/browser/CdpCaptureStorage.test.ts
new file mode 100644
index 000000000..51bb49a08
--- /dev/null
+++ b/src/browser/CdpCaptureStorage.test.ts
@@ -0,0 +1,84 @@
+import { expect, it } from "vitest";
+
+import { inspectWebPageInputSchema } from "../domain/browserObservation.js";
+import { captureStorage } from "./CdpCaptureStorage.js";
+import { CdpCommandRejection } from "./CdpCommandRejection.js";
+import type { CdpConnection } from "./CdpConnection.js";
+
+const NO_CACHE_COMMAND = Symbol("unsupported");
+
+const captureWithCacheResult = async (
+ cacheResult: unknown,
+): Promise>["value"]> => {
+ const connection = {
+ send(method: string): Promise {
+ switch (method) {
+ case "Storage.getUsageAndQuota":
+ return Promise.resolve({ usage: 1, quota: 10 });
+ case "DOMStorage.getDOMStorageItems":
+ return Promise.resolve({ entries: [] });
+ case "IndexedDB.requestDatabaseNames":
+ return Promise.resolve({ databaseNames: [] });
+ case "Network.getCookies":
+ return Promise.resolve({ cookies: [] });
+ case "CacheStorage.requestCacheNames":
+ return cacheResult === NO_CACHE_COMMAND
+ ? Promise.reject(
+ new CdpCommandRejection(
+ "inspect_web_page",
+ method,
+ -32601,
+ "Method not found",
+ ),
+ )
+ : Promise.resolve(cacheResult);
+ case "CacheStorage.requestEntries":
+ return Promise.resolve({ cacheDataEntries: [], returnCount: 0 });
+ default:
+ return Promise.reject(new Error(`Unexpected command: ${method}`));
+ }
+ },
+ } as unknown as CdpConnection;
+ const { value } = await captureStorage(
+ {
+ connection,
+ sessionId: undefined,
+ input: inspectWebPageInputSchema.parse({
+ cdp_endpoint: "http://127.0.0.1:9222",
+ target_id: "fixture-page",
+ include_storage_keys: true,
+ include_storage_fingerprints: true,
+ }),
+ },
+ "https://example.test/",
+ [],
+ );
+ return value;
+};
+
+it.each([
+ ["an observed empty cache list", { caches: [] }, true],
+ ["a response without a cache list", {}, false],
+ ["a non-array cache list", { caches: "nope" }, false],
+ ["a cache list with an unusable entry", { caches: [5] }, false],
+ ["an unsupported cache command", NO_CACHE_COMMAND, false],
+] as const)(
+ "reports fingerprint completeness for %s",
+ async (_label, cacheResult, complete) => {
+ const value = await captureWithCacheResult(cacheResult);
+ expect(value.cache_names).toEqual([]);
+ expect(value.fingerprints_complete).toBe(complete);
+ },
+);
+
+it.each([false, true])(
+ "preserves valid cache evidence beside an unusable entry: %s",
+ async (mixed) => {
+ const cache = { cacheName: "fixture-cache", cacheId: "fixture-cache-id" };
+ const value = await captureWithCacheResult({
+ caches: mixed ? [cache, 5] : [cache],
+ });
+ expect(value.cache_names).toEqual(["fixture-cache"]);
+ expect(value.fingerprints_complete).toBe(!mixed);
+ },
+);
diff --git a/src/browser/CdpCaptureStorage.ts b/src/browser/CdpCaptureStorage.ts
index 8ddcea9a8..1e88ca74a 100644
--- a/src/browser/CdpCaptureStorage.ts
+++ b/src/browser/CdpCaptureStorage.ts
@@ -60,8 +60,8 @@ export const captureStorage = async (
limitations,
)
: undefined;
- const cacheValues = recordsValue(recordValue(cacheRaw)?.caches);
- const caches = capturedCaches(cacheValues);
+ const cacheField = recordValue(cacheRaw)?.caches;
+ const caches = capturedCaches(recordsValue(cacheField));
const fingerprints = context.input.include_storage_fingerprints
? await captureStorageFingerprints({
context,
@@ -78,8 +78,10 @@ export const captureStorage = async (
indexedRaw !== undefined &&
Array.isArray(indexedValues) &&
indexed.length === indexedValues.length &&
- cacheRaw !== undefined &&
- caches.length === cacheValues.length;
+ // Like IndexedDB names, a missing, non-array or partly unusable cache
+ // list is an incomplete enumeration rather than an empty one.
+ Array.isArray(cacheField) &&
+ caches.length === cacheField.length;
return {
value: {
origin,
diff --git a/src/browser/CdpEndpoint.ts b/src/browser/CdpEndpoint.ts
index cf78df183..3db49d28c 100644
--- a/src/browser/CdpEndpoint.ts
+++ b/src/browser/CdpEndpoint.ts
@@ -9,6 +9,7 @@ import type { BrowserObservationOperation } from "../domain/browserObservationEr
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
import { urlDeclaresQueryOrFragment } from "../domain/urlComponents.js";
import { safeParseJson } from "../domain/safeJson.js";
+import type { CdpConnection } from "./CdpConnection.js";
const endpointVersionSchema = z.object({
Browser: z.string().min(1),
@@ -40,6 +41,26 @@ interface CdpDiscoveryResponseLimits {
readonly timeoutMs: number;
}
+const browserSocketVersionSchema = z.object({
+ protocolVersion: z.string().min(1),
+ product: z.string().min(1),
+ revision: z.string(),
+ userAgent: z.string(),
+ jsVersion: z.string(),
+});
+
+const browserSocketTargetsSchema = z.object({
+ targetInfos: z.array(
+ z.object({
+ targetId: z.string().min(1),
+ type: z.string().min(1),
+ title: z.string().default(""),
+ url: z.string(),
+ attached: z.boolean().optional(),
+ }),
+ ),
+});
+
/** Validated direct CDP WebSocket bound to one discovered page target. */
interface CdpPageWebSocketEndpoint {
readonly scope: "page";
@@ -124,6 +145,88 @@ export const discoverCdpEndpoint = async (
};
};
+/**
+ * Run browser-socket discovery work, including its WebSocket handshake, under
+ * the HTTP discovery deadline. Reaching the deadline is a timeout, not a
+ * caller cancellation, so the internal abort is not carried as its cause.
+ */
+export const withCdpDiscoveryDeadline = async (
+ operation: BrowserObservationOperation,
+ signal: AbortSignal | undefined,
+ work: (deadlineSignal: AbortSignal) => Promise,
+ timeoutMs: number = CDP_DISCOVERY_RESPONSE_LIMITS.timeoutMs,
+): Promise => {
+ const deadline = new AbortController();
+ const timer = setTimeout(() => deadline.abort(), timeoutMs);
+ try {
+ return await work(
+ signal === undefined
+ ? deadline.signal
+ : AbortSignal.any([signal, deadline.signal]),
+ );
+ } catch (cause: unknown) {
+ if (deadline.signal.aborted && signal?.aborted !== true)
+ throw new BrowserObservationError(operation, "timeout", {
+ detail: `CDP browser-socket discovery exceeded its ${String(timeoutMs)} ms deadline.`,
+ });
+ throw cause;
+ } finally {
+ clearTimeout(timer);
+ }
+};
+
+/**
+ * Read discovery facts over a browser socket that has no HTTP discovery.
+ * Each reply is bounded by the connection's protocol message limit; callers
+ * apply the discovery deadline through `signal`.
+ */
+export const discoverCdpBrowserSocket = async (
+ connection: CdpConnection,
+ url: string,
+ operation: BrowserObservationOperation,
+ signal?: AbortSignal,
+): Promise => {
+ const versionInput = await connection.send(
+ "Browser.getVersion",
+ {},
+ undefined,
+ signal,
+ );
+ const targetsInput = await connection.send(
+ "Target.getTargets",
+ {},
+ undefined,
+ signal,
+ );
+ const version = parseCdpEndpointValue(
+ browserSocketVersionSchema,
+ versionInput,
+ operation,
+ );
+ const targets = parseCdpEndpointValue(
+ browserSocketTargetsSchema,
+ targetsInput,
+ operation,
+ );
+ return {
+ webSocket: { scope: "browser", url },
+ version: {
+ product: version.product,
+ protocol_version: version.protocolVersion,
+ revision: version.revision,
+ user_agent: version.userAgent,
+ js_version: version.jsVersion,
+ },
+ targets: targets.targetInfos.map((target) => ({
+ id: target.targetId,
+ type: target.type,
+ title: sanitizeTargetTitle(target.title, target.url),
+ url: target.url,
+ attached: target.attached ?? null,
+ })),
+ };
+};
+
const sanitizeTargetTitle = (value: string, targetUrl: string): string => {
let url: URL;
try {
diff --git a/src/browser/CdpSafeMetadata.test.ts b/src/browser/CdpSafeMetadata.test.ts
index 0cff4e094..ef9c42e14 100644
--- a/src/browser/CdpSafeMetadata.test.ts
+++ b/src/browser/CdpSafeMetadata.test.ts
@@ -289,4 +289,25 @@ describe("safe CDP response metadata referrer policy", () => {
expect(captured.response.policies.referrer_policy).toBe(expected);
expect(captured.response.policies.coop).toBe("same-origin");
});
+
+ it.each<[string | number | undefined, number | null]>([
+ ["100", 100],
+ [" 0 ", 0],
+ [100, 100],
+ ["-5", null],
+ ["+12", null],
+ ["0x10", null],
+ ["1e3", null],
+ ["9007199254740991", 9007199254740991],
+ ["9007199254740992", null],
+ [undefined, null],
+ ])("declares Content-Length %j only for decimal digits", (raw, expected) => {
+ const captured = safeResponseMetadata(
+ "request-length",
+ `${origin}/api`,
+ { headers: raw === undefined ? {} : { "Content-Length": raw } },
+ new Set([origin]),
+ );
+ expect(captured.response.content_length).toBe(expected);
+ });
});
diff --git a/src/browser/CdpSafeMetadata.ts b/src/browser/CdpSafeMetadata.ts
index 589c00367..1347cdcca 100644
--- a/src/browser/CdpSafeMetadata.ts
+++ b/src/browser/CdpSafeMetadata.ts
@@ -1,10 +1,6 @@
import type { WebPageInspection } from "../domain/browserObservationSchemas.js";
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
-import {
- numberValue,
- cdpStringValue,
- type UnknownRecord,
-} from "./CdpCaptureValues.js";
+import { cdpStringValue, type UnknownRecord } from "./CdpCaptureValues.js";
type ResponseMetadata = WebPageInspection["metadata"]["responses"][number];
type LinkMetadata = ResponseMetadata["links"][number];
@@ -43,7 +39,7 @@ export const safeResponseMetadata = (
request_id: requestId,
url,
mime_type: boundedHeader(cdpStringValue(response.mimeType)),
- content_length: nonnegativeInteger(headers.get("content-length")),
+ content_length: contentLength(headers.get("content-length")),
content_encoding: boundedHeader(headers.get("content-encoding")),
csp,
links,
@@ -343,9 +339,12 @@ const policyToken = (value: string | undefined): string | null => {
return /^[a-z][a-z0-9_.\x2d]*$/u.test(token) ? token : null;
};
-const nonnegativeInteger = (value: string | undefined): number | null => {
- const number = numberValue(value === undefined ? undefined : Number(value));
- return number === undefined ? null : Math.max(0, Math.trunc(number));
+// RFC 9110 section 8.6: Content-Length = 1*DIGIT. Other spellings declare no length.
+const contentLength = (value: string | undefined): number | null => {
+ const token = (value ?? "").trim();
+ if (!/^[0-9]+$/u.test(token)) return null;
+ const length = Number(token);
+ return Number.isSafeInteger(length) ? length : null;
};
const boundedHeader = (value: string | undefined): string | null =>
diff --git a/src/browser/CdpTargetSession.ts b/src/browser/CdpTargetSession.ts
index 3e1f6eb43..7355e142e 100644
--- a/src/browser/CdpTargetSession.ts
+++ b/src/browser/CdpTargetSession.ts
@@ -1,8 +1,12 @@
+import type { BrowserObservationEndpoint } from "../domain/browserObservation.js";
import { BrowserObservationError } from "../domain/browserObservationError.js";
import type { BrowserObservationOperation } from "../domain/browserObservationErrors.js";
import { CdpConnection } from "./CdpConnection.js";
import {
cdpTargetWebSocket,
+ discoverCdpBrowserSocket,
+ discoverCdpEndpoint,
+ withCdpDiscoveryDeadline,
type CdpEndpointDiscovery,
type CdpEndpointTarget,
} from "./CdpEndpoint.js";
@@ -13,6 +17,59 @@ export interface CdpTargetSession {
readonly sessionId: string | undefined;
}
+/** Discovery for one caller-selected endpoint and the browser socket that read it. */
+export interface CdpOpenedEndpoint {
+ readonly discovery: CdpEndpointDiscovery;
+ /**
+ * Browser socket kept for the target session, because a browser may ask
+ * the user to approve each socket. HTTP discovery opens no socket.
+ */
+ readonly connection: CdpConnection | undefined;
+}
+
+/**
+ * Discover one passive browser endpoint through its selected transport. A
+ * browser socket's handshake and discovery share one discovery deadline.
+ */
+export const openCdpEndpoint = async (
+ endpoint: BrowserObservationEndpoint,
+ operation: BrowserObservationOperation,
+ signal?: AbortSignal,
+ timeoutMs?: number,
+): Promise => {
+ if (endpoint.transport === "http_discovery")
+ return {
+ discovery: await discoverCdpEndpoint(endpoint.url, operation, signal),
+ connection: undefined,
+ };
+ return await withCdpDiscoveryDeadline(
+ operation,
+ signal,
+ async (deadlineSignal) => {
+ const connection = await CdpConnection.connect(
+ endpoint.url,
+ operation,
+ deadlineSignal,
+ );
+ try {
+ return {
+ discovery: await discoverCdpBrowserSocket(
+ connection,
+ endpoint.url,
+ operation,
+ deadlineSignal,
+ ),
+ connection,
+ };
+ } catch (cause: unknown) {
+ await connection.close();
+ throw cause;
+ }
+ },
+ timeoutMs,
+ );
+};
+
/** Open one authorized target through its supported browser or page transport. */
export const openCdpTargetSession = async (
discovery: CdpEndpointDiscovery,
@@ -29,6 +86,16 @@ export const openCdpTargetSession = async (
limits,
);
if (webSocket.scope === "page") return { connection, sessionId: undefined };
+ return await attachCdpTargetSession(connection, target, operation, signal);
+};
+
+/** Attach one target through an open browser socket, which the session then owns. */
+export const attachCdpTargetSession = async (
+ connection: CdpConnection,
+ target: CdpEndpointTarget,
+ operation: BrowserObservationOperation,
+ signal?: AbortSignal,
+): Promise => {
try {
const attached = await connection.send(
"Target.attachToTarget",
diff --git a/src/browser/PlaywrightScenarioBrowser.test.ts b/src/browser/PlaywrightScenarioBrowser.test.ts
index 79e903975..ffd2dba95 100644
--- a/src/browser/PlaywrightScenarioBrowser.test.ts
+++ b/src/browser/PlaywrightScenarioBrowser.test.ts
@@ -164,7 +164,15 @@ it("keeps attached-target ownership after the cleanup response timeout", async (
cleanup: { reason: expect.stringContaining("timed out") },
});
await closeStarted.promise;
- await vi.advanceTimersByTimeAsync(1_000);
+ let responseSettled = false;
+ void timedOutClose
+ .finally(() => {
+ responseSettled = true;
+ })
+ .catch(() => {});
+ await vi.advanceTimersByTimeAsync(29_999);
+ expect(responseSettled).toBe(false);
+ await vi.advanceTimersByTimeAsync(1);
await timeoutExpectation;
expect(settled).toEqual([]);
diff --git a/src/browser/PlaywrightScenarioBrowser.ts b/src/browser/PlaywrightScenarioBrowser.ts
index 615ad055e..3af9ade9c 100644
--- a/src/browser/PlaywrightScenarioBrowser.ts
+++ b/src/browser/PlaywrightScenarioBrowser.ts
@@ -11,6 +11,9 @@ import { BrowserObservationError } from "../domain/browserObservationError.js";
import { withPlaywrightExecutionBoundary } from "./PlaywrightExecutionBoundary.js";
const OPERATION = "capture_browser_scenario" as const;
+// Closing a real Chrome persistent context can take several seconds even when
+// it succeeds. The shorter operation deadline must not misreport cleanup.
+const CLEANUP_CONFIRMATION_TIMEOUT_MS = 30_000;
const throwIfScenarioCancelled = (signal: AbortSignal | undefined): void => {
if (signal?.aborted === true)
@@ -71,7 +74,11 @@ export class PlaywrightScenarioBrowserCleanupOwner {
signal?: AbortSignal,
): Promise {
try {
- await withPlaywrightExecutionBoundary(() => cleanup, 1_000, signal);
+ await withPlaywrightExecutionBoundary(
+ () => cleanup,
+ CLEANUP_CONFIRMATION_TIMEOUT_MS,
+ signal,
+ );
} catch (cause: unknown) {
throw new BrowserObservationError(OPERATION, "cleanup_failed", {
cause,
diff --git a/src/browser/PlaywrightScenarioEvents.sharedContext.test.ts b/src/browser/PlaywrightScenarioEvents.sharedContext.test.ts
new file mode 100644
index 000000000..8e20a83ae
--- /dev/null
+++ b/src/browser/PlaywrightScenarioEvents.sharedContext.test.ts
@@ -0,0 +1,130 @@
+import { EventEmitter } from "node:events";
+import type { Page } from "playwright-core";
+import { expect, it } from "vitest";
+
+import { browserScenarioSchema } from "../domain/browserScenario.js";
+import { BrowserScenarioSecrets } from "./BrowserScenarioSecrets.js";
+import { PlaywrightScenarioEvents } from "./PlaywrightScenarioEvents.js";
+
+it("omits a request without a frame in a shared context", () => {
+ const context = new EventEmitter();
+ const page = new EventEmitter() as unknown as Page;
+ const scenario = browserScenarioSchema.parse({
+ browser: { mode: "launch", executable_path: "/opt/chromium" },
+ start_url: { url: "https://app.example.test/" },
+ actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }],
+ capture: { events: ["network"] },
+ });
+ const secrets = BrowserScenarioSecrets.resolve(scenario, {});
+ if (secrets === undefined) throw new Error("Expected resolved secrets");
+ const events = new PlaywrightScenarioEvents({
+ page,
+ context: context as never,
+ ownsContext: false,
+ enabled: new Set(["network"]),
+ secrets,
+ });
+ const unknownRequest = {
+ frame: () => {
+ throw new Error("Frame for this navigation request is unavailable");
+ },
+ url: () => "https://unrelated.example.test/private",
+ };
+ context.emit("request", unknownRequest);
+ expect(events.result().items).toEqual([]);
+ expect(events.limitations()).toContain(
+ "Shared-context network capture excludes requests without a known root or descendant Page frame.",
+ );
+});
+
+it("detaches root and popup listeners when a scenario finishes", async () => {
+ const context = new EventEmitter();
+ const page = new EventEmitter() as unknown as Page;
+ const popup = Object.assign(new EventEmitter(), {
+ url: () => "https://app.example.test/popup",
+ });
+ const scenario = browserScenarioSchema.parse({
+ browser: { mode: "launch", executable_path: "/opt/chromium" },
+ start_url: { url: "https://app.example.test/" },
+ actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }],
+ capture: { events: ["console", "popups"] },
+ });
+ const secrets = BrowserScenarioSecrets.resolve(scenario, {});
+ if (secrets === undefined) throw new Error("Expected resolved secrets");
+ const events = new PlaywrightScenarioEvents({
+ page,
+ context: context as never,
+ ownsContext: false,
+ enabled: new Set(["console", "popups"]),
+ secrets,
+ });
+ (page as unknown as EventEmitter).emit("popup", popup);
+ expect((page as unknown as EventEmitter).listenerCount("console")).toBe(1);
+ expect(popup.listenerCount("console")).toBe(1);
+ // Popup closure is recorded, then its page-owned listeners are released.
+ expect(popup.listenerCount("close")).toBe(2);
+ await events.finish();
+ expect((page as unknown as EventEmitter).listenerCount("console")).toBe(0);
+ expect((page as unknown as EventEmitter).listenerCount("popup")).toBe(0);
+ expect(popup.listenerCount("console")).toBe(0);
+ expect(popup.listenerCount("popup")).toBe(0);
+ expect(popup.listenerCount("close")).toBe(0);
+});
+
+it("detaches worker and websocket listeners when a scenario finishes", async () => {
+ const context = new EventEmitter();
+ const page = new EventEmitter() as unknown as Page;
+ const socket = Object.assign(new EventEmitter(), {
+ url: () => "wss://app.example.test/socket",
+ });
+ const worker = Object.assign(new EventEmitter(), {
+ url: () => "https://app.example.test/worker.js",
+ });
+ const scenario = browserScenarioSchema.parse({
+ browser: { mode: "launch", executable_path: "/opt/chromium" },
+ start_url: { url: "https://app.example.test/" },
+ actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }],
+ capture: { events: ["websockets", "workers"] },
+ });
+ const secrets = BrowserScenarioSecrets.resolve(scenario, {});
+ if (secrets === undefined) throw new Error("Expected resolved secrets");
+ const events = new PlaywrightScenarioEvents({
+ page,
+ context: context as never,
+ ownsContext: false,
+ enabled: new Set(["websockets", "workers"]),
+ secrets,
+ });
+ (page as unknown as EventEmitter).emit("websocket", socket);
+ (page as unknown as EventEmitter).emit("worker", worker);
+ expect(socket.listenerCount("framesent")).toBe(1);
+ expect(worker.listenerCount("close")).toBe(1);
+ await events.finish();
+ expect(socket.listenerCount("framesent")).toBe(0);
+ expect(socket.listenerCount("framereceived")).toBe(0);
+ expect(socket.listenerCount("close")).toBe(0);
+ expect(worker.listenerCount("close")).toBe(0);
+});
+
+it("detaches owned-context popup discovery when a scenario finishes", async () => {
+ const context = new EventEmitter();
+ const page = new EventEmitter() as unknown as Page;
+ const scenario = browserScenarioSchema.parse({
+ browser: { mode: "launch", executable_path: "/opt/chromium" },
+ start_url: { url: "https://app.example.test/" },
+ actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }],
+ capture: { events: ["console"] },
+ });
+ const secrets = BrowserScenarioSecrets.resolve(scenario, {});
+ if (secrets === undefined) throw new Error("Expected resolved secrets");
+ const events = new PlaywrightScenarioEvents({
+ page,
+ context: context as never,
+ ownsContext: true,
+ enabled: new Set(["console"]),
+ secrets,
+ });
+ expect(context.listenerCount("page")).toBe(1);
+ await events.finish();
+ expect(context.listenerCount("page")).toBe(0);
+});
diff --git a/src/browser/PlaywrightScenarioEvents.ts b/src/browser/PlaywrightScenarioEvents.ts
index 18e1f0e3b..aee9db630 100644
--- a/src/browser/PlaywrightScenarioEvents.ts
+++ b/src/browser/PlaywrightScenarioEvents.ts
@@ -54,10 +54,11 @@ export class PlaywrightScenarioEvents {
private readonly enabled: ReadonlySet;
private readonly secrets: BrowserScenarioSecrets;
private readonly ownsContext: boolean;
- private readonly pages = new Set();
+ private pages = new WeakSet();
private readonly incompleteFamilies = new Set();
private readonly network: PlaywrightScenarioNetwork | undefined;
private stopNetwork: (() => void) | undefined;
+ private readonly stopListeners = new Map void>>();
private sealed = false;
constructor(options: EventCaptureOptions) {
@@ -72,10 +73,12 @@ export class PlaywrightScenarioEvents {
this.secrets = options.secrets;
this.ownsContext = options.ownsContext === true;
this.pages.add(options.page);
- if (this.ownsContext && this.enabled.size > 0)
- (options.context ?? options.page.context()).on("page", (page) =>
- this.popup(page),
- );
+ if (this.ownsContext && this.enabled.size > 0) {
+ const context = options.context ?? options.page.context();
+ const onPage = (page: Page) => this.popup(page);
+ context.on("page", onPage);
+ this.retainListener(context, () => context.off("page", onPage));
+ }
if (this.enabled.has("network")) {
const context = options.context ?? options.page.context();
const collector = new PlaywrightScenarioNetwork({
@@ -138,6 +141,8 @@ export class PlaywrightScenarioEvents {
async finish(): Promise {
this.stopNetwork?.();
this.stopNetwork = undefined;
+ for (const owner of this.stopListeners.keys()) this.releaseListeners(owner);
+ this.pages = new WeakSet();
try {
await this.network?.finish();
} finally {
@@ -184,34 +189,81 @@ export class PlaywrightScenarioEvents {
return this.sequence;
}
+ private retainListener(owner: object, stop: () => void): void {
+ let stops = this.stopListeners.get(owner);
+ if (stops === undefined) {
+ stops = new Set();
+ this.stopListeners.set(owner, stops);
+ }
+ stops.add(stop);
+ }
+
+ private releaseListeners(owner: object): void {
+ const stops = this.stopListeners.get(owner);
+ this.stopListeners.delete(owner);
+ for (const stop of stops ?? []) stop();
+ }
+
private observePage(page: Page): void {
- if (this.enabled.has("console"))
- page.on("console", (message) => this.console(message));
- if (this.enabled.has("page-errors"))
- page.on("pageerror", (error) => {
+ if (this.enabled.size > 0) {
+ const onClose = () => {
+ this.releaseListeners(page);
+ };
+ page.on("close", onClose);
+ this.retainListener(page, () => page.off("close", onClose));
+ }
+
+ if (this.enabled.has("console")) {
+ const onConsole = (message: ConsoleMessage) => this.console(message);
+ page.on("console", onConsole);
+ this.retainListener(page, () => page.off("console", onConsole));
+ }
+ if (this.enabled.has("page-errors")) {
+ const onError = (error: Error) => {
this.push({
kind: "page-error",
message: this.secrets.redact(error.message),
stack:
error.stack === undefined ? null : this.secrets.redact(error.stack),
});
- });
- if (this.enabled.has("websockets"))
- page.on("websocket", (socket) => this.webSocket(socket));
- if (this.enabled.has("frames")) {
- page.on("frameattached", (frame) => this.frame("frame-attached", frame));
- page.on("framedetached", (frame) => this.frame("frame-detached", frame));
- page.on("framenavigated", (frame) =>
- this.frame("frame-navigated", frame),
- );
+ };
+ page.on("pageerror", onError);
+ this.retainListener(page, () => page.off("pageerror", onError));
+ }
+ if (this.enabled.has("websockets")) {
+ const onWebSocket = (socket: WebSocket) => this.webSocket(socket);
+ page.on("websocket", onWebSocket);
+ this.retainListener(page, () => page.off("websocket", onWebSocket));
+ }
+ if (this.enabled.has("frames")) {
+ const onAttached = (frame: Frame) => this.frame("frame-attached", frame);
+ const onDetached = (frame: Frame) => this.frame("frame-detached", frame);
+ const onNavigated = (frame: Frame) =>
+ this.frame("frame-navigated", frame);
+ page.on("frameattached", onAttached);
+ page.on("framedetached", onDetached);
+ page.on("framenavigated", onNavigated);
+ this.retainListener(page, () => page.off("frameattached", onAttached));
+ this.retainListener(page, () => page.off("framedetached", onDetached));
+ this.retainListener(page, () => page.off("framenavigated", onNavigated));
+ }
+ if (this.enabled.has("workers")) {
+ const onWorker = (worker: Worker) => this.worker(worker);
+ page.on("worker", onWorker);
+ this.retainListener(page, () => page.off("worker", onWorker));
}
- if (this.enabled.has("workers"))
- page.on("worker", (worker) => this.worker(worker));
// Any selected family needs descendant discovery in shared contexts.
// Popup lifecycle records remain gated on their own selector below.
- if (this.enabled.size > 0) page.on("popup", (popup) => this.popup(popup));
- if (this.enabled.has("downloads"))
- page.on("download", (download) => this.download(download));
+ if (this.enabled.size > 0) {
+ const onPopup = (popup: Page) => this.popup(popup);
+ page.on("popup", onPopup);
+ this.retainListener(page, () => page.off("popup", onPopup));
+ }
+ if (this.enabled.has("downloads")) {
+ const onDownload = (download: Download) => this.download(download);
+ page.on("download", onDownload);
+ this.retainListener(page, () => page.off("download", onDownload));
+ }
}
private safeUrl(value: string) {
@@ -231,13 +283,20 @@ export class PlaywrightScenarioEvents {
private webSocket(socket: WebSocket): void {
const url = this.safeUrl(socket.url());
this.push({ kind: "websocket-opened", url });
- socket.on("framesent", ({ payload }) =>
- this.webSocketFrame("websocket-frame-sent", url, payload),
- );
- socket.on("framereceived", ({ payload }) =>
- this.webSocketFrame("websocket-frame-received", url, payload),
- );
- socket.on("close", () => this.push({ kind: "websocket-closed", url }));
+ const onSent = ({ payload }: { payload: string | Buffer }) =>
+ this.webSocketFrame("websocket-frame-sent", url, payload);
+ const onReceived = ({ payload }: { payload: string | Buffer }) =>
+ this.webSocketFrame("websocket-frame-received", url, payload);
+ const onClose = () => {
+ this.push({ kind: "websocket-closed", url });
+ this.releaseListeners(socket);
+ };
+ socket.on("framesent", onSent);
+ socket.on("framereceived", onReceived);
+ socket.on("close", onClose);
+ this.retainListener(socket, () => socket.off("framesent", onSent));
+ this.retainListener(socket, () => socket.off("framereceived", onReceived));
+ this.retainListener(socket, () => socket.off("close", onClose));
}
private webSocketFrame(
@@ -286,7 +345,12 @@ export class PlaywrightScenarioEvents {
name: null,
};
this.push({ kind: "worker-created", ...details });
- worker.on("close", () => this.push({ kind: "worker-closed", ...details }));
+ const onClose = () => {
+ this.push({ kind: "worker-closed", ...details });
+ this.releaseListeners(worker);
+ };
+ worker.on("close", onClose);
+ this.retainListener(worker, () => worker.off("close", onClose));
}
private popup(page: Page): void {
@@ -305,13 +369,14 @@ export class PlaywrightScenarioEvents {
};
if (this.enabled.has("popups")) {
this.push({ kind: "popup-opened", ...opened });
- page.on("close", () =>
+ const onClose = () =>
this.push({
kind: "popup-closed",
url: page.url() === "" ? null : this.safeUrl(page.url()),
name: null,
- }),
- );
+ });
+ page.on("close", onClose);
+ this.retainListener(page, () => page.off("close", onClose));
}
this.observePage(page);
}
diff --git a/src/browser/WebSourceMapFetcher.ts b/src/browser/WebSourceMapFetcher.ts
index c4bae782a..5c8f86ea0 100644
--- a/src/browser/WebSourceMapFetcher.ts
+++ b/src/browser/WebSourceMapFetcher.ts
@@ -743,8 +743,8 @@ const originalModuleEdges = (
check();
const dependency = originalDependency(node);
if (dependency === null) return;
- const { kind, specifier } = dependency;
- if (kind === "require" && t.isCallExpression(node)) {
+ const { kind, specifier, requireCallee } = dependency;
+ if (requireCallee !== undefined) {
unboundRequires ??= new Set(
analyzeParsedJavaScriptReferences(parsed, "require")
.filter(
@@ -756,7 +756,7 @@ const originalModuleEdges = (
`${String(location.start.line)}:${String(location.start.column)}`,
),
);
- const location = node.callee.loc?.start;
+ const location = requireCallee.loc?.start;
if (
location === undefined ||
!unboundRequires.has(
@@ -1039,6 +1039,7 @@ const originalDependency = (
): {
readonly kind: ParsedSourceMapItem["original_module_edges"][number]["kind"];
readonly specifier: string;
+ readonly requireCallee?: t.Identifier;
} | null => {
if (
t.isImportDeclaration(node) ||
@@ -1051,11 +1052,15 @@ const originalDependency = (
if (t.isImportExpression(node) && t.isStringLiteral(node.source))
return { kind: "dynamic_import", specifier: node.source.value };
if (
- t.isCallExpression(node) &&
+ (t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
t.isIdentifier(node.callee, { name: "require" }) &&
t.isStringLiteral(node.arguments[0])
)
- return { kind: "require", specifier: node.arguments[0].value };
+ return {
+ kind: "require",
+ specifier: node.arguments[0].value,
+ requireCallee: node.callee,
+ };
// `import x = require("m")` is only legal at module top level and always
// refers to the host loader, so it needs no unbound-`require` check. The
// `moduleReference` is an entity name for a local alias instead, which
diff --git a/src/captures/TsharkCaptureProvider.ts b/src/captures/TsharkCaptureProvider.ts
index d22515929..c4c89b88e 100644
--- a/src/captures/TsharkCaptureProvider.ts
+++ b/src/captures/TsharkCaptureProvider.ts
@@ -8,6 +8,8 @@ import {
FIELD_RECORD_BUDGET,
} from "./parseFieldDiscovery.js";
import {
+ BOUNDED_PRODUCER_LIMITS as LIMITS,
+ prlimitArgumentVector,
readLinuxResourceLimits,
type LinuxResourceLimits,
} from "../process/LinuxResourceLimits.js";
@@ -60,15 +62,6 @@ export const TSHARK_PROVIDER_IDENTITY = {
name: "REA TShark capture adapter",
version: "tshark@4",
} as const;
-// Bound the resident input copy and retained text before allocation/collection.
-const LIMITS = {
- input_bytes: 32 * 1024 * 1024,
- output_bytes: 4 * 1024 * 1024,
- command_timeout_ms: 30_000,
- address_space_bytes: 1024 * 1024 * 1024,
- file_bytes: 64 * 1024 * 1024,
- cpu_seconds: 30,
-} as const;
type Launcher = NonNullable[2]>["launcher"];
/** Inspect selected capture files only, retaining cleanup ownership across failures. */
@@ -165,17 +158,10 @@ export class TsharkCaptureProvider implements PacketCapturePort {
options?.signal,
);
effectiveLimits = await this.#resources.readLimits(limits);
- const limitArguments = [
- `--as=${effectiveLimits.address_space_bytes}:${effectiveLimits.address_space_bytes}`,
- `--fsize=${effectiveLimits.file_bytes}:${effectiveLimits.file_bytes}`,
- `--cpu=${effectiveLimits.cpu_seconds}:${effectiveLimits.cpu_seconds}`,
- "--core=0:0",
- "--",
- command,
- ];
+ const limitArguments = prlimitArgumentVector(effectiveLimits, command);
phase = "workspace";
root = await this.createRuntime();
- this.#resources.retain(root, undefined, operation);
+ this.#resources.retain(root, undefined);
const snapshotPath = join(root.path, "capture");
const configurationPath = join(root.path, "configuration");
await mkdir(configurationPath, { mode: 0o700 });
@@ -406,7 +392,7 @@ export class TsharkCaptureProvider implements PacketCapturePort {
cause instanceof OwnedCommandFailure &&
cause.cleanupOwner !== undefined
)
- this.#resources.retain(root, cause.cleanupOwner, operation);
+ this.#resources.retain(root, cause.cleanupOwner);
result = err(
this.#failure(
cause,
diff --git a/src/cli/androidCommands.ts b/src/cli/androidCommands.ts
index 80a71b235..540dd295d 100644
--- a/src/cli/androidCommands.ts
+++ b/src/cli/androidCommands.ts
@@ -99,13 +99,20 @@ export const registerAndroidCommands = (
}),
});
cli.command(CLI_COMMANDS.traceAndroidReferences, {
- description: "Trace incoming APK class or unique-method references",
+ description:
+ "Trace incoming APK using nodes for a class or selected method",
args: z.object({ path, className }),
options: z.object({
methodName: z
.string()
.optional()
- .describe("Unique declared method name; omit for class references"),
+ .describe("Declared method name; omit for class references"),
+ overloadIndex: z
+ .number()
+ .int()
+ .nonnegative()
+ .optional()
+ .describe("Index among same-name methods from inspect-android-class"),
}),
run: ({ args, options }) =>
execute(CLI_COMMANDS.traceAndroidReferences, "trace_android_references", {
@@ -114,6 +121,9 @@ export const registerAndroidCommands = (
...(options.methodName === undefined
? {}
: { method_name: options.methodName }),
+ ...(options.overloadIndex === undefined
+ ? {}
+ : { overload_index: options.overloadIndex }),
}),
});
};
diff --git a/src/cli/apktoolCommands.ts b/src/cli/apktoolCommands.ts
index c7795f9d4..217889f5a 100644
--- a/src/cli/apktoolCommands.ts
+++ b/src/cli/apktoolCommands.ts
@@ -6,7 +6,10 @@ import { createApktoolResourceAnalysisProvider } from "../composition/apktool.js
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
-import type { ApktoolOperation } from "../domain/apktool/apktoolResourceAnalysis.js";
+import {
+ apktoolInputSchemas,
+ type ApktoolOperation,
+} from "../domain/apktool/apktoolResourceAnalysis.js";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { withCommandCancellation } from "./commandCancellation.js";
@@ -49,22 +52,19 @@ export const registerApktoolCommands = (
args: z.object({
path: z.string().min(1).describe("Local APK file to decode"),
}),
+ // The locale grammar and the string-projection default belong to the
+ // apktool input owner. Read them from that schema rather than restating a
+ // narrower locale pattern, which refused the owner's own advertised
+ // `b+zh+Hans+CN` form at the CLI boundary.
options: z.object({
- locale: z
- .string()
- .min(2)
- .max(16)
- .regex(
- /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/u,
- "Locale must look like de or pt-BR",
- )
- .optional()
- .describe(
- "Project strings from res/values- instead of the default",
- ),
+ locale: apktoolInputSchemas.decode_android_resources.shape.locale,
strings: z
.boolean()
- .default(true)
+ .default(
+ apktoolInputSchemas.decode_android_resources.shape.include_strings.parse(
+ undefined,
+ ),
+ )
.describe(
"Include string resources; use --no-strings to skip projecting them",
),
diff --git a/src/cli/artifactCommands.ts b/src/cli/artifactCommands.ts
index 6b6b452e4..af2eab068 100644
--- a/src/cli/artifactCommands.ts
+++ b/src/cli/artifactCommands.ts
@@ -225,7 +225,7 @@ const registerInspectionCommand = (
description:
"Inspect one artifact and return all available observations and next probes",
args: z.object({
- path: z.string().describe("Application or package path"),
+ path: z.string().min(1).describe("Application or package path"),
}),
options: z.object({
integrityPolicy: z
diff --git a/src/cli/browserAdvancedCommands.ts b/src/cli/browserAdvancedCommands.ts
index 50c9ce446..dc0c268d1 100644
--- a/src/cli/browserAdvancedCommands.ts
+++ b/src/cli/browserAdvancedCommands.ts
@@ -46,7 +46,9 @@ const registerWebMcp = (
cli.command(CLI_COMMANDS.discoverWebMcpTools, {
description: "Passively discover page-declared WebMCP tools",
args: z.object({
- endpoint: z.string().describe("Configured loopback CDP HTTP endpoint"),
+ endpoint: z
+ .string()
+ .describe("Loopback CDP HTTP endpoint or browser WebSocket URL"),
targetId: z.string().describe("Target ID from list-browser-targets"),
}),
options: z.object({
@@ -150,7 +152,9 @@ const registerScreenshot = (
description:
"Capture one visible page viewport within the supplied origin scope",
args: z.object({
- endpoint: z.string().describe("Configured loopback CDP HTTP endpoint"),
+ endpoint: z
+ .string()
+ .describe("Loopback CDP HTTP endpoint or browser WebSocket URL"),
targetId: z.string().describe("Target ID from list-browser-targets"),
}),
options: z.object({
diff --git a/src/cli/browserCommands.ts b/src/cli/browserCommands.ts
index aae6714d2..e9043ec80 100644
--- a/src/cli/browserCommands.ts
+++ b/src/cli/browserCommands.ts
@@ -42,7 +42,9 @@ const registerTargetList = (
description:
"List page targets from a selected loopback CDP browser, optionally filtered by origin",
args: z.object({
- endpoint: z.string().describe("Configured loopback CDP HTTP endpoint"),
+ endpoint: z
+ .string()
+ .describe("Loopback CDP HTTP endpoint or browser WebSocket URL"),
}),
options: z.object({ ...browserScopeOptions }),
run: ({ args, options }) =>
@@ -75,7 +77,9 @@ const registerPageInspection = (
description:
"Passively inspect one page through CDP, defaulting to its current origin",
args: z.object({
- endpoint: z.string().describe("Configured loopback CDP HTTP endpoint"),
+ endpoint: z
+ .string()
+ .describe("Loopback CDP HTTP endpoint or browser WebSocket URL"),
targetId: z.string().describe("Target ID from list-browser-targets"),
}),
options: browserPageInspectionOptions,
@@ -118,7 +122,9 @@ const registerBundleAnalysis = (
description:
"Capture and statically analyze a page bundle, defaulting to the selected target origin",
args: z.object({
- endpoint: z.string().describe("Configured loopback CDP HTTP endpoint"),
+ endpoint: z
+ .string()
+ .describe("Loopback CDP HTTP endpoint or browser WebSocket URL"),
targetId: z.string().describe("Target ID from list-browser-targets"),
}),
options: z.object({
@@ -162,7 +168,9 @@ const registerObservationSession = (
cli.command(CLI_COMMANDS.observeWebSession, {
description: "Observe user-driven page navigation during a chosen window",
args: z.object({
- endpoint: z.string().describe("Configured loopback CDP HTTP endpoint"),
+ endpoint: z
+ .string()
+ .describe("Loopback CDP HTTP endpoint or browser WebSocket URL"),
targetId: z.string().describe("Target ID from list-browser-targets"),
}),
options: z.object({
diff --git a/src/cli/coreBinaryCommands.ts b/src/cli/coreBinaryCommands.ts
index 710b39181..8902cf4e9 100644
--- a/src/cli/coreBinaryCommands.ts
+++ b/src/cli/coreBinaryCommands.ts
@@ -48,7 +48,10 @@ const registerOverviewCommands = (
cli.command(CLI_COMMANDS.analyze, {
description: "Get an overview of an app",
args: z.object({
- path: z.string().describe("App, program, or analysis database path"),
+ path: z
+ .string()
+ .min(1)
+ .describe("App, program, or analysis database path"),
}),
options: overviewOptions.extend({
integrityPolicy: javascriptApplicationOptions.shape.integrityPolicy,
diff --git a/src/cli/coreNativeCommands.ts b/src/cli/coreNativeCommands.ts
index 016eedbc0..80d07187f 100644
--- a/src/cli/coreNativeCommands.ts
+++ b/src/cli/coreNativeCommands.ts
@@ -17,6 +17,46 @@ export const registerCoreNativeCommands = (
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
): void => {
registerAnnotationCommand(cli, logger, runDirectAnalysis);
+ cli.command(CLI_COMMANDS.searchBytes, {
+ description: "Find masked byte patterns in initialized provider memory",
+ args: z.object({
+ path: z.string().describe("Local executable path"),
+ pattern: z.string().describe("Hexadecimal byte pairs with ?? wildcards"),
+ }),
+ options: z.object({
+ mask: z.string().optional().describe("Equal-length hexadecimal bit mask"),
+ start: z.string().optional().describe("Inclusive start address"),
+ end: z.string().optional().describe("Inclusive end address"),
+ cursor: z
+ .string()
+ .optional()
+ .describe("Continuation from the same query's next_cursor"),
+ limit: z
+ .number()
+ .int()
+ .min(1)
+ .max(1000)
+ .default(100)
+ .describe("Maximum matches on this page"),
+ ...nativeOpenOptionFields,
+ }),
+ run: ({ args, options }) =>
+ logCliCommand(logger, CLI_COMMANDS.searchBytes, () =>
+ runDirectAnalysis(
+ args.path,
+ "search_bytes",
+ {
+ pattern: args.pattern,
+ limit: options.limit,
+ ...(options.mask === undefined ? {} : { mask: options.mask }),
+ ...(options.start === undefined ? {} : { start: options.start }),
+ ...(options.end === undefined ? {} : { end: options.end }),
+ ...(options.cursor === undefined ? {} : { cursor: options.cursor }),
+ },
+ directAnalysisOptionsFromCli(logger, options),
+ ),
+ ),
+ });
cli.command(CLI_COMMANDS.inspectNativeLoadImage, {
description:
"Verify loaded native bytes, source mappings, relocations and entry",
diff --git a/src/cli/utilityCommands.ts b/src/cli/utilityCommands.ts
index 33099ee28..a5da57ac5 100644
--- a/src/cli/utilityCommands.ts
+++ b/src/cli/utilityCommands.ts
@@ -2,6 +2,7 @@ import { z } from "incur";
import type { DirectAnalysis } from "../composition/directAnalysis.js";
import type { ReferenceSourcePolicy } from "../domain/referenceSourcePolicy.js";
+import { createHistoricalSourceSummary } from "../domain/referenceSourceSummary.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { importReferenceSource } from "../application/ReferenceSourceImport.js";
@@ -32,89 +33,116 @@ export const registerUtilityCommands = (
registerCapabilityCommands(cli, logger, analysis);
registerNativeCommands(cli, logger, runProviderAnalysis);
registerNativeCallCommands(cli, logger, runProviderAnalysis);
- for (const [command, operation] of [
- [CLI_COMMANDS.observeNativeUi, "observe_native_ui"],
- [CLI_COMMANDS.captureNativeUiScenario, "capture_native_ui_scenario"],
- ] as const) {
- cli.command(command, {
- description:
- "Observe or run a scenario in one exact native application window",
- args: z.object({
- path: z.string().describe("Local native executable or app path"),
- }),
- options: z.object({
- pid: z.number().int().positive().describe("Existing application PID"),
- windowId: z
- .number()
- .int()
- .positive()
- .describe("Exact selected application window ID"),
- steps: z
- .string()
- .optional()
- .describe("JSON array of declarative AX actions or waits"),
- screenshot: z
- .boolean()
- .default(true)
- .describe("Capture only the selected window"),
- accessibility: z
- .boolean()
- .default(true)
- .describe("Read the selected window accessibility tree"),
- maxNodes: z
- .number()
- .int()
- .positive()
- .safe()
- .default(500)
- .describe("Maximum accessibility nodes per capture"),
- }),
- run: ({ args, options }) =>
- logCliCommand(logger, command, async () => {
- const parameters = {
- pid: options.pid,
- window_id: options.windowId,
- screenshot: options.screenshot,
- accessibility: options.accessibility,
- max_nodes: options.maxNodes,
- };
- if (operation === "capture_native_ui_scenario") {
- const decoded =
- options.steps === undefined
- ? { ok: true as const, value: undefined }
- : safeParseJson(options.steps);
- if (!decoded.ok)
- return invalidNativeUiScenarioInput(operation, [
- { path: ["steps"], reason: "invalid_format", expected: "JSON" },
- ]);
- const parsed = nativeUiScenarioInputSchema.safeParse({
- ...parameters,
- steps: decoded.value,
- });
- if (!parsed.success)
- return invalidNativeUiScenarioInput(
- operation,
- projectInputIssues(parsed.error.issues, {
- ...parameters,
- ...(options.steps === undefined
- ? {}
- : { steps: decoded.value }),
- }),
- );
- return runProviderAnalysis(
- args.path,
- operation,
- parsed.data,
- logger,
- );
- }
- return runProviderAnalysis(args.path, operation, parameters, logger);
- }),
- });
- }
+ registerNativeUiCommands(cli, logger, runProviderAnalysis);
registerReferenceSourceCommand(cli, logger, environment);
};
+const nativeUiScopeOptions = {
+ pid: z.number().int().positive().describe("Existing application PID"),
+ windowId: z
+ .number()
+ .int()
+ .positive()
+ .describe("Exact selected application window ID"),
+ screenshot: z
+ .boolean()
+ .default(true)
+ .describe("Capture only the selected window"),
+ accessibility: z
+ .boolean()
+ .default(true)
+ .describe("Read the selected window accessibility tree"),
+ maxNodes: z
+ .number()
+ .int()
+ .positive()
+ .safe()
+ .default(500)
+ .describe("Maximum accessibility nodes per capture"),
+};
+
+const nativeUiScope = (options: {
+ readonly pid: number;
+ readonly windowId: number;
+ readonly screenshot: boolean;
+ readonly accessibility: boolean;
+ readonly maxNodes: number;
+}) => ({
+ pid: options.pid,
+ window_id: options.windowId,
+ screenshot: options.screenshot,
+ accessibility: options.accessibility,
+ max_nodes: options.maxNodes,
+});
+
+/**
+ * Passive observation and active scenarios have different input contracts.
+ * Only the scenario command accepts steps, so passive observation rejects
+ * them as an unknown flag instead of silently dropping them.
+ */
+const registerNativeUiCommands = (
+ cli: CliInstance,
+ logger: Logger,
+ runProviderAnalysis: DirectAnalysis["runProviderAnalysis"],
+): void => {
+ const pathArgument = z.object({
+ path: z.string().describe("Local native executable or app path"),
+ });
+ cli.command(CLI_COMMANDS.observeNativeUi, {
+ description:
+ "Observe one already-running native application window by PID and window ID",
+ args: pathArgument,
+ options: z.object(nativeUiScopeOptions),
+ run: ({ args, options }) =>
+ logCliCommand(logger, CLI_COMMANDS.observeNativeUi, () =>
+ runProviderAnalysis(
+ args.path,
+ "observe_native_ui",
+ nativeUiScope(options),
+ logger,
+ ),
+ ),
+ });
+ const operation = "capture_native_ui_scenario";
+ cli.command(CLI_COMMANDS.captureNativeUiScenario, {
+ description:
+ "Run declarative AX actions or waits in one exact native application window",
+ args: pathArgument,
+ options: z.object({
+ ...nativeUiScopeOptions,
+ steps: z
+ .string()
+ .optional()
+ .describe("JSON array of declarative AX actions or waits"),
+ }),
+ run: ({ args, options }) =>
+ logCliCommand(logger, CLI_COMMANDS.captureNativeUiScenario, async () => {
+ const parameters = nativeUiScope(options);
+ const decoded =
+ options.steps === undefined
+ ? { ok: true as const, value: undefined }
+ : safeParseJson(options.steps);
+ if (!decoded.ok)
+ return invalidNativeUiScenarioInput(operation, [
+ { path: ["steps"], reason: "invalid_format", expected: "JSON" },
+ ]);
+ const parsed = nativeUiScenarioInputSchema.safeParse({
+ ...parameters,
+ steps: decoded.value,
+ });
+ if (!parsed.success)
+ return invalidNativeUiScenarioInput(
+ operation,
+ projectInputIssues(parsed.error.issues, {
+ ...parameters,
+ ...(options.steps === undefined ? {} : { steps: decoded.value }),
+ }),
+ );
+ return runProviderAnalysis(args.path, operation, parsed.data, logger);
+ }),
+ });
+};
+
const invalidNativeUiScenarioInput = (
operation: string,
issues: ConstructorParameters[2],
@@ -214,11 +242,48 @@ const registerReferenceSourceCommand = (
.string()
.describe("Local source directory to import as historical reference"),
}),
- run: ({ args }) =>
+ options: z.object({
+ detail: z
+ .enum(["full", "summary"])
+ .default("full")
+ .describe(
+ "Complete committed graph or compact historical-source summary",
+ ),
+ outlineDepth: z
+ .number()
+ .int()
+ .nonnegative()
+ .safe()
+ .optional()
+ .describe(
+ "Summary outline path depth (default: 2; 0 omits entries); requires --detail summary",
+ ),
+ }),
+ run: ({ args, options }) =>
logCliCommand(
logger,
"import-reference-source",
async () => {
+ if (
+ options.outlineDepth !== undefined &&
+ options.detail !== "summary"
+ )
+ return {
+ error: "Import failed",
+ ...projectAnalysisError(
+ new AnalysisInputError(
+ "import-reference-source",
+ { userMessage: "--outline-depth requires --detail summary" },
+ [
+ {
+ path: ["outlineDepth"],
+ reason: "invalid_value",
+ message: "--outline-depth requires --detail summary",
+ },
+ ],
+ ),
+ ),
+ };
const config = parseConfig(environment);
if (!config.ok)
return {
@@ -228,6 +293,9 @@ const registerReferenceSourceCommand = (
return runReferenceSourceImport(
args.root,
config.value.referenceSourcePolicy,
+ options.detail === "summary"
+ ? (options.outlineDepth ?? 2)
+ : undefined,
);
},
isReferenceSourceImportCliFailure,
@@ -238,6 +306,7 @@ const registerReferenceSourceCommand = (
const runReferenceSourceImport = async (
root: string,
policy: ReferenceSourcePolicy,
+ outlineDepth: number | undefined,
): Promise => {
const resources = new ArtifactResourceScope();
let outcome:
@@ -319,7 +388,9 @@ const runReferenceSourceImport = async (
cause: closeOutcome.cause,
}),
};
- return imported.value;
+ return outlineDepth === undefined
+ ? imported.value
+ : createHistoricalSourceSummary(imported.value, outlineDepth);
};
const mergeReferenceSourceCleanup = (
diff --git a/src/cli/wasmCommands.ts b/src/cli/wasmCommands.ts
index b91bda033..73b8f8d3f 100644
--- a/src/cli/wasmCommands.ts
+++ b/src/cli/wasmCommands.ts
@@ -1,3 +1,6 @@
+import { compareWasmInterfaces } from "../application/wasm/WasmInterfaceComparisonService.js";
+import { parseCliJsonInput } from "../cliJsonInput.js";
+import { CLI_COMMANDS } from "../cliCommandNames.js";
import { z } from "incur";
import { resolve } from "node:path";
import { wasmArtifactModule } from "../composition/wasm.js";
@@ -13,6 +16,29 @@ export const registerWasmCommands = (
logger: Logger,
environment: Readonly,
): void => {
+ cli.command(CLI_COMMANDS.compareWasmInterfaces, {
+ description:
+ "Compare core WASM interfaces from two exact inspection Evidence records",
+ args: z.object({
+ input: z
+ .string()
+ .describe(
+ "Inline JSON or input file containing left/right complete Evidence and required scope",
+ ),
+ }),
+ run: ({ args }) =>
+ withCommandCancellation(async (signal) =>
+ logCliCommand(logger, CLI_COMMANDS.compareWasmInterfaces, async () => {
+ const input = await parseCliJsonInput(
+ args.input,
+ "compare_wasm_interfaces",
+ );
+ if (!input.ok) return input.error;
+ const result = await compareWasmInterfaces(input.value, { signal });
+ return result.ok ? result.value : projectAnalysisError(result.error);
+ }),
+ ),
+ });
const service = wasmArtifactModule.create(environment);
cli.command(wasmArtifactModule.cliCommand, {
description: "Inspect an explicit local WASM artifact with WABT",
diff --git a/src/cliCommandNames.ts b/src/cliCommandNames.ts
index d63a9a2bc..ff208c62e 100644
--- a/src/cliCommandNames.ts
+++ b/src/cliCommandNames.ts
@@ -4,6 +4,7 @@ export const CLI_COMMANDS = Object.freeze({
inspectPacketCapture: "inspect-packet-capture",
inspectSignalDecoders: "inspect-signal-decoders",
decodeSignalRecording: "decode-signal-recording",
+ compareWasmInterfaces: "compare-wasm-interfaces",
inspectWasmArtifact: "inspect-wasm-artifact",
inspectSqliteDatabase: "inspect-sqlite-database",
inspectEvmInterface: "inspect-evm-interface",
@@ -39,6 +40,7 @@ export const CLI_COMMANDS = Object.freeze({
inspectNativeDataType: "inspect-native-data-type",
inspectNativeLoadImage: "inspect-native-load-image",
readBytes: "read-bytes",
+ searchBytes: "search-bytes",
addressToFileOffset: "address-to-file-offset",
traceNativeValues: "trace-native-values",
inspectNativeInstruction: "inspect-native-instruction",
diff --git a/src/cliJsonInput.ts b/src/cliJsonInput.ts
index 4dd38a90e..cf1b6ea72 100644
--- a/src/cliJsonInput.ts
+++ b/src/cliJsonInput.ts
@@ -65,12 +65,19 @@ const accessDeniedSystemCode = (
: undefined;
};
+/**
+ * A `file:` URL or `scheme://` URL is not an operator-relative filesystem path.
+ * One-letter prefixes stay paths so Windows drive paths resolve normally.
+ */
+const URL_REFERENCE = /^(?:file:|[A-Za-z][A-Za-z0-9+.-]+:\/\/)/iu;
+
/**
* Resolve named string fields of CLI-supplied JSON against the operator
* working directory. Each entry is a key path from the document root; only
- * fields holding nonblank strings are resolved, everything else passes through
- * untouched so shared schemas can reject blank paths. Shared MCP contracts reject
- * relative paths, so CLI workflows resolve operator-relative values before validation.
+ * fields holding nonblank, non-URL strings are resolved, everything else passes
+ * through untouched so shared schemas can reject blank paths and URLs. Shared MCP
+ * contracts reject relative paths, so CLI workflows resolve operator-relative
+ * values before validation.
*/
export const resolveCliJsonPaths = (
value: unknown,
@@ -94,7 +101,9 @@ const resolveJsonPath = (
if (tail.length === 0)
return [
key,
- typeof entry === "string" && entry.trim().length > 0
+ typeof entry === "string" &&
+ entry.trim().length > 0 &&
+ !URL_REFERENCE.test(entry)
? resolve(entry)
: entry,
];
diff --git a/src/cliJsonPaths.test.ts b/src/cliJsonPaths.test.ts
index 936bfacea..ed9e18642 100644
--- a/src/cliJsonPaths.test.ts
+++ b/src/cliJsonPaths.test.ts
@@ -56,4 +56,38 @@ describe("CLI JSON path normalization", () => {
});
expect(input.browser.executable_path).toBe(" browser ");
});
+
+ it.each([
+ "file:///opt/electron",
+ "FILE:electron",
+ "https://example.test/app",
+ ])("keeps URL %j distinguishable from a local path", (value) => {
+ const input = {
+ executable_path: value,
+ application_path: resolve("/synthetic/app"),
+ };
+ const normalized = resolveCliJsonPaths(input, paths);
+ expect(normalized).toEqual(input);
+ const parsed = electronActiveObservationInputSchema.safeParse(normalized);
+ expect(parsed.success).toBe(false);
+ if (parsed.success) throw new Error("Expected URL path rejection");
+ expect(parsed.error.issues).toEqual([
+ expect.objectContaining({
+ path: ["executable_path"],
+ message: expect.stringContaining(
+ "must be absolute local filesystem paths",
+ ),
+ }),
+ ]);
+ });
+
+ it.each([
+ "bin/electron",
+ "release:v1/electron",
+ "C:\\Electron\\electron.exe",
+ ])("keeps resolving local path %j", (value) => {
+ expect(resolveCliJsonPaths({ executable_path: value }, paths)).toEqual({
+ executable_path: resolve(value),
+ });
+ });
});
diff --git a/src/contracts/android/androidToolContracts.ts b/src/contracts/android/androidToolContracts.ts
index b555fcbea..2710e0faa 100644
--- a/src/contracts/android/androidToolContracts.ts
+++ b/src/contracts/android/androidToolContracts.ts
@@ -51,7 +51,7 @@ export const ANDROID_TOOL_CONTRACTS = [
name: "inspect_android_class",
...toolContractMetadata("inspect_android_class"),
description:
- "Inspect one exact Android class's parsed methods, fields and inner classes, including synthetic members. Returns display signatures and explicit unknown DEX descriptors without generating source. Overload indices remain consistent within the artifact and engine session.",
+ "Inspect one exact Android class's parsed methods, fields and inner classes, including synthetic members. Returns display signatures and exact DEX prototypes without generating source. A prototype is not a complete raw method ID; overload indices remain consistent within the artifact and engine session.",
kind: "android-provider",
inputSchema: androidInputSchemas.inspect_android_class,
outputSchema: evidenceResultOf(androidResultSchemas.inspect_android_class),
@@ -66,7 +66,7 @@ export const ANDROID_TOOL_CONTRACTS = [
name: "inspect_android_method",
...toolContractMetadata("inspect_android_method"),
description:
- "Decompile one Android method with smali fallback. Requires explicit overload_index when multiple methods share a name. Reports representation, fallback markers and source truncation; never treats display signatures as exact DEX identities.",
+ "Decompile one Android method with smali fallback. Requires explicit overload_index when multiple methods share a name. Reports its DEX prototype, representation, fallback markers and source truncation; display signatures may be aliases.",
kind: "android-provider",
inputSchema: androidInputSchemas.inspect_android_method,
outputSchema: evidenceResultOf(androidResultSchemas.inspect_android_method),
@@ -85,7 +85,7 @@ export const ANDROID_TOOL_CONTRACTS = [
name: "trace_android_references",
...toolContractMetadata("trace_android_references"),
description:
- "Trace incoming static references to an exact Android class or unique method. Overloaded method reference queries are explicitly unsupported by the current engine. Source lines and DEX offsets remain unknown; no runtime calls are inferred.",
+ "Trace incoming static using nodes to an exact Android class or selected method overload. The total counts containing nodes, not individual invoke instructions. Use inspect_android_class to choose an overload_index when names collide. Source lines and the containing node dex_offset remain unknown. direct_invokes carries exact DEX code-unit instruction offsets when available; direct_invokes_exhaustive false marks byte-bounded partial instruction evidence, and null means coverage is unknown. Using-node coverage remains separate; no runtime calls are inferred.",
kind: "android-provider",
inputSchema: androidInputSchemas.trace_android_references,
outputSchema: evidenceResultOf(
diff --git a/src/contracts/functionComparisonExample.ts b/src/contracts/functionComparisonExample.ts
index 39dfe6f92..aefc75e1a 100644
--- a/src/contracts/functionComparisonExample.ts
+++ b/src/contracts/functionComparisonExample.ts
@@ -16,7 +16,7 @@ const dossier = (text: string) =>
locals: [],
},
pseudocode: text,
- assembly: [],
+ assembly: ["ret"],
comments: [],
callers: [],
callees: [],
@@ -27,6 +27,16 @@ const dossier = (text: string) =>
basic_blocks: [{ start: "0x1000", end: "0x1001", successors: [] }],
native_api: null,
native_value_flow: null,
+ facet_availability: {
+ pseudocode: { available: true },
+ assembly: { available: true },
+ comments: { available: true },
+ callers: { available: true },
+ callees: { available: true },
+ references: { available: true },
+ strings_names: { available: true },
+ cfg: { available: true },
+ },
limitations: ["The fixture omits native API and value-flow observations."],
});
diff --git a/src/contracts/hopperMemoryContracts.ts b/src/contracts/hopperMemoryContracts.ts
index e4c49788a..df7e43169 100644
--- a/src/contracts/hopperMemoryContracts.ts
+++ b/src/contracts/hopperMemoryContracts.ts
@@ -1,8 +1,27 @@
import { z } from "zod";
import { address, document } from "./toolContractHelpers.js";
+import {
+ bytePatternFields,
+ validateBytePattern,
+} from "../domain/native/bytePattern.js";
/** Hopper memory and file-mapping contracts. */
export const HOPPER_MEMORY_TOOL_DEFINITIONS = [
+ {
+ name: "search_bytes",
+ description:
+ "Find masked byte signatures in initialized analyzed memory. Hex byte pairs and ?? wildcards are supported; an optional equal-length hex mask selects bits. Bounds are inclusive. Matches are ordered by provider address and never span blocks. Coverage describes this page; reuse the same query with next_cursor to continue. Uninitialized blocks are listed separately. Ghidra only; Hopper is unavailable.",
+ inputSchema: z
+ .strictObject({
+ ...bytePatternFields,
+ document,
+ start: address.optional(),
+ end: address.optional(),
+ limit: z.number().int().min(1).max(1000).default(100),
+ cursor: z.string().min(1).optional(),
+ })
+ .superRefine(validateBytePattern),
+ },
{
name: "read_bytes",
description:
diff --git a/src/contracts/inputSchemaPresentation.ts b/src/contracts/inputSchemaPresentation.ts
index 9663dd832..22ad0ae30 100644
--- a/src/contracts/inputSchemaPresentation.ts
+++ b/src/contracts/inputSchemaPresentation.ts
@@ -30,6 +30,7 @@ const resolveReference = (
root: Readonly>,
reference: string,
): unknown => {
+ if (reference === "#") return root;
if (!reference.startsWith("#/")) return undefined;
let value: unknown = root;
for (const token of reference.slice(2).split("/")) {
@@ -229,3 +230,76 @@ export const presentInputJsonSchema = (
const described = describeSchema(flattened, flattened, describeProperty);
return isObject(described) ? described : flattened;
};
+
+/**
+ * Clients that render advertised inputs into SDK declarations expand
+ * schema-local references with a shared per-input budget: Pi 1.1.0 code-mode
+ * allows 32 expansions and degrades every later reference to `unknown`
+ * (morluto/rea#1632), hiding the fields those references describe. The budget
+ * counts conservatively — every resolvable, non-cyclic reference occurrence
+ * in every schema position — so any reference-resolving client stays within
+ * its measured limit.
+ */
+export const advertisedReferenceExpansionBudget = 32;
+
+const definitionKeys = new Set(["$defs", "definitions"]);
+
+const countReferenceExpansions = (
+ node: unknown,
+ root: Readonly>,
+ active: ReadonlySet,
+ count: { value: number },
+): void => {
+ if (Array.isArray(node)) {
+ node.forEach((child) =>
+ countReferenceExpansions(child, root, active, count),
+ );
+ return;
+ }
+ if (!isObject(node)) return;
+ if (typeof node.$ref === "string" && !active.has(node.$ref)) {
+ const target = resolveReference(root, node.$ref);
+ if (target !== undefined) {
+ count.value += 1;
+ countReferenceExpansions(
+ target,
+ root,
+ new Set([...active, node.$ref]),
+ count,
+ );
+ return;
+ }
+ }
+ for (const [key, value] of Object.entries(node)) {
+ if (definitionKeys.has(key)) continue;
+ if (schemaMaps.has(key) && isObject(value)) {
+ for (const child of Object.values(value))
+ countReferenceExpansions(child, root, active, count);
+ } else if (schemaArrays.has(key) || schemaChildren.has(key)) {
+ countReferenceExpansions(value, root, active, count);
+ }
+ }
+};
+
+/** Conservative client expansion count over every schema position. */
+export const countLocalReferenceExpansions = (
+ schema: Record,
+): number => {
+ const count = { value: 0 };
+ countReferenceExpansions(schema, schema, new Set(), count);
+ return count.value;
+};
+
+/**
+ * Keep the shared-definition form of an advertised input while clients can
+ * still resolve it. Once rendering would exhaust the reference budget, fall
+ * back to the inline projection, which carries no shared definitions for the
+ * nonrecursive schemas this repository advertises, so every field renders
+ * with complete structure in every reference-resolving client.
+ */
+export const inputSchemaWithinReferenceExpansionBudget = (
+ shared: Record,
+ inline: () => Record,
+ budget: number,
+): Record =>
+ countLocalReferenceExpansions(shared) <= budget ? shared : inline();
diff --git a/src/contracts/managed/managedWorkflowToolContracts.ts b/src/contracts/managed/managedWorkflowToolContracts.ts
index 3842c3814..519c2e125 100644
--- a/src/contracts/managed/managedWorkflowToolContracts.ts
+++ b/src/contracts/managed/managedWorkflowToolContracts.ts
@@ -1,5 +1,6 @@
import { z } from "zod";
import { evidenceSchema } from "../../domain/evidence.js";
+import { managedNativeVerificationInputSchema } from "../../domain/managed/managedNativeVerificationSchemas.js";
import { managedReconstructionImportInputSchema } from "../../domain/managed/managedReconstruction.js";
import type { ToolContract } from "../toolContractTypes.js";
@@ -34,35 +35,6 @@ export const managedReconstructionReferenceInputSchema =
static_members: evidenceSchema,
});
-/** Inline Evidence inputs for managed/native verification. */
-export const managedNativeVerificationReferenceInputSchema = z
- .strictObject({
- managed_boundaries: evidenceSchema,
- native_observations: z
- .array(evidenceSchema)
- .min(1)
- .describe("Native function or export Evidence records"),
- })
- .superRefine((input, context) => {
- const ids = new Set([input.managed_boundaries.evidence_id]);
- for (const [index, evidence] of input.native_observations.entries()) {
- if (evidence.evidence_id === input.managed_boundaries.evidence_id)
- context.addIssue({
- code: "custom",
- path: ["native_observations", index],
- message:
- "Native observation Evidence must be distinct from managed boundary Evidence",
- });
- if (ids.has(evidence.evidence_id))
- context.addIssue({
- code: "custom",
- path: ["native_observations", index],
- message: "Native observation Evidence IDs must be unique",
- });
- ids.add(evidence.evidence_id);
- }
- });
-
const managedApplicationGraphReferenceFacts = {} as const;
const managedApplicationGraphReferenceShape = z.strictObject({
@@ -146,7 +118,7 @@ export const MANAGED_WORKFLOW_TOOL_CONTRACTS = [
description:
"Verify managed P/Invoke/native-boundary declarations against authenticated native export or function Evidence without executing managed code or translating managed metadata tokens into native addresses. The workflow preserves declaration-only, verified, inferred, contradicted, and unresolved states.",
kind: "application",
- inputSchema: managedNativeVerificationReferenceInputSchema,
+ inputSchema: managedNativeVerificationInputSchema,
outputSchema: nativeVerificationOutputSchema,
examples: [
{
diff --git a/src/contracts/native/binaryDiagnosticsToolContracts.ts b/src/contracts/native/binaryDiagnosticsToolContracts.ts
index 64a80e42d..8c872f9d9 100644
--- a/src/contracts/native/binaryDiagnosticsToolContracts.ts
+++ b/src/contracts/native/binaryDiagnosticsToolContracts.ts
@@ -67,7 +67,7 @@ export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [
...toolContractMetadata("inspect_recorded_crash"),
kind: "native-provider",
description:
- "Inspect a supplied Linux ELF64 amd64 little-endian core without launching its target or attaching to any live PID. Returns all recorded segments, raw note owners/descriptors and source ranges, every Linux CORE PRSTATUS thread/register and SIGINFO fields inline with SHA-256 Evidence. Historical PID is recorded metadata; current process/file identity, note coverage and signal-thread associations stay unknown. Optionally add derived core-only GDB/pwndbg mapping candidates through explicit BYO configuration; zero map flags mean unknown permissions. Uses the same stable snapshot for both stages, bounded complete output and owned cleanup. Initial Linux x64 profile: unchanged pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2; optional unchanged pwndbg 2026.09.15. No implicit executable/library pairing, GDB command language, host configuration changes or dependency installation.",
+ "Inspect a supplied Linux ELF64 amd64 or AArch64 little-endian core without launching its target or attaching to any live PID. Returns all recorded segments, raw note owners/descriptors and source ranges, every Linux CORE PRSTATUS thread/register and SIGINFO fields inline with SHA-256 Evidence. Historical PID is recorded metadata; current process/file identity, note coverage and signal-thread associations stay unknown. Optionally add derived core-only GDB/pwndbg mapping candidates through explicit BYO configuration; zero map flags mean unknown permissions. Uses the same stable snapshot for both stages, bounded complete output and owned cleanup. Linux x64 host profile: unchanged pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2; optional unchanged pwndbg 2026.09.15. No implicit executable/library pairing, GDB command language, host configuration changes or dependency installation.",
inputSchema: inspectRecordedCrashInputSchema,
outputSchema: evidenceResultOf(recordedCrashSchema),
examples: [
diff --git a/src/contracts/toolContractExamples.ts b/src/contracts/toolContractExamples.ts
index 66b0f3518..50a745e75 100644
--- a/src/contracts/toolContractExamples.ts
+++ b/src/contracts/toolContractExamples.ts
@@ -26,6 +26,7 @@ export const TOOL_EXAMPLE_OVERRIDES: Readonly<
resolve_native_call_targets: { address: "0x1000" },
read_function_instructions: { procedure: "main" },
read_bytes: { address: "0x1000", length: 16 },
+ search_bytes: { pattern: "48 8B ?? ?? 89" },
address_to_file_offset: { address: "0x1000" },
procedure_references: { procedure: "main" },
procedure_pseudo_code: { procedure: "main" },
diff --git a/src/contracts/toolEffects.test.ts b/src/contracts/toolEffects.test.ts
index fe18c96b3..3a4acca99 100644
--- a/src/contracts/toolEffects.test.ts
+++ b/src/contracts/toolEffects.test.ts
@@ -27,6 +27,8 @@ const EXECUTION_WORDING_EXCEPTIONS: Readonly> = {
inspect_macho: "runs host Mach-O inspection tools, not the target",
inspect_evm_interface:
"decodes bytecode in REA's bundled worker without executing it",
+ compare_wasm_interfaces:
+ "compares retained inspection Evidence without executing modules",
inspect_wasm_artifact:
"WABT validates and decompiles the module without executing it",
identify_flutter_build:
diff --git a/src/contracts/toolEffects.ts b/src/contracts/toolEffects.ts
index 70a40c6a1..377905480 100644
--- a/src/contracts/toolEffects.ts
+++ b/src/contracts/toolEffects.ts
@@ -438,6 +438,7 @@ export const TOOL_EFFECTS: Readonly> = {
inspect_native_data_type: evidence,
resolve_native_call_targets: evidence,
read_bytes: evidence,
+ search_bytes: evidence,
address_to_file_offset: evidence,
procedure_references: evidence,
procedure_pseudo_code: evidence,
@@ -622,6 +623,7 @@ export const TOOL_EFFECTS: Readonly> = {
}),
compare_process_captures: sessionEvidence,
compare_artifacts: sessionEvidence,
+ compare_wasm_interfaces: sessionEvidence,
compare_functions: sessionEvidence,
compare_bundles: effects({ mutatesSession: true }),
find_changed_behavior: sessionEvidence,
diff --git a/src/contracts/toolInputReferences.test.ts b/src/contracts/toolInputReferences.test.ts
index beb63b7e7..f75a06531 100644
--- a/src/contracts/toolInputReferences.test.ts
+++ b/src/contracts/toolInputReferences.test.ts
@@ -3,7 +3,12 @@ import { Ajv2020 } from "ajv/dist/2020.js";
import { describe, expect, it } from "vitest";
import { z } from "zod";
-import { presentInputJsonSchema } from "./inputSchemaPresentation.js";
+import {
+ advertisedReferenceExpansionBudget,
+ countLocalReferenceExpansions,
+ inputSchemaWithinReferenceExpansionBudget,
+ presentInputJsonSchema,
+} from "./inputSchemaPresentation.js";
import { toolContract } from "./toolContracts.js";
import { toolInputSchemaWithMetadata } from "./toolSchemaMetadata.js";
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "./javascript/javascriptRuntimeReconciliationExample.js";
@@ -166,6 +171,105 @@ describe("advertised input references", () => {
);
});
+describe("advertised reference expansion budget", () => {
+ it("counts references in keyword-named fields, excluding literals", () => {
+ const fields = ["default", "enum", "examples", "const", "$defs"];
+ const schema = {
+ type: "object",
+ properties: Object.fromEntries(
+ fields.map((name) => [name, { $ref: "#/$defs/value" }]),
+ ),
+ $defs: { value: { type: "string" } },
+ default: { $ref: "#/$defs/value" },
+ examples: [{ $ref: "#/$defs/value" }],
+ };
+ expect(countLocalReferenceExpansions(schema)).toBe(fields.length);
+ });
+ const reusedComponent = z
+ .object({ kind: z.literal("rec"), value: z.string() })
+ .strict();
+ const inputWithReusedFields = (count: number) =>
+ z.strictObject(
+ Object.fromEntries(
+ Array.from({ length: count }, (_, index) => [
+ `field_${String(index).padStart(2, "0")}`,
+ reusedComponent,
+ ]),
+ ),
+ );
+ const advertisedInput = (
+ canonical: z.ZodType>>,
+ ) =>
+ toolInputSchemaWithMetadata({
+ ...toolContract("open_binary"),
+ inputSchema: canonical,
+ })["~standard"].jsonSchema.input({ target: "draft-2020-12" });
+
+ it("keeps shared definitions while every client can resolve them", () => {
+ const advertised = advertisedInput(inputWithReusedFields(8));
+ expect(advertised.$defs).toBeDefined();
+ expect(countLocalReferenceExpansions(advertised)).toBe(8);
+ });
+
+ it("keeps shared definitions exactly at the budget", () => {
+ const advertised = advertisedInput(
+ inputWithReusedFields(advertisedReferenceExpansionBudget),
+ );
+ expect(advertised.$defs).toBeDefined();
+ expect(countLocalReferenceExpansions(advertised)).toBe(
+ advertisedReferenceExpansionBudget,
+ );
+ });
+
+ it("re-projects without shared definitions once the budget is exceeded", () => {
+ const advertised = advertisedInput(
+ inputWithReusedFields(advertisedReferenceExpansionBudget + 1),
+ );
+ expect(advertised.$defs).toBeUndefined();
+ expect(countLocalReferenceExpansions(advertised)).toBe(0);
+ const properties = advertised.properties as Record;
+ expect(properties.field_00).toMatchObject({
+ type: "object",
+ properties: { kind: { const: "rec" }, value: { type: "string" } },
+ required: ["kind", "value"],
+ });
+ });
+
+ it("keeps the shared form untouched when it fits and swaps it when not", () => {
+ const shared = {
+ type: "object",
+ properties: { value: { $ref: "#/$defs/shared" } },
+ $defs: { shared: { type: "string" } },
+ };
+ const inlined = {
+ type: "object",
+ properties: { value: { type: "string" } },
+ };
+ expect(
+ inputSchemaWithinReferenceExpansionBudget(
+ shared,
+ () => inlined,
+ advertisedReferenceExpansionBudget,
+ ),
+ ).toBe(shared);
+ expect(
+ inputSchemaWithinReferenceExpansionBudget(
+ {
+ ...shared,
+ properties: Object.fromEntries(
+ Array.from({ length: 33 }, (_, index) => [
+ `field_${index}`,
+ { $ref: "#/$defs/shared" },
+ ]),
+ ),
+ },
+ () => inlined,
+ advertisedReferenceExpansionBudget,
+ ),
+ ).toBe(inlined);
+ });
+});
+
describe("root input presentation", () => {
it("preserves prototype-named properties in object-root presentation", () => {
const fields = Object.fromEntries([
diff --git a/src/contracts/toolOutputSchemaGroups.ts b/src/contracts/toolOutputSchemaGroups.ts
index 76294e2db..1a36c0896 100644
--- a/src/contracts/toolOutputSchemaGroups.ts
+++ b/src/contracts/toolOutputSchemaGroups.ts
@@ -1,4 +1,5 @@
import { z } from "zod";
+import { byteSearchResultSchema } from "../domain/native/bytePattern.js";
import {
artifactInventorySummaryViewResultSchema,
machoSummaryViewResultSchema,
@@ -108,6 +109,7 @@ const bookmarkFacetSchema = z.discriminatedUnion("state", [
/** Exact structured-content schemas shared by direct analysis providers. */
export const officialOutputSchemas: Readonly> = {
+ search_bytes: evidenceResultOf(byteSearchResultSchema),
annotate_native_function: evidenceResultOf(nativeFunctionAnnotationsSchema),
inspect_native_load_image: evidenceResultOf(nativeLoadImageSchema),
inspect_native_data_type: evidenceResultOf(nativeDataTypeSchema),
diff --git a/src/contracts/toolSchemaMetadata.ts b/src/contracts/toolSchemaMetadata.ts
index 5ed63db5b..3034b8354 100644
--- a/src/contracts/toolSchemaMetadata.ts
+++ b/src/contracts/toolSchemaMetadata.ts
@@ -2,7 +2,11 @@ import { z } from "zod";
import type { ToolContract } from "./toolContractTypes.js";
import { advertisedInputExamples } from "./advertisedInputExamples.js";
-import { presentInputJsonSchema } from "./inputSchemaPresentation.js";
+import {
+ advertisedReferenceExpansionBudget,
+ inputSchemaWithinReferenceExpansionBudget,
+ presentInputJsonSchema,
+} from "./inputSchemaPresentation.js";
import { digestCanonicalValue } from "../domain/canonicalDigest.js";
const PROPERTY_DESCRIPTIONS: Readonly> = {
@@ -66,7 +70,25 @@ export const toolInputSchemaWithMetadata = (
});
const examples = advertisedInputExamples(contract);
return {
- ...presentInputJsonSchema(shared, fallbackPropertyDescription),
+ // Sharing definitions only helps clients that resolve them. A client
+ // rendering declarations from a shared expansion budget degrades
+ // every reference past it to `unknown`, so an input whose shared form
+ // exceeds the budget is re-projected without shared definitions.
+ ...inputSchemaWithinReferenceExpansionBudget(
+ presentInputJsonSchema(shared, fallbackPropertyDescription),
+ () =>
+ presentInputJsonSchema(
+ project({
+ ...options,
+ libraryOptions: {
+ ...options.libraryOptions,
+ reused: "inline",
+ },
+ }),
+ fallbackPropertyDescription,
+ ),
+ advertisedReferenceExpansionBudget,
+ ),
...(examples.length === 0 ? {} : { examples }),
};
},
diff --git a/src/contracts/wasm/wasmComparisonContracts.ts b/src/contracts/wasm/wasmComparisonContracts.ts
new file mode 100644
index 000000000..7eae508b7
--- /dev/null
+++ b/src/contracts/wasm/wasmComparisonContracts.ts
@@ -0,0 +1,63 @@
+import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js";
+import { z } from "zod";
+import { evidenceInputSchema } from "../evidenceInputContracts.js";
+import { wasmInterfaceSchema } from "../../domain/wasm/wasmInterface.js";
+import { toolContractMetadata } from "../toolEffects.js";
+import type { ToolContract } from "../toolContractTypes.js";
+
+export const compareWasmInterfacesInputSchema = z.strictObject({
+ left: evidenceInputSchema.describe(
+ "Exact complete inspect_wasm_artifact Evidence or retained session reference",
+ ),
+ right: evidenceInputSchema.describe(
+ "Exact complete inspect_wasm_artifact Evidence or retained session reference",
+ ),
+ scope: z
+ .enum(["imports", "exports", "both"])
+ .describe(
+ "Explicit interface selection; static equality does not establish runtime compatibility",
+ ),
+});
+const identity = z.object({
+ evidence_id: z.string(),
+ artifact: z.object({
+ path: z.string(),
+ sha256: z.string(),
+ bytes: z.number().int().nonnegative(),
+ }),
+});
+export const wasmInterfaceComparisonSchema = z.object({
+ left: identity,
+ right: identity,
+ scope: z.enum(["imports", "exports", "both"]),
+ coverage: z.enum(["complete-within-inputs", "partial", "unknown"]),
+ changes: z.array(
+ z.object({
+ direction: z.enum(["import", "export"]),
+ module: z.string().nullable(),
+ name: z.string(),
+ state: z.enum(["added", "removed", "changed", "unchanged", "unknown"]),
+ left: wasmInterfaceSchema.shape.entries,
+ right: wasmInterfaceSchema.shape.entries,
+ reason: z.string().nullable(),
+ }),
+ ),
+ summary: z.object({
+ added: z.number(),
+ removed: z.number(),
+ changed: z.number(),
+ unchanged: z.number(),
+ unknown: z.number(),
+ }),
+ limitations: z.array(z.string()),
+});
+export const COMPARE_WASM_INTERFACES_CONTRACT = {
+ name: "compare_wasm_interfaces",
+ ...toolContractMetadata("compare_wasm_interfaces"),
+ kind: "application",
+ description:
+ "Compare typed core WASM import/export names, kinds and exact signatures from two authenticated inspect_wasm_artifact Evidence records. Select imports, exports or both explicitly. Retained references resolve only in the current session; CLI accepts portable inline Evidence. Duplicate names, unsupported types and missing historical interface facets remain unknown. Preserve original module digests and every occurrence; no source acquisition, module execution or runtime-compatibility inference.",
+ inputSchema: compareWasmInterfacesInputSchema,
+ outputSchema: evidenceResultOf(wasmInterfaceComparisonSchema),
+ examples: [],
+} as const satisfies ToolContract;
diff --git a/src/contracts/wasm/wasmToolContracts.ts b/src/contracts/wasm/wasmToolContracts.ts
index 34f26f11d..cf05166d2 100644
--- a/src/contracts/wasm/wasmToolContracts.ts
+++ b/src/contracts/wasm/wasmToolContracts.ts
@@ -1,3 +1,4 @@
+import { COMPARE_WASM_INTERFACES_CONTRACT } from "./wasmComparisonContracts.js";
import {
inspectWasmArtifactInputSchema,
wasmArtifactSchema,
@@ -40,4 +41,7 @@ export const INSPECT_WASM_ARTIFACT_CONTRACT = {
} as const satisfies ToolContract;
/** Keep the public catalog in its existing order, using its named contract. */
-export const WASM_TOOL_CONTRACTS = [INSPECT_WASM_ARTIFACT_CONTRACT] as const;
+export const WASM_TOOL_CONTRACTS = [
+ INSPECT_WASM_ARTIFACT_CONTRACT,
+ COMPARE_WASM_INTERFACES_CONTRACT,
+] as const;
diff --git a/src/contracts/workflowInputContracts.test.ts b/src/contracts/workflowInputContracts.test.ts
index 32e1ddf02..237469487 100644
--- a/src/contracts/workflowInputContracts.test.ts
+++ b/src/contracts/workflowInputContracts.test.ts
@@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest";
+import { managedNativeVerificationInputSchema } from "../domain/managed/managedNativeVerificationSchemas.js";
+import { toolContract } from "./toolContracts.js";
+import { toolInputSchemaWithMetadata } from "./toolSchemaMetadata.js";
+
import { traceJavaScriptSemanticsRequestSchema } from "./javascript/applicationWorkflowInputContracts.js";
import { JAVASCRIPT_FEATURE_TRACE_EXAMPLE } from "./javascript/javascriptApplicationWorkflowExamples.js";
import {
@@ -10,7 +14,6 @@ import {
import {
compareManagedMembersReferenceInputSchema,
managedApplicationGraphReferenceInputSchema,
- managedNativeVerificationReferenceInputSchema,
} from "./managed/managedWorkflowToolContracts.js";
describe("workflow input contracts", () => {
@@ -41,17 +44,6 @@ describe("workflow input contracts", () => {
}).success,
).toBe(false);
- expect(
- managedNativeVerificationReferenceInputSchema.safeParse({
- managed_boundaries:
- MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries,
- native_observations: [
- MANAGED_NATIVE_VERIFICATION_EXAMPLE.native_observations[0],
- MANAGED_NATIVE_VERIFICATION_EXAMPLE.native_observations[0],
- ],
- }).success,
- ).toBe(false);
-
expect(
managedApplicationGraphReferenceInputSchema.safeParse({
managed_artifact: MANAGED_APPLICATION_GRAPH_EXAMPLE.managed_members,
@@ -62,4 +54,80 @@ describe("workflow input contracts", () => {
managedApplicationGraphReferenceInputSchema.safeParse({}).success,
).toBe(false);
});
+
+ it.each([
+ {
+ name: "one observation reusing the boundary",
+ observations: [MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries],
+ issues: [
+ {
+ path: ["native_observations", 0],
+ message:
+ "Native observation Evidence must be distinct from managed boundary Evidence",
+ },
+ ],
+ },
+ {
+ name: "repeated native observations",
+ observations: [
+ MANAGED_NATIVE_VERIFICATION_EXAMPLE.native_observations[0],
+ MANAGED_NATIVE_VERIFICATION_EXAMPLE.native_observations[0],
+ ],
+ issues: [
+ {
+ path: ["native_observations", 1],
+ message: "Native observation Evidence IDs must be unique",
+ },
+ ],
+ },
+ {
+ name: "repeated observations reusing the boundary",
+ observations: [
+ MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries,
+ MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries,
+ ],
+ issues: [
+ {
+ path: ["native_observations", 0],
+ message:
+ "Native observation Evidence must be distinct from managed boundary Evidence",
+ },
+ {
+ path: ["native_observations", 1],
+ message:
+ "Native observation Evidence must be distinct from managed boundary Evidence",
+ },
+ {
+ path: ["native_observations", 1],
+ message: "Native observation Evidence IDs must be unique",
+ },
+ ],
+ },
+ {
+ name: "distinct native observations",
+ observations: MANAGED_NATIVE_VERIFICATION_EXAMPLE.native_observations,
+ issues: [],
+ },
+ ])(
+ "preserves managed/native diagnostics for $name",
+ async ({ observations, issues }) => {
+ const input = {
+ managed_boundaries:
+ MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries,
+ native_observations: observations,
+ };
+ const contract = toolContract("verify_managed_native_boundaries");
+ for (const schema of [
+ managedNativeVerificationInputSchema,
+ contract.inputSchema,
+ toolInputSchemaWithMetadata(contract),
+ ]) {
+ const result = await schema["~standard"].validate(input);
+ expect(
+ result.issues?.map(({ path, message }) => ({ path, message })) ?? [],
+ ).toEqual(issues);
+ if (issues.length === 0) expect(result).toEqual({ value: input });
+ }
+ },
+ );
});
diff --git a/src/domain/analysisView/analysisView.test.ts b/src/domain/analysisView/analysisView.test.ts
index 07e6909c3..4bf16e4b4 100644
--- a/src/domain/analysisView/analysisView.test.ts
+++ b/src/domain/analysisView/analysisView.test.ts
@@ -792,3 +792,29 @@ it("rejects unsupported parent operations and incompatible collections", () => {
error: { _tag: "AnalysisInputError" },
});
});
+
+it("preserves selected native facet unavailability instead of an exhausted empty page", () => {
+ const parent = nativeParent({
+ ...functionDossierSchema.parse(ghidraFunctionDossier()),
+ facet_availability: {
+ comments: { available: false, reason: "Typed comments unavailable" },
+ references: { available: false, reason: "Typed references unavailable" },
+ },
+ });
+ for (const [facet, reason] of [
+ ["comments", "Typed comments unavailable"],
+ ["incoming_references", "Typed references unavailable"],
+ ] as const) {
+ const result = projectAnalysisView(parent, {
+ kind: "native",
+ facet,
+ offset: 0,
+ limit: 8,
+ });
+ if (!result.ok) throw result.error;
+ expect(result.value).toMatchObject({
+ item: { available: false, reason },
+ unknowns: expect.arrayContaining([reason]),
+ });
+ }
+});
diff --git a/src/domain/analysisView/nativeFunctionView.ts b/src/domain/analysisView/nativeFunctionView.ts
index b56de24a1..cac7ef82f 100644
--- a/src/domain/analysisView/nativeFunctionView.ts
+++ b/src/domain/analysisView/nativeFunctionView.ts
@@ -110,6 +110,22 @@ export const projectNativeFunctionView = (
]),
);
const common = parentFields(parent, dossier, artifact);
+ const availabilityKeys: Readonly> = {
+ incoming_references: "references",
+ outgoing_references: "references",
+ referenced_strings: "strings_names",
+ referenced_names: "strings_names",
+ basic_blocks: "cfg",
+ };
+ const availabilityKey = availabilityKeys[view.facet] ?? view.facet;
+ const availability =
+ dossier.facet_availability?.[
+ availabilityKey as keyof NonNullable<
+ FunctionDossier["facet_availability"]
+ >
+ ];
+ if (availability?.available === false)
+ common.unknowns = uniqueSorted([...common.unknowns, availability.reason]);
const singleton = (item: unknown): UnsignedAnalysisView => ({
kind: "native",
view,
@@ -117,6 +133,7 @@ export const projectNativeFunctionView = (
coverage: completeWithinViewCoverage(1, 1),
...common,
});
+ if (availability?.available === false) return ok(singleton(availability));
const flow = dossier.native_value_flow;
if (
view.facet === "procedure" ||
diff --git a/src/domain/android/androidAnalysis.ts b/src/domain/android/androidAnalysis.ts
index ca4d6db59..caa3bf5ee 100644
--- a/src/domain/android/androidAnalysis.ts
+++ b/src/domain/android/androidAnalysis.ts
@@ -40,7 +40,15 @@ export const androidInputSchemas = {
.min(1)
.optional()
.describe(
- "Omit for class references; ambiguous overloaded methods are unsupported",
+ "Omit for class references; select ambiguous methods with overload_index",
+ ),
+ overload_index: z
+ .number()
+ .int()
+ .nonnegative()
+ .optional()
+ .describe(
+ "Index among same-name methods from inspect_android_class; requires method_name",
),
}),
} as const;
@@ -90,12 +98,19 @@ const textCoverage = z.strictObject({
text: z.string(),
reported_total_bytes: z.number().int().nonnegative().nullable(),
});
+/** A DEX prototype: parameter descriptors followed by the return descriptor. */
+export const dexDescriptorSchema = z
+ .string()
+ .regex(
+ /^\((?:\[*(?:[ZBSCIJFD]|L[^;\x28\x29]+;))*\)(?:V|\[*(?:[ZBSCIJFD]|L[^;\x28\x29]+;))$/u,
+ );
+
const method = z.strictObject({
name: z.string(),
reported_signature: z.string(),
overload_index: z.number().int().nonnegative(),
is_constructor: z.boolean(),
- dex_descriptor: z.null(),
+ dex_descriptor: dexDescriptorSchema,
});
const engine = z.strictObject({
name: z.string().min(1),
@@ -159,6 +174,9 @@ export const androidResultSchemas = {
...common,
class_name: z.string(),
method_name: z.string().nullable(),
+ overload_index: z.number().int().nonnegative().nullable(),
+ reported_signature: z.string().nullable(),
+ dex_descriptor: dexDescriptorSchema.nullable(),
total: z.number().int().nonnegative(),
references: z.array(
z.strictObject({
@@ -169,8 +187,23 @@ export const androidResultSchemas = {
top_class: z.string(),
source_line: z.null(),
dex_offset: z.null(),
+ /** false means transport-bounded instruction evidence; null/omitted is unknown. */
+ direct_invokes_exhaustive: z.boolean().nullable().default(null),
+ /** Raw direct invoke instructions inside this using node, when available. */
+ direct_invokes: z
+ .array(
+ z.strictObject({
+ dex_entry: z.string().min(1),
+ source_raw_method_id: z.string().min(1),
+ dex_code_unit_offset: z.number().int().nonnegative(),
+ dex_method_index: z.number().int().nonnegative(),
+ opcode: z.string().regex(/^INVOKE_/u),
+ }),
+ )
+ .nullable(),
}),
),
coverage: z.literal("complete"),
+ reference_unit: z.literal("using_node"),
}),
} as const;
diff --git a/src/domain/apple/appleApplication.test.ts b/src/domain/apple/appleApplication.test.ts
index 9caf21fcb..3f8906e2b 100644
--- a/src/domain/apple/appleApplication.test.ts
+++ b/src/domain/apple/appleApplication.test.ts
@@ -417,6 +417,34 @@ describe("Apple bundle layouts and containers", () => {
});
});
+describe("Apple IPA components", () => {
+ it("excludes AppleDouble sidecars from IPA components", () => {
+ const result = project(
+ inventoryEvidence("ipa", "Fixture.ipa", [
+ { path: "Payload/Fixture.app/Info.plist" },
+ macho("Payload/Fixture.app/Fixture"),
+ { path: "Payload/Fixture.app/Contents/Resources/._libfoo.dylib" },
+ { path: "Payload/Fixture.app/Frameworks/Foo.framework/._Foo" },
+ { path: "__MACOSX/Payload/Fixture.app/libghost.dylib" },
+ macho("Payload/Fixture.app/Contents/Resources/libreal.dylib"),
+ macho("SwiftSupport/iphoneos/libswiftCore.dylib"),
+ ]),
+ );
+ expect(result.components.native_libraries.map(({ path }) => path)).toEqual([
+ "Payload/Fixture.app/Contents/Resources/libreal.dylib",
+ "SwiftSupport/iphoneos/libswiftCore.dylib",
+ ]);
+ expect(result.components.frameworks).toEqual([]);
+ expect(result.components.executables.map(({ path }) => path)).toEqual([
+ "Payload/Fixture.app/Contents/Resources/libreal.dylib",
+ "Payload/Fixture.app/Fixture",
+ ]);
+ expect(result.limitations).toContainEqual(
+ expect.stringContaining("AppleDouble sidecar entries"),
+ );
+ });
+});
+
describe("macOS application scope", () => {
it("attributes only components inside the application roots", () => {
const result = project(
diff --git a/src/domain/apple/appleApplication.ts b/src/domain/apple/appleApplication.ts
index d46ddf26b..dfb34e3c5 100644
--- a/src/domain/apple/appleApplication.ts
+++ b/src/domain/apple/appleApplication.ts
@@ -218,8 +218,9 @@ export const appleProjectionId = (projection: unknown): string =>
`aap_${digest(projection)}`;
/**
- * IPA projection keeps every archive component, as before. Directory, ZIP,
- * and DMG inventories can hold installers and other siblings, so only
+ * AppleDouble sidecars describe neighbouring files and are never components.
+ * IPA projection keeps every other archive component, as before. Directory,
+ * ZIP, and DMG inventories can hold installers and other siblings, so only
* components inside an application root are attributed to it.
*/
const attributeComponents = (
@@ -227,8 +228,8 @@ const attributeComponents = (
roots: readonly string[],
rootFormat: RootFormat,
): { readonly all: Component[]; readonly unattributed: number } => {
- if (rootFormat === "ipa") return { all: [...inventoried], unattributed: 0 };
const visible = inventoried.filter(({ path }) => !isSidecar(path));
+ if (rootFormat === "ipa") return { all: visible, unattributed: 0 };
const all = visible.filter(({ path }) =>
roots.some((root) => isWithin(path, root)),
);
diff --git a/src/domain/browserObservation.test.ts b/src/domain/browserObservation.test.ts
index c5eb73287..6bcf09f68 100644
--- a/src/domain/browserObservation.test.ts
+++ b/src/domain/browserObservation.test.ts
@@ -2,7 +2,9 @@ import { describe, expect, it } from "vitest";
import {
browserEndpointSchema,
+ browserObservationEndpointSchema,
browserOriginSchema,
+ recordedBrowserObservationEndpointSchema,
sanitizeEndpointCandidate,
sanitizeBrowserUrl,
} from "./browserObservation.js";
@@ -45,6 +47,58 @@ describe("browser observation contracts", () => {
])
expect(browserEndpointSchema.safeParse(value).success, value).toBe(false);
});
+
+ it("accepts a loopback browser socket and records it without its identifier", () => {
+ expect(
+ browserObservationEndpointSchema.parse("http://127.0.0.1:9222/"),
+ ).toEqual({
+ transport: "http_discovery",
+ url: "http://127.0.0.1:9222",
+ recorded: "http://127.0.0.1:9222",
+ });
+ expect(
+ browserObservationEndpointSchema.parse(
+ "ws://[::1]:9222/devtools/browser/0b1c-socket-id",
+ ),
+ ).toEqual({
+ transport: "browser_websocket",
+ url: "ws://[::1]:9222/devtools/browser/0b1c-socket-id",
+ recorded: "ws://[::1]:9222/devtools/browser/[REDACTED]",
+ });
+ for (const value of [
+ "ws://127.0.0.1/devtools/browser/id",
+ "ws://localhost:9222/devtools/browser/id",
+ "wss://127.0.0.1:9222/devtools/browser/id",
+ "ws://user:pass@127.0.0.1:9222/devtools/browser/id",
+ "ws://127.0.0.1:9222/devtools/browser/id?token=x",
+ "ws://127.0.0.1:9222/devtools/browser/id#fragment",
+ "ws://127.0.0.1:9222/devtools/browser/id?",
+ "ws://127.0.0.1:9222/devtools/browser/id#",
+ "ws://127.0.0.1:9222/devtools/browser/",
+ "ws://127.0.0.1:9222/devtools/browser/id/extra",
+ "ws://127.0.0.1:9222/devtools/page/id",
+ ])
+ expect(
+ browserObservationEndpointSchema.safeParse(value).success,
+ value,
+ ).toBe(false);
+ });
+
+ it("accepts recorded browser sockets only in their redacted form", () => {
+ for (const value of [
+ "http://127.0.0.1:9222",
+ "ws://127.0.0.1:9222/devtools/browser/[REDACTED]",
+ ])
+ expect(
+ recordedBrowserObservationEndpointSchema.safeParse(value).success,
+ value,
+ ).toBe(true);
+ expect(
+ recordedBrowserObservationEndpointSchema.safeParse(
+ "ws://127.0.0.1:9222/devtools/browser/0b1c-socket-id",
+ ).success,
+ ).toBe(false);
+ });
});
describe("browser observation sensitive surfaces and retention", () => {
diff --git a/src/domain/browserObservation.ts b/src/domain/browserObservation.ts
index f4b8984b6..e15265305 100644
--- a/src/domain/browserObservation.ts
+++ b/src/domain/browserObservation.ts
@@ -43,46 +43,125 @@ export const browserOriginSchema = z
export const isLiteralLoopbackHostname = (hostname: string): boolean =>
hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
+const HTTP_ENDPOINT_MESSAGE =
+ "CDP endpoint must be an explicit-port HTTP URL on 127.0.0.1 or ::1";
+const BROWSER_SOCKET_PATH = "/devtools/browser/";
+const REDACTED_BROWSER_SOCKET_PATH = `${BROWSER_SOCKET_PATH}[REDACTED]`;
+
+/** Normalized origin of an accepted loopback CDP HTTP discovery endpoint. */
+const httpDiscoveryOrigin = (url: URL): string | undefined =>
+ url.protocol === "http:" &&
+ isLiteralLoopbackHostname(url.hostname) &&
+ url.port !== "" &&
+ url.username === "" &&
+ url.password === "" &&
+ url.pathname === "/" &&
+ !urlDeclaresQueryOrFragment(url)
+ ? url.origin
+ : undefined;
+
+/** Scheme and authority of an accepted loopback CDP browser WebSocket. */
+const browserSocketAuthority = (url: URL): string | undefined => {
+ const id = url.pathname.slice(BROWSER_SOCKET_PATH.length);
+ return url.protocol === "ws:" &&
+ isLiteralLoopbackHostname(url.hostname) &&
+ url.port !== "" &&
+ url.username === "" &&
+ url.password === "" &&
+ !urlDeclaresQueryOrFragment(url) &&
+ url.pathname.startsWith(BROWSER_SOCKET_PATH) &&
+ id.length > 0 &&
+ !id.includes("/")
+ ? `${url.protocol}//${url.host}`
+ : undefined;
+};
+
/** Loopback-only HTTP endpoint accepted for a user-owned CDP browser. */
export const browserEndpointSchema = z
.string()
.min(1)
.transform((value, context) => {
- let url: URL;
- try {
- url = new URL(value);
- } catch (cause: unknown) {
- // Invalid input is reported through the zod issue.
- void cause;
+ const url = URL.parse(value);
+ const origin = url === null ? undefined : httpDiscoveryOrigin(url);
+ if (origin !== undefined) return origin;
+ context.addIssue({
+ code: "custom",
+ message:
+ url === null ? "Invalid CDP endpoint URL" : HTTP_ENDPOINT_MESSAGE,
+ });
+ return z.NEVER;
+ });
+
+/**
+ * Caller-selected endpoint for passive browser observation. A browser socket
+ * skips HTTP discovery; its identifier gates access, so `recorded` omits it.
+ */
+export interface BrowserObservationEndpoint {
+ readonly transport: "http_discovery" | "browser_websocket";
+ readonly url: string;
+ readonly recorded: string;
+}
+
+/** Loopback CDP HTTP discovery origin or browser WebSocket URL. */
+export const browserObservationEndpointSchema = z
+ .string()
+ .min(1)
+ .describe(
+ "Literal loopback CDP endpoint: http://127.0.0.1:PORT, or ws://127.0.0.1:PORT/devtools/browser/ID.",
+ )
+ .transform((value, context): BrowserObservationEndpoint => {
+ const url = URL.parse(value);
+ if (url === null) {
context.addIssue({ code: "custom", message: "Invalid CDP endpoint URL" });
return z.NEVER;
}
- if (
- url.protocol !== "http:" ||
- !isLiteralLoopbackHostname(url.hostname) ||
- url.port === "" ||
- url.username !== "" ||
- url.password !== "" ||
- url.pathname !== "/" ||
- urlDeclaresQueryOrFragment(url)
- ) {
- context.addIssue({
- code: "custom",
- message:
- "CDP endpoint must be an explicit-port HTTP URL on 127.0.0.1 or ::1",
- });
- return z.NEVER;
- }
- return url.origin;
+ const origin = httpDiscoveryOrigin(url);
+ if (origin !== undefined)
+ return { transport: "http_discovery", url: origin, recorded: origin };
+ const authority = browserSocketAuthority(url);
+ if (authority !== undefined)
+ return {
+ transport: "browser_websocket",
+ url: url.href,
+ recorded: `${authority}${REDACTED_BROWSER_SOCKET_PATH}`,
+ };
+ context.addIssue({
+ code: "custom",
+ message:
+ url.protocol === "ws:"
+ ? "CDP browser WebSocket must be ws://127.0.0.1:PORT/devtools/browser/ID or ws://[::1]:PORT/devtools/browser/ID"
+ : HTTP_ENDPOINT_MESSAGE,
+ });
+ return z.NEVER;
});
+/** Evidence form of a passive browser endpoint, with any socket identifier redacted. */
+export const recordedBrowserObservationEndpointSchema = z
+ .string()
+ .min(1)
+ .refine(
+ (value) => {
+ const url = URL.parse(value);
+ return (
+ url !== null &&
+ (httpDiscoveryOrigin(url) !== undefined ||
+ (browserSocketAuthority(url) !== undefined &&
+ url.pathname === REDACTED_BROWSER_SOCKET_PATH))
+ );
+ },
+ {
+ message:
+ "Recorded CDP endpoint must be a loopback HTTP origin or a redacted browser WebSocket URL",
+ },
+ );
+
export const browserAllowedOriginsSchema = z
.array(browserOriginSchema)
.transform((origins) => Array.from(new Set(origins)).sort())
.default([]);
const browserInput = {
- cdp_endpoint: browserEndpointSchema,
+ cdp_endpoint: browserObservationEndpointSchema,
allowed_origins: browserAllowedOriginsSchema,
};
diff --git a/src/domain/browserSession.ts b/src/domain/browserSession.ts
index 17ecfa8db..c2f00e9a2 100644
--- a/src/domain/browserSession.ts
+++ b/src/domain/browserSession.ts
@@ -2,14 +2,14 @@ import { z } from "zod";
import {
browserAllowedOriginsSchema,
- browserEndpointSchema,
+ browserObservationEndpointSchema,
} from "./browserObservation.js";
import { browserCompletenessSchema } from "./browserCompleteness.js";
import { browserVersionSchema } from "./browserObservationSchemas.js";
/** Input for a navigation-aware browser observation window. */
export const observeWebSessionInputSchema = z.strictObject({
- cdp_endpoint: browserEndpointSchema,
+ cdp_endpoint: browserObservationEndpointSchema,
allowed_origins: browserAllowedOriginsSchema,
target_id: z.string().trim().min(1),
observation_ms: z.number().int().min(1).default(10_000),
diff --git a/src/domain/callPath.test.ts b/src/domain/callPath.test.ts
index e78a5f936..80a260ac3 100644
--- a/src/domain/callPath.test.ts
+++ b/src/domain/callPath.test.ts
@@ -15,6 +15,7 @@ const observe = (
readonly evidenceLimitations?: readonly string[];
readonly dossierLimitations?: readonly string[];
readonly relationshipCoverage?: RelationshipCoverage;
+ readonly calleeUnavailable?: string;
} = {},
): Evidence => {
const value = {
@@ -52,6 +53,13 @@ const observe = (
native_value_flow: null,
basic_blocks: [],
limitations: [...(options.dossierLimitations ?? [])],
+ ...(options.calleeUnavailable === undefined
+ ? {}
+ : {
+ facet_availability: {
+ callees: { available: false, reason: options.calleeUnavailable },
+ },
+ }),
} satisfies JsonValue;
return createEvidence(
{ path: "/tmp/a", sha256: "a".repeat(64), format: "mach-o" },
@@ -541,3 +549,13 @@ describe("call path evidence admission", () => {
);
});
});
+
+it("keeps an absent path unknown when only callee facet availability is declared", () => {
+ const result = run([
+ observe("0x1000", [], "rea-workflow", {
+ calleeUnavailable: "Graph callees unavailable",
+ }),
+ ]);
+ expect(result.status).toBe("unknown");
+ expect(result.limitations).toContain("Graph callees unavailable");
+});
diff --git a/src/domain/callPath.ts b/src/domain/callPath.ts
index 0bd5e88e1..3ffbc2356 100644
--- a/src/domain/callPath.ts
+++ b/src/domain/callPath.ts
@@ -253,6 +253,11 @@ const inspectSearch = ({
// cannot add a path to the goal, so its dossier limits are contextual
// but do not make the searched start-to-goal closure incomplete.
if (node === goal) continue;
+ const callees = snapshot.dossier.facet_availability?.callees;
+ if (callees?.available === false) {
+ limitations.push(callees.reason);
+ incompleteExpandedCoverage = true;
+ }
// Explicit, Evidence-bound relationship coverage owns completeness;
// generic provider/dossier limitations remain contextual. Legacy Evidence
// without that metadata retains its conservative limitation-based rule.
diff --git a/src/domain/evm/evmInterface.test.ts b/src/domain/evm/evmInterface.test.ts
index 93ab32add..1b2a36de6 100644
--- a/src/domain/evm/evmInterface.test.ts
+++ b/src/domain/evm/evmInterface.test.ts
@@ -2,6 +2,14 @@ import { expect, it } from "vitest";
import { evmInterfaceSchema } from "./evmInterface.js";
import { evmInterfaceFixture } from "../../../tests/fixtures/evm/interface.js";
+it("accepts historical metadata omission as an unknown facet without changing the source", () => {
+ const value = evmInterfaceFixture();
+ const { metadata: _metadata, ...oldBytecode } = value.bytecode;
+ const old = { ...value, bytecode: oldBytecode };
+ expect(evmInterfaceSchema.parse(old).bytecode.metadata).toBeNull();
+ expect(old.bytecode).not.toHaveProperty("metadata");
+});
+
it.each(["byte-count", "body-offset", "selector", "authenticity"])(
"rejects false interface evidence: %s",
(problem) => {
diff --git a/src/domain/evm/evmInterface.ts b/src/domain/evm/evmInterface.ts
index 6db3f7e16..bde6ecba7 100644
--- a/src/domain/evm/evmInterface.ts
+++ b/src/domain/evm/evmInterface.ts
@@ -1,6 +1,7 @@
import { z } from "zod";
import { localPathStringSchema } from "../localPath.js";
+import { solidityMetadataSchema } from "./solidityMetadata.js";
const byteOffset = z.number().int().nonnegative();
const sha256 = z.string().regex(/^[a-f0-9]{64}$/);
@@ -34,6 +35,7 @@ export const evmInterfaceSchema = z
kind: z.literal("unknown"),
deployment_authenticity: z.literal("unknown"),
hardfork: z.literal("unknown"),
+ metadata: solidityMetadataSchema.nullable().default(null),
}),
evidence_kind: z.literal("inferred"),
functions: z.array(
@@ -62,6 +64,23 @@ export const evmInterfaceSchema = z
message:
"Selected byte count does not match the bytecode hex representation.",
});
+ const metadata = value.bytecode.metadata;
+ if (
+ metadata !== null &&
+ metadata.status !== "unavailable" &&
+ (metadata.bytecode_offset === null ||
+ metadata.cbor_bytes === null ||
+ metadata.bytecode_offset + metadata.cbor_bytes + 2 !==
+ value.bytecode.bytes ||
+ metadata.cbor_bytes !==
+ Number.parseInt(value.bytecode.hex.slice(-4), 16))
+ )
+ context.addIssue({
+ code: "custom",
+ path: ["bytecode", "metadata"],
+ message:
+ "Metadata range must match the terminal length suffix in selected bytecode.",
+ });
for (const [index, fn] of value.functions.entries())
if (fn.bytecode_offset >= value.bytecode.bytes)
context.addIssue({
diff --git a/src/domain/evm/solidityMetadata.ts b/src/domain/evm/solidityMetadata.ts
new file mode 100644
index 000000000..28cdee95f
--- /dev/null
+++ b/src/domain/evm/solidityMetadata.ts
@@ -0,0 +1,89 @@
+import { z } from "zod";
+
+/** Scalar view of CBOR; uninterpreted containers remain in original bytecode. */
+export const solidityMetadataScalarSchema = z.discriminatedUnion("kind", [
+ z.strictObject({ kind: z.literal("text"), value: z.string().max(65535) }),
+ z.strictObject({
+ kind: z.literal("integer"),
+ value: z
+ .string()
+ .max(21)
+ .regex(/^-?(?:0|[1-9][0-9]*)$/),
+ }),
+ z.strictObject({
+ kind: z.literal("bytes"),
+ value: z
+ .string()
+ .max(131070)
+ .regex(/^(?:[a-f0-9]{2})*$/),
+ }),
+ z.strictObject({ kind: z.literal("boolean"), value: z.boolean() }),
+ z.strictObject({ kind: z.literal("null") }),
+ z.strictObject({
+ kind: z.literal("uninterpreted"),
+ container: z.enum(["array", "map"]),
+ }),
+]);
+
+/** A trailer candidate, not authentication of Solidity origin or deployment. */
+export const solidityMetadataSchema = z
+ .strictObject({
+ profile: z.literal("terminal-cbor-scalars-v1"),
+ status: z.enum(["unavailable", "decoded", "partial", "not-decoded"]),
+ bytecode_offset: z.number().int().nonnegative().nullable(),
+ cbor_bytes: z.number().int().positive().max(65535).nullable(),
+ entries: z
+ .array(
+ z.strictObject({
+ key: solidityMetadataScalarSchema,
+ value: solidityMetadataScalarSchema,
+ }),
+ )
+ .max(32767),
+ compiler_version: z
+ .strictObject({
+ representation: z.enum(["release-bytes", "prerelease-text"]),
+ value: z.string().min(1).max(65535),
+ })
+ .nullable(),
+ diagnostic: z.string().max(512).nullable(),
+ })
+ .superRefine((value, context) => {
+ const unavailable = value.status === "unavailable";
+ if (
+ unavailable
+ ? value.bytecode_offset !== null || value.cbor_bytes !== null
+ : value.bytecode_offset === null || value.cbor_bytes === null
+ )
+ context.addIssue({
+ code: "custom",
+ message: "Only unavailable metadata omits the candidate range.",
+ });
+ const unexamined = unavailable || value.status === "not-decoded";
+ if (
+ unexamined &&
+ (value.entries.length !== 0 || value.compiler_version !== null)
+ )
+ context.addIssue({
+ code: "custom",
+ message:
+ "Unavailable interpretation cannot report decoded entries or compiler identity.",
+ });
+ const uninterpreted = value.entries.some(
+ ({ key, value: entryValue }) =>
+ key.kind === "uninterpreted" || entryValue.kind === "uninterpreted",
+ );
+ if (
+ (value.status === "decoded" && uninterpreted) ||
+ (value.status === "partial" && !uninterpreted)
+ )
+ context.addIssue({
+ code: "custom",
+ message: "Scalar coverage must preserve uninterpreted containers.",
+ });
+ });
+
+export type SolidityMetadata = z.infer;
+export type SolidityMetadataScalar = z.infer<
+ typeof solidityMetadataScalarSchema
+>;
diff --git a/src/domain/functionComparison.test.ts b/src/domain/functionComparison.test.ts
index 677fdc0cc..16f8282c9 100644
--- a/src/domain/functionComparison.test.ts
+++ b/src/domain/functionComparison.test.ts
@@ -44,6 +44,16 @@ const dossier = (
referenced_names: [],
native_api: null,
native_value_flow: null,
+ facet_availability: {
+ pseudocode: { available: true },
+ assembly: { available: true },
+ comments: { available: true },
+ callers: { available: true },
+ callees: { available: true },
+ references: { available: true },
+ strings_names: { available: true },
+ cfg: { available: true },
+ },
basic_blocks: [
{
start: base,
@@ -118,34 +128,118 @@ const observe = (
},
);
-describe("function comparison normalized identity", () => {
- it("keeps differing generated names ambiguous and stable names mismatched", () => {
- const named = (base: "0x1000" | "0x2000", name: string) =>
- functionDossierSchema.parse({
- ...dossier("return 0;", base),
- procedure: {
- ...procedure(base, name),
- signature: "int helper(void)",
- locals: [],
- },
- });
- const generated = compareFunctions(
- observe("1", named("0x1000", "sub_1000")),
- observe("2", named("0x2000", "sub_2000")),
- );
- expect(generated.function_match.status).toBe("ambiguous");
- expect(generated.status).toBe("unknown");
- expect(
- generated.dimensions.find(({ dimension }) => dimension === "identity"),
- ).toMatchObject({ status: "unknown" });
+const rejectsMalformedFacetAvailability = (): void => {
+ expect(
+ functionDossierSchema.safeParse({
+ ...dossier("return 0;", "0x1000"),
+ facet_availability: { cfg: { available: false, reason: "" } },
+ }).success,
+ ).toBe(false);
+ expect(
+ functionDossierSchema.safeParse({
+ ...dossier("return 0;", "0x1000"),
+ facet_availability: { cfg: undefined },
+ }).success,
+ ).toBe(false);
+};
- const stable = compareFunctions(
- observe("3", named("0x1000", "firstFunction")),
- observe("4", named("0x2000", "secondFunction")),
+const keepsGeneratedNamesAmbiguous = (): void => {
+ const named = (base: "0x1000" | "0x2000", name: string) =>
+ functionDossierSchema.parse({
+ ...dossier("return 0;", base),
+ procedure: {
+ ...procedure(base, name),
+ signature: "int helper(void)",
+ locals: [],
+ },
+ });
+ const generated = compareFunctions(
+ observe("1", named("0x1000", "sub_1000")),
+ observe("2", named("0x2000", "sub_2000")),
+ );
+ expect(generated.function_match.status).toBe("ambiguous");
+ expect(generated.status).toBe("unknown");
+ expect(
+ generated.dimensions.find(({ dimension }) => dimension === "identity"),
+ ).toMatchObject({ status: "unknown" });
+
+ const stable = compareFunctions(
+ observe("3", named("0x1000", "firstFunction")),
+ observe("4", named("0x2000", "secondFunction")),
+ );
+ expect(stable.function_match.status).toBe("mismatched");
+ expect(stable.status).toBe("changed");
+};
+
+describe("function comparison normalized identity", () => {
+ it("retains unresolved call details alongside missing facet coverage", () => {
+ const value: Record = {
+ ...dossier("return 0;", "0x1000"),
+ unresolved_calls: [
+ { address: "0x1010", reason: "Unknown indirect target" },
+ ],
+ };
+ delete value.facet_availability;
+ const legacy = observe("a", functionDossierSchema.parse(value), "ida");
+ const calls = compareFunctions(legacy, legacy).dimensions.find(
+ ({ dimension }) => dimension === "calls",
);
- expect(stable.function_match.status).toBe("mismatched");
- expect(stable.status).toBe("changed");
+ expect(calls).toMatchObject({
+ status: "unknown",
+ limitations: expect.arrayContaining([
+ "Left: Unresolved call at 0x1010: Unknown indirect target",
+ "Left: callers: Provider did not declare coverage for this empty facet.",
+ ]),
+ });
});
+ it("keeps empty legacy facets without coverage declarations unknown", () => {
+ const value: Record = {
+ ...dossier("return 0;", "0x1000"),
+ basic_blocks: [],
+ };
+ delete value.facet_availability;
+ const legacy = observe("a", functionDossierSchema.parse(value), "ida");
+ const result = compareFunctions(legacy, legacy);
+ for (const dimension of [
+ "comments",
+ "references",
+ "strings_names",
+ "cfg",
+ "calls",
+ ]) {
+ expect(
+ result.dimensions.find((item) => item.dimension === dimension),
+ ).toMatchObject({ status: "unknown" });
+ }
+ });
+ it("keeps unavailable facets unknown even when one side supplies values", () => {
+ const left = observe(
+ "a",
+ functionDossierSchema.parse({
+ ...dossier("return 0;", "0x1000"),
+ facet_availability: {
+ comments: { available: false, reason: "Not collected" },
+ },
+ comments: [
+ { address: "0x1000", kind: "comment", text: "partial record" },
+ ],
+ }),
+ );
+ const right = observe("b", dossier("return 0;", "0x2000"));
+ expect(
+ compareFunctions(left, right).dimensions.find(
+ ({ dimension }) => dimension === "comments",
+ ),
+ ).toMatchObject({ status: "unknown", left_count: null, right_count: null });
+ });
+ it(
+ "rejects malformed facet availability instead of treating it as absence",
+ rejectsMalformedFacetAvailability,
+ );
+ it(
+ "keeps differing generated names ambiguous and stable names mismatched",
+ keepsGeneratedNamesAmbiguous,
+ );
it.each(["sub_deallocate", "sub_deadbeef_handler", "fcn.dispatch"])(
"recognizes the explicit symbol %s without a generated-name prefix false positive",
@@ -999,3 +1093,51 @@ const expectFunctionDimensionAlgebra = (
}).success,
).toBe(false);
};
+
+it.each([
+ { match: "mismatched", changedDimensions: false, expected: "changed" },
+ { match: "ambiguous", changedDimensions: false, expected: "unknown" },
+ { match: "ambiguous", changedDimensions: true, expected: "unknown" },
+] as const)(
+ "validates $match match precedence with changed dimensions=$changedDimensions",
+ ({ match, changedDimensions, expected }) => {
+ const result = compareFunctions(
+ FUNCTION_COMPARISON_EXAMPLE.left,
+ changedDimensions
+ ? FUNCTION_COMPARISON_EXAMPLE.right
+ : FUNCTION_COMPARISON_EXAMPLE.left,
+ );
+ const candidate = {
+ ...result,
+ function_match: {
+ status: match,
+ method: "explicit",
+ left_name: "main",
+ right_name: "other",
+ },
+ };
+ expect(
+ functionComparisonResultSchema.safeParse({
+ ...candidate,
+ status: expected,
+ }).success,
+ ).toBe(true);
+ for (const status of ["unchanged", "changed", "unknown"] as const) {
+ if (status === expected) continue;
+ const parsed = functionComparisonResultSchema.safeParse({
+ ...candidate,
+ status,
+ });
+ expect(parsed.success).toBe(false);
+ if (!parsed.success)
+ expect(parsed.error.issues).toMatchObject([
+ {
+ path: ["status"],
+ code: "custom",
+ message:
+ "Function comparison status contradicts its dimensions or function match.",
+ },
+ ]);
+ }
+ },
+);
diff --git a/src/domain/functionComparisonDimensions.ts b/src/domain/functionComparisonDimensions.ts
index c5a0f373f..f91057a6b 100644
--- a/src/domain/functionComparisonDimensions.ts
+++ b/src/domain/functionComparisonDimensions.ts
@@ -52,7 +52,67 @@ export const compareDimensions = (
compareReferences(left, right, context),
compareStringsAndNames(left, right, context),
compareCfg(left, right, context),
- ];
+ ].map((result) => {
+ const limitations = [
+ ...facetLimitations(left, result.dimension).map(
+ (reason) => `Left: ${reason}`,
+ ),
+ ...facetLimitations(right, result.dimension).map(
+ (reason) => `Right: ${reason}`,
+ ),
+ ];
+ return limitations.length === 0
+ ? result
+ : unresolvedDimension({
+ dimension: result.dimension,
+ links: context.links,
+ leftCount: null,
+ rightCount: null,
+ limitations: [...result.limitations, ...limitations],
+ });
+ });
+};
+
+/** An empty undeclared facet is not evidence of observed absence. */
+const facetLimitations = (
+ snapshot: FunctionSnapshot,
+ dimension: DimensionName,
+): string[] => {
+ const { dossier } = snapshot;
+ const fields = {
+ identity: [],
+ pseudocode: [["pseudocode", dossier.pseudocode.length]],
+ assembly: [["assembly", dossier.assembly.length]],
+ comments: [["comments", dossier.comments.length]],
+ calls: [
+ ["callers", dossier.callers.length],
+ ["callees", dossier.callees.length],
+ ],
+ references: [
+ [
+ "references",
+ dossier.incoming_references.length + dossier.outgoing_references.length,
+ ],
+ ],
+ strings_names: [
+ [
+ "strings_names",
+ dossier.referenced_strings.length + dossier.referenced_names.length,
+ ],
+ ],
+ cfg: [["cfg", dossier.basic_blocks.length]],
+ } satisfies Record<
+ DimensionName,
+ [keyof NonNullable, number][]
+ >;
+ return fields[dimension].flatMap(([field, count]) => {
+ const availability = dossier.facet_availability?.[field];
+ if (availability?.available === false)
+ return [`${field}: ${availability.reason}`];
+ return availability === undefined && count === 0
+ ? [`${field}: Provider did not declare coverage for this empty facet.`]
+ : [];
+ });
};
const compareText = (
diff --git a/src/domain/functionComparisonSchemas.ts b/src/domain/functionComparisonSchemas.ts
index bafc3def1..844bdc9b9 100644
--- a/src/domain/functionComparisonSchemas.ts
+++ b/src/domain/functionComparisonSchemas.ts
@@ -1,4 +1,5 @@
import { z } from "zod";
+import { overallStatus } from "./functionComparisonResults.js";
import { evidenceSchema } from "./evidence.js";
import { digestSchema } from "./../domain/digests.js";
import { prefixedDigestSchema } from "./../domain/digests.js";
@@ -79,20 +80,33 @@ const functionMatchSchema = z.union([
]);
/** Deterministic dimension-classified function comparison Evidence payload. */
-export const functionComparisonResultSchema = z.object({
- status: comparisonStatusSchema,
- function_match: functionMatchSchema,
- left_subject_sha256: digestSchema,
- right_subject_sha256: digestSchema,
- summary: z.object({
- unchanged: z.number().int().min(0),
- changed: z.number().int().min(0),
- unknown: z.number().int().min(0),
- }),
- dimensions: z.array(functionDimensionSchema).length(8),
- changes: z.array(functionDimensionSchema),
- limitations: z.array(z.string()),
-});
+export const functionComparisonResultSchema = z
+ .object({
+ status: comparisonStatusSchema,
+ function_match: functionMatchSchema,
+ left_subject_sha256: digestSchema,
+ right_subject_sha256: digestSchema,
+ summary: z.object({
+ unchanged: z.number().int().min(0),
+ changed: z.number().int().min(0),
+ unknown: z.number().int().min(0),
+ }),
+ dimensions: z.array(functionDimensionSchema).length(8),
+ changes: z.array(functionDimensionSchema),
+ limitations: z.array(z.string()),
+ })
+ .superRefine((result, context) => {
+ if (
+ result.status !==
+ overallStatus(result.dimensions, result.function_match.status)
+ )
+ context.addIssue({
+ code: "custom",
+ path: ["status"],
+ message:
+ "Function comparison status contradicts its dimensions or function match.",
+ });
+ });
export type FunctionComparisonResult = z.infer<
typeof functionComparisonResultSchema
diff --git a/src/domain/hopperValues.ts b/src/domain/hopperValues.ts
index d6344ff3e..b42a98383 100644
--- a/src/domain/hopperValues.ts
+++ b/src/domain/hopperValues.ts
@@ -1,4 +1,5 @@
import { z } from "zod";
+import { addressCoordinate } from "./native/addressCoordinate.js";
import type { JsonValue } from "./jsonValue.js";
@@ -38,16 +39,6 @@ export const addressDistance = (start: string, end: string): number => {
);
};
-const addressCoordinate = (
- value: string,
-): { readonly space: string; readonly offset: bigint } | null => {
- const separator = value.lastIndexOf(":0x");
- const space = separator < 0 ? "default" : value.slice(0, separator);
- const offset = separator < 0 ? value : value.slice(separator + 1);
- if (!/^0x[0-9a-f]+$/u.test(offset)) return null;
- return { space, offset: BigInt(offset) };
-};
-
const procedureMapSchema = z.record(z.string(), z.string());
const addressedNamesSchema = z.array(
z.object({ address: z.string(), name: z.string() }),
@@ -128,9 +119,9 @@ export const functionBodySchema = z.discriminatedUnion("available", [
})
.superRefine((body, context) => {
let total = 0n;
- const bounds = new Map();
- const closedSpaces = new Set();
- let previousSpace: string | null = null;
+ const bounds = new Map();
+ const closedSpaces = new Set();
+ let previousSpace: string | null | undefined;
for (const range of body.ranges) {
const start = addressCoordinate(range.start);
const end = addressCoordinate(range.end);
@@ -146,7 +137,7 @@ export const functionBodySchema = z.discriminatedUnion("available", [
});
return;
}
- if (previousSpace !== null && previousSpace !== start.space)
+ if (previousSpace !== undefined && previousSpace !== start.space)
closedSpaces.add(previousSpace);
const previous = bounds.get(start.space);
if (
@@ -371,6 +362,11 @@ export const referenceEdgeSchema = z
.exactOptional(),
})
.strict();
+const facetAvailabilitySchema = z.discriminatedUnion("available", [
+ z.object({ available: z.literal(true) }).strict(),
+ z.object({ available: z.literal(false), reason: z.string().min(1) }).strict(),
+]);
+
export const functionDossierSchema = z
.object({
procedure: procedureIdentitySchema.safeExtend({
@@ -428,6 +424,19 @@ export const functionDossierSchema = z
),
native_api: nativeApiBoundarySchema.nullable(),
native_value_flow: nativeValueFlowSchema.nullable(),
+ facet_availability: z
+ .object({
+ pseudocode: facetAvailabilitySchema.exactOptional(),
+ assembly: facetAvailabilitySchema.exactOptional(),
+ comments: facetAvailabilitySchema.exactOptional(),
+ callers: facetAvailabilitySchema.exactOptional(),
+ callees: facetAvailabilitySchema.exactOptional(),
+ references: facetAvailabilitySchema.exactOptional(),
+ strings_names: facetAvailabilitySchema.exactOptional(),
+ cfg: facetAvailabilitySchema.exactOptional(),
+ })
+ .strict()
+ .exactOptional(),
limitations: z.array(z.string()),
})
.strict();
diff --git a/src/domain/javascript/childProcessOptionsRegression.test.ts b/src/domain/javascript/childProcessOptionsRegression.test.ts
new file mode 100644
index 000000000..8c18fe830
--- /dev/null
+++ b/src/domain/javascript/childProcessOptionsRegression.test.ts
@@ -0,0 +1,97 @@
+import { describe, expect, it } from "vitest";
+import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
+
+describe("child-process options detection", () => {
+ it("detects options without an args array", () => {
+ const ir = analyzeJavaScriptSemantics(`
+ import { spawn, execFile, fork } from "node:child_process";
+
+ spawn("tool", {
+ env: { A: "1" },
+ stdio: "pipe"
+ });
+
+ execFile("tool", {
+ env: { A: "1" }
+ });
+
+ fork("worker.js", {
+ env: { A: "1" },
+ stdio: ["pipe", "pipe", "pipe", "ipc"]
+ });
+ `);
+
+ expect(
+ ir.childProcessSpawns.map(
+ ({ method, environmentSupplied, stdioMode, resolution }) => ({
+ method,
+ environmentSupplied,
+ stdioMode,
+ resolution,
+ }),
+ ),
+ ).toEqual([
+ {
+ method: "spawn",
+ environmentSupplied: true,
+ stdioMode: "pipe",
+ resolution: "complete",
+ },
+ {
+ method: "execFile",
+ environmentSupplied: true,
+ stdioMode: "default",
+ resolution: "complete",
+ },
+ {
+ method: "fork",
+ environmentSupplied: true,
+ stdioMode: "array",
+ resolution: "complete",
+ },
+ ]);
+ });
+
+ it("preserves behavior with explicit args arrays", () => {
+ const ir = analyzeJavaScriptSemantics(`
+ import { spawn, execFile, fork } from "node:child_process";
+
+ spawn("tool", [], { env: { A: "1" }, stdio: "pipe" });
+ execFile("tool", [], { env: { A: "1" } });
+ fork("worker.js", [], {
+ env: { A: "1" },
+ stdio: ["pipe", "pipe", "pipe", "ipc"]
+ });
+ `);
+
+ expect(
+ ir.childProcessSpawns.map(({ environmentSupplied, stdioMode }) => ({
+ environmentSupplied,
+ stdioMode,
+ })),
+ ).toEqual([
+ { environmentSupplied: true, stdioMode: "pipe" },
+ { environmentSupplied: true, stdioMode: "default" },
+ { environmentSupplied: true, stdioMode: "array" },
+ ]);
+ });
+
+ it("does not invent options when omitted", () => {
+ const ir = analyzeJavaScriptSemantics(`
+ import { spawn, execFile, fork } from "node:child_process";
+
+ spawn("tool");
+ execFile("tool");
+ fork("worker.js");
+ `);
+
+ expect(
+ ir.childProcessSpawns.every(
+ ({ environmentSupplied, stdioMode }) =>
+ environmentSupplied === false && stdioMode === "default",
+ ),
+ ).toBe(true);
+
+ expect(ir.childProcessSpawns).toHaveLength(3);
+ });
+});
diff --git a/src/domain/javascript/electronStaticAnalysisTypes.ts b/src/domain/javascript/electronStaticAnalysisTypes.ts
index b8d2c03fb..97cd32375 100644
--- a/src/domain/javascript/electronStaticAnalysisTypes.ts
+++ b/src/domain/javascript/electronStaticAnalysisTypes.ts
@@ -219,6 +219,8 @@ export interface ElectronCallPathStep {
export interface ElectronCallPath {
readonly entry_kind: "bridge-member" | "ipc-handler";
readonly entry: string;
+ /** Exact bridge API and property segments; `entry` remains display text. */
+ readonly entry_path?: readonly string[];
readonly channel: string | null;
readonly terminal: string;
readonly resolution: "candidate";
diff --git a/src/domain/javascript/electronStaticAnalysisValues.ts b/src/domain/javascript/electronStaticAnalysisValues.ts
index 2fe9d5858..90ef1f0a5 100644
--- a/src/domain/javascript/electronStaticAnalysisValues.ts
+++ b/src/domain/javascript/electronStaticAnalysisValues.ts
@@ -153,6 +153,13 @@ const collectMembersAt = (
state.unknown += 1;
continue;
}
+ if (
+ t.isObjectProperty(property) &&
+ !property.computed &&
+ !property.shorthand &&
+ name === "__proto__"
+ )
+ continue;
const segments = [...prefix, name];
const path = segments.join(".");
state.members.push(path);
diff --git a/src/domain/javascript/electronWrappedCallPaths.test.ts b/src/domain/javascript/electronWrappedCallPaths.test.ts
index 2740d7915..6d30a10f5 100644
--- a/src/domain/javascript/electronWrappedCallPaths.test.ts
+++ b/src/domain/javascript/electronWrappedCallPaths.test.ts
@@ -357,6 +357,83 @@ it("preserves guarded alternatives in direct exposed object literals", () => {
]),
);
});
+it("preserves bridge member segments and excludes prototype setters", () => {
+ const result = analyzeJavaScriptStaticSource(`
+ const e=require("electron");
+ const write=()=>e.ipcRenderer.send("write");
+ e.contextBridge.exposeInMainWorld("desktop", {
+ "nested.write": write,
+ nested: { write },
+ __proto__: { inherited: write },
+ ["__proto__"]: { own: write },
+ });
+ `);
+ expect(result.electron.call_paths).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ entry: "desktop.nested.write",
+ entry_path: ["desktop", "nested.write"],
+ }),
+ expect.objectContaining({
+ entry: "desktop.nested.write",
+ entry_path: ["desktop", "nested", "write"],
+ }),
+ expect.objectContaining({
+ entry: "desktop.__proto__.own",
+ entry_path: ["desktop", "__proto__", "own"],
+ }),
+ ]),
+ );
+ expect(result.electron.call_paths).not.toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ entry_path: ["desktop", "__proto__", "inherited"],
+ }),
+ ]),
+ );
+});
+it("follows an explicit object-literal prototype lookup in a bridge wrapper", () => {
+ const result = analyzeJavaScriptStaticSource(`
+ const e=require("electron");
+ const prototype={send:()=>e.ipcRenderer.send("through-prototype")};
+ const object={__proto__:prototype};
+ e.contextBridge.exposeInMainWorld("desktop", {
+ action:()=>object.__proto__.send(),
+ });
+ `);
+ expect(result.electron.call_paths).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ entry_path: ["desktop", "action"],
+ channel: "through-prototype",
+ }),
+ ]),
+ );
+});
+it("does not spread a large nested bridge member list into function arguments", () => {
+ const members = Array.from(
+ { length: 125_000 },
+ (_, index) => `m${index}(){}`,
+ ).join(",");
+ const source = `const e=require("electron");e.contextBridge.exposeInMainWorld("desktop",{nested:{${members}}});`;
+ expect(() => analyzeJavaScriptStaticSource(source)).not.toThrow();
+});
+it("reports nested bridge depth truncation while retaining shallow paths", () => {
+ const nested =
+ Array.from({ length: 130 }, (_, index) => `level${index}:{`).join("") +
+ `deep:()=>e.ipcRenderer.send("deep")` +
+ "}".repeat(130);
+ const result = analyzeJavaScriptStaticSource(`
+ const e=require("electron");
+ e.contextBridge.exposeInMainWorld("desktop", {
+ shallow:()=>e.ipcRenderer.send("shallow"), ${nested}
+ });
+ `);
+ expect(result.electron.call_paths).toContainEqual(
+ expect.objectContaining({ entry: "desktop.shallow", channel: "shallow" }),
+ );
+ expect(result.limitations.join(" ")).toContain("depth 128");
+});
it("preserves conditions when a wrapper dispatches through conditional external methods", () => {
const result = analyzeJavaScriptStaticSource(`
const e=require("electron");
diff --git a/src/domain/javascript/electronWrappedCallPaths.ts b/src/domain/javascript/electronWrappedCallPaths.ts
index 048ad30f5..051595eb6 100644
--- a/src/domain/javascript/electronWrappedCallPaths.ts
+++ b/src/domain/javascript/electronWrappedCallPaths.ts
@@ -44,6 +44,7 @@ const callLabel = (node: t.Node): string =>
interface PathContext {
readonly entryNode: t.Node;
readonly entry: string;
+ readonly entryPath?: readonly string[];
readonly entryKind: ElectronCallPath["entry_kind"] | null;
readonly channel: string | null;
readonly steps: readonly ElectronCallPathStep[];
@@ -122,6 +123,37 @@ export const collectElectronWrappedCallPaths = (
};
const hasConditions = (value: SymbolicValue): boolean =>
symbolicVariants(value).some((variant) => variant.conditions?.length);
+ function* bridgeMemberFunctions(
+ value: SymbolicValue,
+ segments: readonly string[] = [],
+ active = new Set(),
+ ): Generator<{
+ readonly name: string;
+ readonly path: readonly string[];
+ readonly fn: SymbolicFunction;
+ }> {
+ if (active.has(value)) return;
+ if (active.size >= 128) {
+ accumulator.unknownFindings += 1;
+ accumulator.electronCallPathLimitReached = true;
+ return;
+ }
+ const nested = new Set(active);
+ nested.add(value);
+ for (const member of [...resolver.members(value)].sort()) {
+ if (--remainingVisits < 0) return;
+ const path = [...segments, member];
+ const name = path.join(".");
+ for (const variant of symbolicVariants(
+ resolver.property(value, member),
+ )) {
+ if (variant.kind === "function") yield { name, path, fn: variant };
+ else if (resolver.members(variant).length > 0)
+ yield* bridgeMemberFunctions(variant, path, nested);
+ }
+ if (remainingVisits < 0) return;
+ }
+ }
const isolatedBridgeCall = (
node: t.CallExpression | t.OptionalCallExpression,
):
@@ -167,6 +199,10 @@ export const collectElectronWrappedCallPaths = (
const path: ElectronCallPath = {
entry_kind: context.entryKind,
entry: context.entry,
+ ...(context.entryKind !== "bridge-member" ||
+ context.entryPath === undefined
+ ? {}
+ : { entry_path: context.entryPath }),
channel,
terminal,
resolution: "candidate",
@@ -405,27 +441,25 @@ export const collectElectronWrappedCallPaths = (
offset: node.start ?? -1,
value: finding,
});
- for (const name of members) {
- const value = resolver.property(object, name);
- for (const fn of symbolicVariants(value))
- if (fn.kind === "function") {
- const memberContext = metadataContext(keyContext, fn);
- if (!memberContext) continue;
- functionBody(
- fn,
- fn.node.params.map(() => symbolicUnknown("public API argument")),
- {
- ...memberContext,
- entryNode: node,
- entryKind: "bridge-member",
- entry: `${key.value}.${name}`,
- channel: null,
- steps: [step(node, "exposure", `${key.value}.${name}`)],
- active: new Set(),
- },
- node,
- );
- }
+ for (const { name, path, fn } of bridgeMemberFunctions(object)) {
+ const memberContext = metadataContext(keyContext, fn);
+ if (!memberContext) continue;
+ const entry = `${key.value}.${name}`;
+ functionBody(
+ fn,
+ fn.node.params.map(() => symbolicUnknown("public API argument")),
+ {
+ ...memberContext,
+ entryNode: node,
+ entryKind: "bridge-member",
+ entry,
+ entryPath: [key.value, ...path],
+ channel: null,
+ steps: [step(node, "exposure", entry)],
+ active: new Set(),
+ },
+ node,
+ );
}
};
const call = (
diff --git a/src/domain/javascript/javascriptFeatureSeed.array_members.test.ts b/src/domain/javascript/javascriptFeatureSeed.array_members.test.ts
index 9cab84239..50f6ceb09 100644
--- a/src/domain/javascript/javascriptFeatureSeed.array_members.test.ts
+++ b/src/domain/javascript/javascriptFeatureSeed.array_members.test.ts
@@ -11,7 +11,10 @@ import {
} from "./javascriptApplicationGraph.fixture.js";
import { findApplicationFeatureSeeds } from "./javascriptFeatureSeed.js";
import { traceApplicationFeature } from "./javascriptFeatureTrace.js";
-import type { ApplicationFeatureSeed } from "./javascriptFeatureTraceSchemas.js";
+import {
+ applicationFeatureSeedSchema,
+ type ApplicationFeatureSeed,
+} from "./javascriptFeatureTraceSchemas.js";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import type { JsonValue } from "../jsonValue.js";
@@ -268,6 +271,32 @@ describe("feature seed selection controls", () => {
});
describe("public feature trace from contextBridge source members", () => {
+ it.each(["openProject", "missingProject"])(
+ "normalizes optional seed defaults before deriving trace identity for %s",
+ (value) => {
+ const input = {
+ sourceEvidenceId,
+ graph: contextBridgeGraph(),
+ nativeEvidence: [],
+ direction: "both" as const,
+ };
+ const omitted = traceApplicationFeature({
+ ...input,
+ seed: { kind: "api", value },
+ });
+ const explicit = traceApplicationFeature({
+ ...input,
+ seed: {
+ kind: "api",
+ value,
+ match: null,
+ case_sensitive: false,
+ },
+ });
+ expect(omitted).toEqual(explicit);
+ },
+ );
+
it("traces one exact member seed through the public feature workflow", () => {
const mode = {
value: "openProject",
@@ -381,12 +410,13 @@ const sourceEvidenceId = `ev_${"b".repeat(64)}`;
const seed = (
kind: ApplicationFeatureSeed["kind"],
value = "openProject",
-): ApplicationFeatureSeed => ({
- kind,
- value,
- match: "exact",
- case_sensitive: true,
-});
+): ApplicationFeatureSeed =>
+ applicationFeatureSeedSchema.parse({
+ kind,
+ value,
+ match: "exact",
+ case_sensitive: true,
+ });
const memberNode = (
kind: ApplicationNode["kind"],
diff --git a/src/domain/javascript/javascriptFeatureSeed.ts b/src/domain/javascript/javascriptFeatureSeed.ts
index 929733f00..d5f06b817 100644
--- a/src/domain/javascript/javascriptFeatureSeed.ts
+++ b/src/domain/javascript/javascriptFeatureSeed.ts
@@ -52,10 +52,28 @@ const seedMatch = (
]
: [];
if (!kindMatches(node, seed.kind)) return [];
+ const requestedEntryPath =
+ seed.kind === "api" && "entry_path" in seed ? seed.entry_path : undefined;
+ if (
+ seed.kind === "api" &&
+ requestedEntryPath !== undefined &&
+ !node.observations.some(({ properties }) => {
+ const entryPath = properties.entry_path;
+ return (
+ Array.isArray(entryPath) &&
+ entryPath.length === requestedEntryPath.length &&
+ entryPath.every(
+ (segment, index) => segment === requestedEntryPath[index],
+ )
+ );
+ })
+ )
+ return [];
const mode = featureSeedMatchMode(seed);
- const query = normalized(seed.value, seed.case_sensitive);
+ const caseSensitive = seed.case_sensitive;
+ const query = normalized(seed.value, caseSensitive);
const match = candidateFields(node, seed.kind).find((candidate) => {
- const value = normalized(candidate.value, seed.case_sensitive);
+ const value = normalized(candidate.value, caseSensitive);
return mode === "exact" ? value === query : value.includes(query);
});
return match === undefined
diff --git a/src/domain/javascript/javascriptFeatureTrace.ts b/src/domain/javascript/javascriptFeatureTrace.ts
index 47333bab6..e4109b85d 100644
--- a/src/domain/javascript/javascriptFeatureTrace.ts
+++ b/src/domain/javascript/javascriptFeatureTrace.ts
@@ -12,6 +12,8 @@ import type {
} from "./javascriptApplicationGraphSchemas.js";
import {
applicationFeatureTraceResultSchema,
+ applicationFeatureSeedSchema,
+ type ApplicationFeatureSeedInput,
type ApplicationFeatureTraceResult,
type TraceApplicationFeatureInput,
} from "./javascriptFeatureTraceSchemas.js";
@@ -30,14 +32,18 @@ export interface ApplicationFeatureTraceProjectionInput {
readonly sourceEvidenceId: string;
readonly graph: JavaScriptApplicationGraph;
readonly nativeEvidence: readonly Evidence[];
- readonly seed: TraceApplicationFeatureInput["seed"];
+ readonly seed: ApplicationFeatureSeedInput;
readonly direction: TraceApplicationFeatureInput["direction"];
}
/** Trace a literal feature through its complete reachable, authority-preserving JAG. */
export const traceApplicationFeature = (
- input: ApplicationFeatureTraceProjectionInput,
+ source: ApplicationFeatureTraceProjectionInput,
): ApplicationFeatureTraceResult => {
+ const input = {
+ ...source,
+ seed: applicationFeatureSeedSchema.parse(source.seed),
+ };
const allSeedMatches = findApplicationFeatureSeeds(
input.graph.nodes,
input.seed,
diff --git a/src/domain/javascript/javascriptFeatureTraceSchemas.ts b/src/domain/javascript/javascriptFeatureTraceSchemas.ts
index ee34bad09..b341ce142 100644
--- a/src/domain/javascript/javascriptFeatureTraceSchemas.ts
+++ b/src/domain/javascript/javascriptFeatureTraceSchemas.ts
@@ -17,33 +17,65 @@ export const featureSeedMatchMode = (seed: {
}): "exact" | "contains" =>
seed.match ?? (seed.kind === "string" ? "contains" : "exact");
-const literalSeedKinds = [
- "route",
- "string",
- "api",
- "channel",
- "module",
- "native-export",
-] as const;
+const apiSeedSchema = z.union([
+ z.strictObject({
+ kind: z.literal("api"),
+ value: boundedTextSchema,
+ match: z.enum(["exact", "contains"]).nullable().default(null),
+ case_sensitive: z.boolean().default(false),
+ }),
+ z.strictObject({
+ kind: z.literal("api"),
+ value: z.literal(""),
+ match: z.literal("exact"),
+ case_sensitive: z.boolean().default(false),
+ }),
+]);
+
+const apiPathSeedSchema = z.union([
+ z.strictObject({
+ kind: z.literal("api"),
+ value: boundedTextSchema,
+ match: z.enum(["exact", "contains"]).nullable().default(null),
+ case_sensitive: z.boolean().default(false),
+ entry_path: z.array(z.string()).min(1),
+ }),
+ z.strictObject({
+ kind: z.literal("api"),
+ value: z.literal(""),
+ match: z.literal("exact"),
+ case_sensitive: z.boolean().default(false),
+ entry_path: z.array(z.string()).min(1),
+ }),
+]);
/**
* Literal starting point for one application feature trace. An empty value,
* such as the channel in `ipcMain.handle("")`, must be an explicit exact
* literal seed: empty containment would select every node.
*/
-const applicationFeatureSeedSchema = z.union([
+export const applicationFeatureSeedSchema = z.union([
z.strictObject({
- kind: z.enum(["node-id", ...literalSeedKinds]),
+ kind: z.enum([
+ "node-id",
+ "route",
+ "string",
+ "channel",
+ "module",
+ "native-export",
+ ]),
value: boundedTextSchema,
match: z.enum(["exact", "contains"]).nullable().default(null),
case_sensitive: z.boolean().default(false),
}),
z.strictObject({
- kind: z.enum(literalSeedKinds),
+ kind: z.enum(["route", "string", "channel", "module", "native-export"]),
value: z.literal(""),
match: z.literal("exact"),
case_sensitive: z.boolean().default(false),
}),
+ apiSeedSchema,
+ apiPathSeedSchema,
]);
/** Check unique native identities before projecting an application trace. */
@@ -156,7 +188,10 @@ export const applicationFeatureTraceResultSchema = z.strictObject({
export type TraceApplicationFeatureInput = z.infer<
typeof traceApplicationFeatureInputSchema
>;
-export type ApplicationFeatureSeed = z.infer<
+export type ApplicationFeatureSeed = z.output<
+ typeof applicationFeatureSeedSchema
+>;
+export type ApplicationFeatureSeedInput = z.input<
typeof applicationFeatureSeedSchema
>;
export type ApplicationFeatureTraceResult = z.infer<
diff --git a/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts b/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts
index baba3395d..be428b06c 100644
--- a/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts
+++ b/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts
@@ -3,6 +3,7 @@ import { z } from "zod";
import {
browserAllowedOriginsSchema,
browserEndpointSchema,
+ recordedBrowserObservationEndpointSchema,
} from "../browserObservation.js";
import {
webPageInspectionSchema,
@@ -424,14 +425,17 @@ const assertRuntimeParameters = (
): void => {
const common = {
target_id: z.string().trim().min(1),
- cdp_endpoint: browserEndpointSchema,
include_script_sources: z.boolean(),
};
if (expected.kind === "browser") {
const parameters = parseAtPath(
(value) =>
z
- .object({ ...common, allowed_origins: browserAllowedOriginsSchema })
+ .object({
+ ...common,
+ cdp_endpoint: recordedBrowserObservationEndpointSchema,
+ allowed_origins: browserAllowedOriginsSchema,
+ })
.passthrough()
.parse(value),
evidence.parameters,
@@ -455,7 +459,11 @@ const assertRuntimeParameters = (
return;
}
const parameters = parseAtPath(
- (value) => z.object(common).passthrough().parse(value),
+ (value) =>
+ z
+ .object({ ...common, cdp_endpoint: browserEndpointSchema })
+ .passthrough()
+ .parse(value),
evidence.parameters,
[...path, "parameters"],
);
diff --git a/src/domain/javascript/javascriptSemanticAnalysis.logical_assignment.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.logical_assignment.test.ts
new file mode 100644
index 000000000..255376cc1
--- /dev/null
+++ b/src/domain/javascript/javascriptSemanticAnalysis.logical_assignment.test.ts
@@ -0,0 +1,96 @@
+import { describe, expect, it } from "vitest";
+import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
+
+const observe = (source: string) => {
+ const ir = analyzeJavaScriptSemantics(source);
+ return {
+ binding: ir.bindings.find((item) => item.name === "x")?.value,
+ returned: ir.callables.find((item) => item.name === "read")?.returnSites[0]
+ ?.value,
+ };
+};
+
+describe("JavaScript semantic analysis: logical assignment", () => {
+ it.each(["||=", "??=", "&&="])(
+ "keeps a parameter uncertain after %s",
+ (operator) => {
+ const { binding, returned } = observe(
+ `function read(x) { x ${operator} 2; return x; }`,
+ );
+ expect(binding?.status).toBe("unknown");
+ expect(returned?.status).toBe("unknown");
+ },
+ );
+
+ it.each(["||=", "??=", "&&="])(
+ "keeps a destructured parameter uncertain after %s",
+ (operator) => {
+ const { binding, returned } = observe(
+ `function read({ x }) { x ${operator} 2; return x; }`,
+ );
+ expect(binding?.status).toBe("unknown");
+ expect(returned?.status).toBe("unknown");
+ },
+ );
+
+ it.each(["||=", "??=", "&&="])(
+ "keeps a catch binding uncertain after %s",
+ (operator) => {
+ const { binding, returned } = observe(
+ `function read() { try { run(); } catch (x) { x ${operator} 2; return x; } }`,
+ );
+ expect(binding?.status).toBe("unknown");
+ expect(returned?.status).toBe("unknown");
+ },
+ );
+
+ it.each([
+ ["let", "||="],
+ ["let", "??="],
+ ["var", "||="],
+ ["var", "??="],
+ ])(
+ "keeps an uninitialized %s local exact after %s",
+ (declaration, operator) => {
+ const { binding, returned } = observe(
+ `function read() { ${declaration} x; x ${operator} 2; return x; }`,
+ );
+ expect(binding).toEqual({ status: "literal", value: 2 });
+ expect(returned).toEqual({ status: "literal", value: 2 });
+ },
+ );
+
+ it("keeps an unconditional parameter assignment exact", () => {
+ const { binding, returned } = observe(
+ "function read(x) { x = 2; return x; }",
+ );
+ expect(binding).toEqual({ status: "literal", value: 2 });
+ expect(returned).toEqual({ status: "literal", value: 2 });
+ });
+
+ it("keeps an unchanged parameter unknown", () => {
+ expect(observe("function read(x) { return x; }").returned?.status).toBe(
+ "unknown",
+ );
+ });
+
+ it("reproduces the issue #2646 inputs without an exact literal", () => {
+ for (const operator of ["||=", "??="]) {
+ const { binding, returned } = observe(
+ `function read(x) { x ${operator} 2; return x; }`,
+ );
+ expect(binding).not.toEqual({ status: "literal", value: 2 });
+ expect(returned).not.toEqual({ status: "literal", value: 2 });
+ }
+ const readOr = (x: number) => {
+ x ||= 2;
+ return x;
+ };
+ const readNullish = (x: number) => {
+ x ??= 2;
+ return x;
+ };
+ expect(readOr(5)).toBe(5);
+ expect(readNullish(5)).toBe(5);
+ });
+});
diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts
index f6c6db25d..119fbdd45 100644
--- a/src/domain/javascript/javascriptSemanticAnalysis.ts
+++ b/src/domain/javascript/javascriptSemanticAnalysis.ts
@@ -247,19 +247,24 @@ export function* classifyParsedJavaScriptRpcClientRootsSteps(
return roots;
}
-/** Receiver identity facts for the overloaded `.open` syntax only. */
+/**
+ * Receiver identity facts for the overloaded `.open` syntax, plus
+ * `configuration` for `.get` reads of a `getConfiguration(...)` result.
+ */
export type JavaScriptOpenReceiverFact =
| "window"
| "indexed-db"
| "cache-storage"
| "local"
| "local-indexed-db"
- | "local-cache-storage";
+ | "local-cache-storage"
+ | "configuration";
/**
* Resolve only the lexical receiver identity needed to distinguish browser
- * `window.open` from XHR and global storage `.open` calls. This deliberately
- * builds definitions without running semantic value or provenance analysis.
+ * `window.open` from XHR and global storage `.open` calls, and configuration
+ * `.get` reads from HTTP client calls. This deliberately builds definitions
+ * without running semantic value or provenance analysis.
*/
export const classifyParsedJavaScriptOpenReceivers = (
file: ParsedJavaScriptSource,
@@ -278,14 +283,19 @@ export function* classifyParsedJavaScriptOpenReceiversSteps(
if (
(!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) ||
(!t.isMemberExpression(node.callee) &&
- !t.isOptionalMemberExpression(node.callee)) ||
- semanticStaticPropertyKey(
- node.callee.property,
- node.callee.computed,
- ) !== "open" ||
- !t.isIdentifier(node.callee.object)
+ !t.isOptionalMemberExpression(node.callee))
)
return;
+ const method = semanticStaticPropertyKey(
+ node.callee.property,
+ node.callee.computed,
+ );
+ if (method === "get") {
+ if (isConfigurationReceiver(state, node.callee.object))
+ facts.set(node.start ?? -1, "configuration");
+ return;
+ }
+ if (method !== "open" || !t.isIdentifier(node.callee.object)) return;
const receiver = node.callee.object.name;
const binding = resolveSemanticBindingState(state, node, receiver);
if (binding === undefined) {
@@ -351,6 +361,82 @@ export function* classifyParsedJavaScriptOpenReceiversSteps(
return facts;
}
+/**
+ * Whether a call spells VS Code's `workspace.getConfiguration(section)`, whose
+ * result `.get` reads setting keys rather than request paths. The `workspace`
+ * member is required: a bare or differently scoped `getConfiguration` may be
+ * an application's own HTTP client factory. Minifiers keep property names, so
+ * bundled `x.workspace.getConfiguration` still matches.
+ */
+const isConfigurationAccessorCall = (
+ state: JavaScriptSemanticAnalysisState,
+ node: t.Node,
+): boolean => {
+ if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node))
+ return false;
+ const callee = node.callee;
+ if (
+ (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee)) ||
+ semanticStaticPropertyKey(callee.property, callee.computed) !==
+ "getConfiguration"
+ )
+ return false;
+ const host = callee.object;
+ const matchesSpelling = t.isIdentifier(host)
+ ? host.name === "workspace"
+ : (t.isMemberExpression(host) || t.isOptionalMemberExpression(host)) &&
+ semanticStaticPropertyKey(host.property, host.computed) === "workspace";
+ if (!matchesSpelling) return false;
+ const root = t.isIdentifier(host)
+ ? host
+ : t.isMemberExpression(host) || t.isOptionalMemberExpression(host)
+ ? host.object
+ : undefined;
+ if (!t.isIdentifier(root)) return false;
+ const binding = resolveSemanticBindingState(state, root, root.name);
+ if (binding === undefined)
+ return !semanticResolutionBlocked(state, root, root.name);
+ const initializer = semanticStableInitializer(binding, state);
+ const origins = [
+ ...binding.directOrigins,
+ ...(initializer === undefined
+ ? []
+ : [semanticRequireOrigin(initializer.node, state)?.origin]),
+ ];
+ return origins.some(
+ (origin) =>
+ origin?.specifier === "vscode" &&
+ (t.isIdentifier(host)
+ ? origin.importedPath.at(-1) === "workspace" ||
+ initializer?.projection.at(-1) === "workspace"
+ : origin.importedPath.length === 0 ||
+ (origin.importedPath.length === 1 &&
+ (origin.importedPath[0] === "*" ||
+ origin.importedPath[0] === "default"))),
+ );
+};
+
+/**
+ * Whether a `.get` receiver is a configuration accessor result, either inline
+ * or through a binding whose only visible write is one unconditional
+ * initializer. Minifiers emit `let` for such bindings, so declaration kind
+ * alone does not decide.
+ */
+const isConfigurationReceiver = (
+ state: JavaScriptSemanticAnalysisState,
+ receiver: t.Node,
+): boolean => {
+ if (isConfigurationAccessorCall(state, receiver)) return true;
+ if (!t.isIdentifier(receiver)) return false;
+ const binding = resolveSemanticBindingState(state, receiver, receiver.name);
+ const initializer = semanticStableInitializer(binding, state);
+ return (
+ initializer !== undefined &&
+ initializer.projection.length === 0 &&
+ isConfigurationAccessorCall(state, initializer.node)
+ );
+};
+
const openGlobalFact = (
name: string,
): JavaScriptOpenReceiverFact | undefined => {
@@ -833,6 +919,21 @@ const bindImportEquals = (
});
};
+// A parameter or catch binding starts with a caller-supplied value that a
+// conditional logical assignment can retain, so its RHS is not exact.
+const hasIncomingValue = (
+ identifier: t.Identifier,
+ scope: JavaScriptSemanticScopeState,
+ state: JavaScriptSemanticAnalysisState,
+): boolean => {
+ const kind = resolveSemanticBindingFromScope(
+ scope,
+ identifier.name,
+ state,
+ )?.kind;
+ return kind === "parameter" || kind === "catch";
+};
+
const bindAssignment = (
node: t.Node,
scope: JavaScriptSemanticScopeState,
@@ -843,8 +944,8 @@ const bindAssignment = (
addAssignment(
node.left,
node.operator === "=" ||
- node.operator === "||=" ||
- node.operator === "??="
+ ((node.operator === "||=" || node.operator === "??=") &&
+ !hasIncomingValue(node.left, scope, state))
? node.right
: node,
scope,
diff --git a/src/domain/javascript/javascriptSemanticChildProcesses.ts b/src/domain/javascript/javascriptSemanticChildProcesses.ts
index fe7b5e802..1cffb5164 100644
--- a/src/domain/javascript/javascriptSemanticChildProcesses.ts
+++ b/src/domain/javascript/javascriptSemanticChildProcesses.ts
@@ -324,17 +324,25 @@ const resolveChildCandidates = (
? []
: (spawnsByBinding.get(binding.bindingId) ?? []);
};
-
const spawnOptions = (candidate: SpawnCandidate): t.ObjectExpression | null => {
- const index = candidate.method === "exec" ? 1 : 2;
- const value = candidate.node.arguments[index];
+ const args = candidate.node.arguments;
+
+ const value =
+ candidate.method === "exec"
+ ? args[1]
+ : t.isObjectExpression(args[1])
+ ? args[1]
+ : args[2];
+
return t.isObjectExpression(value) ? value : null;
};
-
const spawnArgvCount = (candidate: SpawnCandidate): number | null => {
if (candidate.method === "exec") return null;
const args = candidate.node.arguments[1];
- return t.isArrayExpression(args) ? args.elements.length : null;
+ return t.isArrayExpression(args) &&
+ !args.elements.some((element) => t.isSpreadElement(element))
+ ? args.elements.length
+ : null;
};
const objectHasProperty = (
diff --git a/src/domain/javascript/javascriptSemanticDataEffectHelpers.ts b/src/domain/javascript/javascriptSemanticDataEffectHelpers.ts
index 5485fcde9..0f7a6d942 100644
--- a/src/domain/javascript/javascriptSemanticDataEffectHelpers.ts
+++ b/src/domain/javascript/javascriptSemanticDataEffectHelpers.ts
@@ -275,12 +275,3 @@ export const dataEffectMemberCallee = (
t.isMemberExpression(node.callee) || t.isOptionalMemberExpression(node.callee)
? node.callee
: null;
-
-/** Read a parent member's object when present. */
-export const dataEffectMemberObject = (
- node: t.Node | null,
-): t.Expression | t.Super | null =>
- node !== null &&
- (t.isMemberExpression(node) || t.isOptionalMemberExpression(node))
- ? node.object
- : null;
diff --git a/src/domain/javascript/javascriptSemanticDataEffects.ts b/src/domain/javascript/javascriptSemanticDataEffects.ts
index 445cd2e5e..607843aab 100644
--- a/src/domain/javascript/javascriptSemanticDataEffects.ts
+++ b/src/domain/javascript/javascriptSemanticDataEffects.ts
@@ -24,7 +24,6 @@ import {
dataEffectArgumentBindingId as argumentBindingId,
dataEffectLiteralString as literalString,
dataEffectMemberCallee as memberCallee,
- dataEffectMemberObject as memberObject,
dataEffectPrimitive as primitiveValue,
isSemanticProcessObject as isProcessObject,
outerDataEffectBinding as outerAssignedBinding,
@@ -83,8 +82,8 @@ const collectConfigurations = (
context: TraversalContext,
): ConfigurationCandidate[] => {
const output: ConfigurationCandidate[] = [];
- traverseWithContext(program, context, (node, parent) => {
- const details = configurationDetails(node, parent, context);
+ traverseWithContext(program, context, (node) => {
+ const details = configurationDetails(node, context);
if (details === null) return;
const operation: JavaScriptSemanticConfigurationOperation = {
configId: `config:${details.kind}:${String(node.start ?? -1)}:${String(node.end ?? -1)}`,
@@ -104,17 +103,13 @@ const collectConfigurations = (
const configurationDetails = (
node: t.Node,
- parent: t.Node | null,
context: TraversalContext,
): {
readonly kind: "environment" | "argv" | "file";
readonly key: string | null;
readonly resolution: "complete" | "partial";
} | null => {
- if (
- (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) &&
- !(memberObject(parent) === node)
- ) {
+ if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
const processMember = processConfigurationMember(node, context.state);
if (processMember !== null) return processMember;
}
diff --git a/src/domain/javascript/javascriptStaticAnalysisCalls.ts b/src/domain/javascript/javascriptStaticAnalysisCalls.ts
index 148f7fb7e..b098b3c26 100644
--- a/src/domain/javascript/javascriptStaticAnalysisCalls.ts
+++ b/src/domain/javascript/javascriptStaticAnalysisCalls.ts
@@ -429,7 +429,12 @@ export const addSourceMapDirectives = (
end === null
)
continue;
- const commentSource = source.slice(start, end);
+ // The parser owns comment boundaries. Exclude only the closing block
+ // delimiter; an asterisk elsewhere remains part of the declared URL.
+ const commentSource = source.slice(
+ start,
+ comment.type === "CommentBlock" ? end - 2 : end,
+ );
if (
!(commentSource.startsWith("//") || commentSource.startsWith("/*")) ||
!(commentSource[2] === "#" || commentSource[2] === "@")
@@ -449,9 +454,7 @@ export const addSourceMapDirectives = (
const urlOffset = commentSource.slice(equalsStart + 1).search(/\S/u);
if (urlOffset === -1) continue;
const declaredStart = equalsStart + 1 + urlOffset;
- const terminator = commentSource
- .slice(declaredStart)
- .search(commentSource.startsWith("//") ? /\s/u : /[\s*]/u);
+ const terminator = commentSource.slice(declaredStart).search(/\s/u);
const declaredEnd =
terminator === -1 ? commentSource.length : declaredStart + terminator;
if (declaredEnd === declaredStart) continue;
diff --git a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts
index f2045a1eb..71628bc8c 100644
--- a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts
+++ b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts
@@ -17,6 +17,7 @@ import {
semanticStaticPropertyName,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
+import type { JavaScriptOpenReceiverFact } from "./javascriptSemanticAnalysis.js";
import {
compareUnicodeCodePoints,
compositeKey,
@@ -470,13 +471,7 @@ const xhrOpenUrl = (
name: string,
methodNode: t.Node | null | undefined,
urlNode: t.Node | null | undefined,
- receiverFact?:
- | "window"
- | "indexed-db"
- | "cache-storage"
- | "local"
- | "local-indexed-db"
- | "local-cache-storage",
+ receiverFact?: JavaScriptOpenReceiverFact,
): string | undefined => {
const url = stringValue(urlNode);
if (url === undefined) return undefined;
@@ -555,13 +550,7 @@ export const endpointArgument = (
| t.ArgumentPlaceholder
)[],
callee: t.Node,
- receiverFact?:
- | "window"
- | "indexed-db"
- | "cache-storage"
- | "local"
- | "local-indexed-db"
- | "local-cache-storage",
+ receiverFact?: JavaScriptOpenReceiverFact,
): string | undefined => {
if (receiverFact === "window") return undefined;
if (name === "fetch" || name.endsWith(".fetch") || name === "WebSocket")
@@ -575,10 +564,12 @@ export const endpointArgument = (
const value = stringValue(args[0]);
// Only get and delete also belong to the keyed-collection APIs, whose
// keys are header, field, and parameter names rather than URLs or paths.
+ // Configuration `get` reads setting keys.
return value !== undefined &&
(method === "get" || method === "delete") &&
!isRequestUrlLiteral(value) &&
- isKeyedCollectionReceiver(callee)
+ (isKeyedCollectionReceiver(callee) ||
+ (method === "get" && receiverFact === "configuration"))
? undefined
: value;
}
diff --git a/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts b/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts
index f5664f73c..6779d35bd 100644
--- a/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts
+++ b/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts
@@ -72,10 +72,10 @@ describe("source map directives", () => {
it("preserves Unicode whitespace and URL terminator boundaries", () => {
const source =
"ğ/*@\u00a0sourceMappingURL\u2003=\ufeffapp.js.map* ignored */";
- const declaredEnd = source.indexOf("* ignored");
+ const declaredEnd = source.indexOf(" ignored");
expect(analyzeJavaScriptStaticSource(source).source_map_urls).toEqual([
{
- declared_url: "app.js.map",
+ declared_url: "app.js.map*",
location: {
start: { line: 1, column: 1 },
end: { line: 1, column: declaredEnd },
@@ -84,6 +84,29 @@ describe("source map directives", () => {
]);
});
+ it.each([
+ ["/*#", "app*part.js.map", " */"],
+ ["/*@", "*part.js.map", "*/"],
+ ["/*#", "app.js.map*", "*/"],
+ ["/*#", "**part.js.map", " */"],
+ ["/*#", "app.js.map", "*/"],
+ ["//#", "app*part.js.map", ""],
+ ])("preserves the URL and range in %s %s%s", (marker, url, suffix) => {
+ const directive = marker + " sourceMappingURL=" + url;
+ const result = analyzeJavaScriptStaticSource(
+ "export const value = 1;\n" + directive + suffix,
+ );
+ expect(result.source_map_urls).toEqual([
+ {
+ declared_url: url,
+ location: {
+ start: { line: 2, column: 0 },
+ end: { line: 2, column: directive.length },
+ },
+ },
+ ]);
+ });
+
it("reports nothing when no directive is present", () => {
expect(sourceMapUrls("const value = 1;")).toEqual([]);
});
diff --git a/src/domain/javascript/javascriptStaticAnalysisState.ts b/src/domain/javascript/javascriptStaticAnalysisState.ts
index 3c93d2fa3..5173362fa 100644
--- a/src/domain/javascript/javascriptStaticAnalysisState.ts
+++ b/src/domain/javascript/javascriptStaticAnalysisState.ts
@@ -1,4 +1,5 @@
import type * as t from "@babel/types";
+import type { JavaScriptOpenReceiverFact } from "./javascriptSemanticAnalysis.js";
import type { JavaScriptRpcScan } from "./javascriptRpcEndpoints.js";
import type {
@@ -64,15 +65,7 @@ export interface JavaScriptFindingContext {
readonly rpcScan: JavaScriptRpcScan;
readonly source: string;
readonly accumulator: JavaScriptAnalysisAccumulator;
- readonly openReceiverFacts?: ReadonlyMap<
- number,
- | "window"
- | "indexed-db"
- | "cache-storage"
- | "local"
- | "local-indexed-db"
- | "local-cache-storage"
- >;
+ readonly openReceiverFacts?: ReadonlyMap;
/** Proven Electron export path segments by root offset. */
readonly electronBindings?: ReadonlyMap;
}
diff --git a/src/domain/javascript/javascriptSymbolicBindings.ts b/src/domain/javascript/javascriptSymbolicBindings.ts
index b1f37269f..62c70f953 100644
--- a/src/domain/javascript/javascriptSymbolicBindings.ts
+++ b/src/domain/javascript/javascriptSymbolicBindings.ts
@@ -47,6 +47,14 @@ interface SymbolicReturnFlow {
readonly fallsThrough: boolean;
}
const emptyEnvironment: SymbolicEnvironment = { values: new Map() };
+const isObjectPrototypeSetter = (
+ property: t.ObjectMethod | t.ObjectProperty,
+ name: string,
+): boolean =>
+ t.isObjectProperty(property) &&
+ !property.computed &&
+ !property.shorthand &&
+ name === "__proto__";
/** Demand-driven substitutions for local bindings, closures and literal module tables. */
export class JavaScriptSymbolicBindings {
@@ -243,15 +251,30 @@ export class JavaScriptSymbolicBindings {
index = null;
break;
}
+ if (isObjectPrototypeSetter(property, name)) continue;
properties.set(name, property);
}
this.#literalPropertyIndex.set(value.node, index);
}
if (index !== null) {
const property = index.get(key);
- return property === undefined
- ? symbolicUnknown("missing property")
- : this.#objectPropertyValue(property, value.environment);
+ if (property !== undefined)
+ return this.#objectPropertyValue(property, value.environment);
+ if (key === "__proto__") {
+ const prototypeSetter = [...value.node.properties]
+ .reverse()
+ .filter(
+ (candidate): candidate is t.ObjectMethod | t.ObjectProperty =>
+ !t.isSpreadElement(candidate),
+ )
+ .find((candidate) => isObjectPrototypeSetter(candidate, key));
+ if (prototypeSetter !== undefined)
+ return this.#objectPropertyValue(
+ prototypeSetter,
+ value.environment,
+ );
+ }
+ return symbolicUnknown("missing property");
}
for (const p of [...value.node.properties].reverse()) {
if (t.isSpreadElement(p)) {
@@ -314,7 +337,8 @@ export class JavaScriptSymbolicBindings {
t.isSpreadElement(p)
? this.members(this.evaluate(p.argument, value.environment), active)
: [semanticStaticPropertyKey(p.key, p.computed)].filter(
- (k): k is string => k !== null,
+ (k): k is string =>
+ k !== null && !isObjectPrototypeSetter(p, k),
),
),
),
diff --git a/src/domain/managed/managedNativeVerificationSchemas.ts b/src/domain/managed/managedNativeVerificationSchemas.ts
index 8e543bdab..16afe4048 100644
--- a/src/domain/managed/managedNativeVerificationSchemas.ts
+++ b/src/domain/managed/managedNativeVerificationSchemas.ts
@@ -92,7 +92,10 @@ export type PinvokeVerification = z.infer;
export const managedNativeVerificationInputSchema = z
.strictObject({
managed_boundaries: evidenceSchema,
- native_observations: z.array(evidenceSchema).min(1),
+ native_observations: z
+ .array(evidenceSchema)
+ .min(1)
+ .describe("Native function or export Evidence records"),
})
.superRefine((input, context) => {
const ids = new Set();
diff --git a/src/domain/native/addressCoordinate.ts b/src/domain/native/addressCoordinate.ts
new file mode 100644
index 000000000..d43b1b4f1
--- /dev/null
+++ b/src/domain/native/addressCoordinate.ts
@@ -0,0 +1,11 @@
+/** Parse canonical native offsets without discarding their address space. */
+export const addressCoordinate = (
+ value: string,
+): { readonly space: string | null; readonly offset: bigint } | null => {
+ const separator = value.lastIndexOf(":0x");
+ if (separator === 0) return null;
+ const space = separator < 0 ? null : value.slice(0, separator);
+ const offset = separator < 0 ? value : value.slice(separator + 1);
+ if (!/^0x[0-9a-f]+$/u.test(offset)) return null;
+ return { space, offset: BigInt(offset) };
+};
diff --git a/src/domain/native/bytePattern.test.ts b/src/domain/native/bytePattern.test.ts
new file mode 100644
index 000000000..cc3b6dd9a
--- /dev/null
+++ b/src/domain/native/bytePattern.test.ts
@@ -0,0 +1,33 @@
+import { describe, expect, it } from "vitest";
+import { parseBytePattern } from "./bytePattern.js";
+
+describe("masked byte patterns", () => {
+ it("preserves whole-byte wildcards and combines explicit bit masks", () => {
+ const parsed = parseBytePattern("48 8b\t??89", "ff f0 ff 0f");
+ if (!parsed.ok) throw parsed.error;
+ expect(parsed.value).toEqual({
+ bytes: [0x48, 0x8b, 0, 0x89],
+ masks: [255, 240, 0, 15],
+ });
+ const exact = parseBytePattern("Aa\u00a000");
+ if (!exact.ok) throw exact.error;
+ expect(exact.value).toEqual({ bytes: [170, 0], masks: [255, 255] });
+ });
+
+ it.each([
+ ["", undefined, "pattern"],
+ ["4", undefined, "pattern"],
+ ["4?", undefined, "pattern"],
+ ["gg", undefined, "pattern"],
+ ["?? ??", undefined, "pattern"],
+ ["ff", "00", "pattern"],
+ ["ffff", "ff", "mask"],
+ ["ff", "??", "mask"],
+ ])("rejects invalid pattern %s with typed issues", (pattern, mask, field) => {
+ const parsed = parseBytePattern(pattern, mask);
+ if (parsed.ok) throw new Error("Expected a rejected pattern");
+ expect(parsed.error.issues).toMatchObject([
+ { path: [field], reason: "invalid_value" },
+ ]);
+ });
+});
diff --git a/src/domain/native/bytePattern.ts b/src/domain/native/bytePattern.ts
new file mode 100644
index 000000000..add5bb7ef
--- /dev/null
+++ b/src/domain/native/bytePattern.ts
@@ -0,0 +1,125 @@
+import { z } from "zod";
+import { addressCoordinate } from "./addressCoordinate.js";
+import { AnalysisInputError } from "../analysisErrorCore.js";
+import { err, ok, type Result } from "../result.js";
+
+/** Byte pairs, with whole-byte wildcards; explicit masks can select individual bits. */
+export const bytePatternFields = {
+ pattern: z
+ .string()
+ .regex(/^([0-9a-fA-F]{2}|\?\?)(\s*([0-9a-fA-F]{2}|\?\?))*$/u),
+ mask: z
+ .string()
+ .regex(/^[0-9a-fA-F]{2}(\s*[0-9a-fA-F]{2})*$/u)
+ .optional(),
+};
+
+/** Parse once at the input boundary, retaining typed correction issues. */
+export const parseBytePattern = (
+ pattern: string,
+ mask?: string,
+): Result<{ bytes: number[]; masks: number[] }, AnalysisInputError> => {
+ const invalid = (field: string, message: string) =>
+ err(
+ new AnalysisInputError("search_bytes", undefined, [
+ { path: [field], reason: "invalid_value", message },
+ ]),
+ );
+ if (!bytePatternFields.pattern.safeParse(pattern).success)
+ return invalid("pattern", "Use hexadecimal byte pairs or ?? wildcards");
+ if (mask !== undefined && !bytePatternFields.mask.safeParse(mask).success)
+ return invalid("mask", "Use hexadecimal mask byte pairs");
+ const pairs = pattern.replace(/\s/gu, "").match(/../gu) ?? [];
+ const maskPairs = mask?.replace(/\s/gu, "").match(/../gu);
+ if (maskPairs && maskPairs.length !== pairs.length)
+ return invalid("mask", "Mask length must equal pattern length");
+ const bytes = pairs.map((pair) =>
+ pair === "??" ? 0 : Number.parseInt(pair, 16),
+ );
+ const masks = pairs.map((pair, index) =>
+ pair === "??" ? 0 : Number.parseInt(maskPairs?.[index] ?? "ff", 16),
+ );
+ if (!masks.some((value) => value !== 0))
+ return invalid("pattern", "Pattern must constrain at least one bit");
+ return ok({ bytes, masks });
+};
+
+/** Shared semantic validation for public and provider inputs. */
+export const validateBytePattern = (
+ value: { pattern: string; mask?: string | undefined },
+ context: z.RefinementCtx,
+): void => {
+ const parsed = parseBytePattern(value.pattern, value.mask);
+ if (!parsed.ok)
+ for (const issue of parsed.error.issues)
+ context.addIssue({
+ code: "custom",
+ path: [...issue.path],
+ message: issue.message ?? "Invalid byte pattern",
+ });
+};
+
+const coverageBlock = z
+ .strictObject({
+ segment: z.string(),
+ start: z.string(),
+ end: z.string(),
+ bytes: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
+ })
+ .superRefine((value, context) => {
+ const start = addressCoordinate(value.start);
+ const end = addressCoordinate(value.end);
+ if (
+ start === null ||
+ end === null ||
+ start.space !== end.space ||
+ start.offset > end.offset ||
+ end.offset - start.offset + 1n !== BigInt(value.bytes)
+ )
+ context.addIssue({
+ code: "custom",
+ message:
+ "Byte coverage must equal its inclusive address range in one space",
+ });
+ });
+
+/** Inclusive page coverage; matches never span memory blocks. */
+export const byteSearchResultSchema = z
+ .strictObject({
+ matches: z
+ .array(
+ z.strictObject({
+ address: z.string(),
+ segment: z.string(),
+ file_offset: z
+ .number()
+ .int()
+ .nonnegative()
+ .max(Number.MAX_SAFE_INTEGER)
+ .nullable(),
+ procedure: z
+ .strictObject({ address: z.string(), name: z.string() })
+ .nullable(),
+ }),
+ )
+ .max(1000),
+ searched: z.strictObject({
+ blocks: z.array(coverageBlock),
+ bytes: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
+ skipped_uninitialized: z.array(coverageBlock),
+ skipped_not_loaded: z.array(coverageBlock),
+ }),
+ complete: z.boolean(),
+ next_cursor: z.string().min(1).nullable(),
+ })
+ .superRefine((value, context) => {
+ if (
+ value.complete !== (value.next_cursor === null) ||
+ value.searched.bytes !==
+ value.searched.blocks.reduce((sum, block) => sum + block.bytes, 0)
+ )
+ context.addIssue({
+ code: "custom",
+ message: "Byte search coverage and continuation disagree",
+ });
+ });
diff --git a/src/domain/native/objcSwiftMetadata.ts b/src/domain/native/objcSwiftMetadata.ts
index 5778e5dd7..a360c3202 100644
--- a/src/domain/native/objcSwiftMetadata.ts
+++ b/src/domain/native/objcSwiftMetadata.ts
@@ -1,4 +1,5 @@
import { z } from "zod";
+import { providerSchema } from "../evidence.js";
import {
compareUnicodeCodePoints,
@@ -115,6 +116,7 @@ export const nativeMetadataEvidenceSchema = z.strictObject({
"decompiler",
]),
description: z.string().min(1),
+ provider: providerSchema.optional(),
location: nativeMetadataLocationSchema,
artifact_path: z.string().min(1).nullable().default(null),
artifact_sha256: z
diff --git a/src/domain/native/recordedCrash.test.ts b/src/domain/native/recordedCrash.test.ts
index 7a050d417..18414128b 100644
--- a/src/domain/native/recordedCrash.test.ts
+++ b/src/domain/native/recordedCrash.test.ts
@@ -1,7 +1,8 @@
-import { expect, it } from "vitest";
+import { describe, expect, it } from "vitest";
import {
inspectRecordedCrashInputSchema,
recordedCrashSchema,
+ type RecordedCrash,
} from "./recordedCrash.js";
import {
recordedCrashFixture,
@@ -282,3 +283,95 @@ it("keeps absent map flags unknown and validates reported permission bits", () =
}).success,
).toBe(false);
});
+
+describe("recorded AArch64 core profile", () => {
+ it("accepts the complete AArch64 register window in ABI order", () => {
+ const value = recordedCrashSchema.parse(
+ recordedCrashFixture("/selected/aarch64.core", "aarch64-little-endian"),
+ );
+ expect(value.architecture).toBe("aarch64-little-endian");
+ const registers = value.threads[0]?.registers ?? [];
+ expect(registers.map(({ name }) => name)).toEqual([
+ ...Array.from({ length: 31 }, (_, index) => `x${String(index)}`),
+ "sp",
+ "pc",
+ "pstate",
+ ]);
+ expect(registers[14]?.value).toBe("0x1122334455667788");
+ expect(registers.every(({ location }) => location.bytes === "0x8")).toBe(
+ true,
+ );
+ });
+
+ it("keeps the AArch64 note ranges inside their recorded segment", () => {
+ const value = recordedCrashSchema.parse(
+ recordedCrashSignalFixture(undefined, "aarch64-little-endian"),
+ );
+ const note = value.notes[0];
+ const segment = value.segments[0];
+ if (note === undefined || segment === undefined)
+ throw new Error("Expected recorded notes");
+ expect(
+ BigInt(note.location.offset) + BigInt(note.location.bytes),
+ ).toBeLessThanOrEqual(BigInt(segment.offset) + BigInt(segment.file_size));
+ expect(value.signals[0]?.fault_address).toBe("0x1122334455667788");
+ });
+
+ it.each([
+ [
+ "an amd64 register name inside an AArch64 window",
+ (v: RecordedCrash) => ({
+ ...v,
+ threads: v.threads.map((thread) => ({
+ ...thread,
+ registers: thread.registers.map((register, index) =>
+ index === 0 ? { ...register, name: "rax" } : register,
+ ),
+ })),
+ }),
+ ],
+ [
+ "the amd64 register count",
+ (v: RecordedCrash) => ({
+ ...v,
+ threads: v.threads.map((thread) => ({
+ ...thread,
+ registers: thread.registers.slice(0, 27),
+ })),
+ }),
+ ],
+ [
+ "a 4-byte AArch64 register source",
+ (v: RecordedCrash) => ({
+ ...v,
+ threads: v.threads.map((thread) => ({
+ ...thread,
+ registers: thread.registers.map((register, index) =>
+ index === 0
+ ? {
+ ...register,
+ location: { ...register.location, bytes: "0x4" },
+ }
+ : register,
+ ),
+ })),
+ }),
+ ],
+ ] as const)("rejects %s", (_name, change) => {
+ const value = recordedCrashFixture(
+ "/selected/aarch64.core",
+ "aarch64-little-endian",
+ );
+ expect(recordedCrashSchema.safeParse(change(value)).success).toBe(false);
+ });
+
+ it("rejects an architecture outside the interpreted profiles", () => {
+ const value = recordedCrashFixture();
+ expect(
+ recordedCrashSchema.safeParse({
+ ...value,
+ architecture: "arm-little-endian",
+ }).success,
+ ).toBe(false);
+ });
+});
diff --git a/src/domain/native/recordedCrash.ts b/src/domain/native/recordedCrash.ts
index d4b533838..8bd04cac1 100644
--- a/src/domain/native/recordedCrash.ts
+++ b/src/domain/native/recordedCrash.ts
@@ -1,5 +1,6 @@
import { z } from "zod";
+import type { BinaryArchitecture } from "../binaryTargetTypes.js";
import { localPathStringSchema } from "../localPath.js";
import { CANONICAL_BASE64_PATTERN } from "../stringPatterns.js";
import { recordedNoteCoverageIssues } from "./recordedCrashNoteCoverage.js";
@@ -18,7 +19,7 @@ const diagnostics = z.strictObject({
const LINUX_CORE_OWNER_BASE64 = "Q09SRQA=";
/** Complete register identities in the supported Linux amd64 PRSTATUS ABI order. */
-export const RECORDED_LINUX_AMD64_REGISTERS: readonly string[] = [
+const RECORDED_LINUX_AMD64_REGISTERS: readonly string[] = [
"r15",
"r14",
"r13",
@@ -47,7 +48,95 @@ export const RECORDED_LINUX_AMD64_REGISTERS: readonly string[] = [
"fs",
"gs",
];
-const supportedRegisters = new Set(RECORDED_LINUX_AMD64_REGISTERS);
+const RECORDED_LINUX_AARCH64_REGISTERS: readonly string[] = [
+ "x0",
+ "x1",
+ "x2",
+ "x3",
+ "x4",
+ "x5",
+ "x6",
+ "x7",
+ "x8",
+ "x9",
+ "x10",
+ "x11",
+ "x12",
+ "x13",
+ "x14",
+ "x15",
+ "x16",
+ "x17",
+ "x18",
+ "x19",
+ "x20",
+ "x21",
+ "x22",
+ "x23",
+ "x24",
+ "x25",
+ "x26",
+ "x27",
+ "x28",
+ "x29",
+ "x30",
+ "sp",
+ "pc",
+ "pstate",
+];
+
+/** Recorded-core architectures whose Linux note ABI is interpreted. */
+export const RECORDED_LINUX_CORE_ARCHITECTURES = [
+ "x86_64-little-endian",
+ "aarch64-little-endian",
+] as const;
+
+export type RecordedCrashArchitecture =
+ (typeof RECORDED_LINUX_CORE_ARCHITECTURES)[number];
+
+/**
+ * ABI facts for one recorded-core architecture, measured from unchanged pwntools
+ * 4.15.0 `elf_prstatus_*` and `user_regs_struct_*` structures and mirrored by
+ * `bridge/pwntools/recorded_crash_impl.py`. Both profiles place `pr_cursig` at
+ * 12, `pr_pid` at 32 and the register window at 112; the ELF machine, the window
+ * contents and the Evidence CPU family differ per profile.
+ */
+export const RECORDED_LINUX_CORE_PROFILES: Record<
+ RecordedCrashArchitecture,
+ {
+ readonly machine: number;
+ readonly evidenceArchitecture: BinaryArchitecture;
+ readonly registers: readonly string[];
+ readonly registerBytes: number;
+ readonly registerOffset: number;
+ readonly prstatusBytes: number;
+ }
+> = {
+ "x86_64-little-endian": {
+ machine: 62,
+ evidenceArchitecture: "x86_64",
+ registers: RECORDED_LINUX_AMD64_REGISTERS,
+ registerBytes: 8,
+ registerOffset: 112,
+ prstatusBytes: 336,
+ },
+ "aarch64-little-endian": {
+ machine: 183,
+ evidenceArchitecture: "arm64",
+ registers: RECORDED_LINUX_AARCH64_REGISTERS,
+ registerBytes: 8,
+ registerOffset: 112,
+ prstatusBytes: 392,
+ },
+};
+
+const recordedCoreRegisterSets: Record<
+ RecordedCrashArchitecture,
+ ReadonlySet
+> = {
+ "x86_64-little-endian": new Set(RECORDED_LINUX_AMD64_REGISTERS),
+ "aarch64-little-endian": new Set(RECORDED_LINUX_AARCH64_REGISTERS),
+};
/** Select an explicit recording and optionally request debugger-derived mapping context. */
export const inspectRecordedCrashInputSchema = z.strictObject({
@@ -108,7 +197,7 @@ const recordedCrashObjectSchema = z.strictObject({
bytes: index,
}),
format: z.literal("elf-core"),
- architecture: z.literal("x86_64-little-endian"),
+ architecture: z.enum(RECORDED_LINUX_CORE_ARCHITECTURES),
target_execution: z.literal("not-performed"),
live_process_identity: z.literal("unknown"),
segments: z.array(
@@ -284,6 +373,8 @@ export const recordedCrashSchema = recordedCrashObjectSchema.superRefine(
"Each supported raw note requires exactly one corresponding interpretation.",
);
}
+ const profile = RECORDED_LINUX_CORE_PROFILES[value.architecture];
+ const profileRegisters = recordedCoreRegisterSets[value.architecture];
for (const thread of value.threads) {
const note = value.notes[thread.note_index];
if (
@@ -295,8 +386,8 @@ export const recordedCrashSchema = recordedCrashObjectSchema.superRefine(
continue;
}
if (
- thread.registers.length !== RECORDED_LINUX_AMD64_REGISTERS.length ||
- !thread.registers.every(({ name }) => supportedRegisters.has(name)) ||
+ thread.registers.length !== profile.registers.length ||
+ !thread.registers.every(({ name }) => profileRegisters.has(name)) ||
new Set(thread.registers.map(({ name }) => name)).size !==
thread.registers.length
)
@@ -305,7 +396,7 @@ export const recordedCrashSchema = recordedCrashObjectSchema.superRefine(
);
for (const register of thread.registers)
if (
- BigInt(register.location.bytes) !== 8n ||
+ BigInt(register.location.bytes) !== BigInt(profile.registerBytes) ||
!within(
register.location,
BigInt(note.descriptor_location.offset),
diff --git a/src/domain/process/processCapture.comparison.test.ts b/src/domain/process/processCapture.comparison.test.ts
index 4d238da2d..0bc69335d 100644
--- a/src/domain/process/processCapture.comparison.test.ts
+++ b/src/domain/process/processCapture.comparison.test.ts
@@ -192,3 +192,75 @@ it("keeps filesystem comparison unknown without coverage and reports observed ch
}),
).toMatchObject({ filesystem: "changed", status: "changed" });
});
+
+it.each([
+ [500, "interaction", 6],
+ [4, "process", 4],
+] as const)(
+ "orders an interaction difference by its dispatch time (process sample at %i ms)",
+ (processTime, dimension, at) => {
+ const capture = (side: "left" | "right") => {
+ const base = emptyCapture();
+ const journal = [
+ { collection: "filesystem_checkpoints" as const, index: 0 },
+ ...(processTime < 6
+ ? [{ collection: "process_samples" as const, index: 0 }]
+ : []),
+ { collection: "interaction_events" as const, index: 0 },
+ ...(processTime >= 6
+ ? [{ collection: "process_samples" as const, index: 0 }]
+ : []),
+ { collection: "lifecycle" as const, index: 0 },
+ { collection: "lifecycle" as const, index: 1 },
+ { collection: "filesystem_checkpoints" as const, index: 1 },
+ ].map((entry, capture_order) => ({ ...entry, capture_order }));
+ return {
+ ...base,
+ manifest: {
+ ...base.manifest,
+ completed_at: "2026-01-01T00:00:01.000Z",
+ },
+ settlement: { ...base.settlement, elapsed_ms: 1000 },
+ filesystem_checkpoints: base.filesystem_checkpoints.map(
+ (checkpoint, index) => ({
+ ...checkpoint,
+ at_ms: index === 0 ? 0 : 1000,
+ }),
+ ),
+ interaction_events: [
+ {
+ sequence: 0,
+ scheduled_at_ms: 5,
+ dispatched_at_ms: 6,
+ type: "input" as const,
+ data: "hello",
+ outcome:
+ side === "left" ? ("dispatched" as const) : ("failed" as const),
+ },
+ ],
+ process_samples: [
+ {
+ at_ms: processTime,
+ pid: 2,
+ parent_pid: 1,
+ command: side === "left" ? "worker-a" : "worker-b",
+ process_group_id: 1,
+ session_id: 1,
+ },
+ ],
+ event_journal: journal,
+ };
+ };
+ const comparison = compareFullyObserved(capture("left"), capture("right"));
+ expect(comparison).toMatchObject({
+ interaction: "changed",
+ process: "changed",
+ first_divergence: {
+ status: "found",
+ dimension,
+ left_at_ms: at,
+ right_at_ms: at,
+ },
+ });
+ },
+);
diff --git a/src/domain/process/processCapture.validation.test.ts b/src/domain/process/processCapture.validation.test.ts
index df0344b7c..db36b2c64 100644
--- a/src/domain/process/processCapture.validation.test.ts
+++ b/src/domain/process/processCapture.validation.test.ts
@@ -839,3 +839,27 @@ it("requires compatible contracts and enforces capture age through a clock seam"
}),
);
});
+
+it("requires interaction dispatch times to follow their sequence", () => {
+ const capture = emptyCapture();
+ const interaction = (sequence: number, dispatched: number) => ({
+ sequence,
+ scheduled_at_ms: 0,
+ dispatched_at_ms: dispatched,
+ type: "input" as const,
+ data: "x",
+ outcome: "dispatched" as const,
+ });
+ const issues = (dispatched: readonly number[]) =>
+ processCaptureIssues({
+ ...capture,
+ interaction_events: dispatched.map((at, index) => interaction(index, at)),
+ }).filter(({ message }) => message.includes("dispatch timestamps"));
+ expect(issues([5, 5, 10])).toEqual([]);
+ expect(issues([10, 5])).toEqual([
+ {
+ path: "interaction_events",
+ message: expect.stringContaining("dispatch timestamps must be ordered"),
+ },
+ ]);
+});
diff --git a/src/domain/process/processCaptureValidation.ts b/src/domain/process/processCaptureValidation.ts
index 33c6d5483..537f6f92f 100644
--- a/src/domain/process/processCaptureValidation.ts
+++ b/src/domain/process/processCaptureValidation.ts
@@ -17,12 +17,10 @@ type RequireInvariant = (
message: string,
) => void;
-const orderedTimestamps = (
- values: readonly { readonly at_ms: number }[],
-): boolean =>
+const orderedTimestamps = (values: readonly number[]): boolean =>
values.every((value, index) => {
const previous = values[index - 1];
- return previous === undefined || value.at_ms >= previous.at_ms;
+ return previous === undefined || value >= previous;
});
const validateCommitments = (
@@ -66,7 +64,14 @@ const validateOrdering = (
["rendered_frames", capture.rendered_frames],
["process_samples", capture.process_samples],
] as const)
- require(orderedTimestamps(values), name, "timestamps must be ordered");
+ require(orderedTimestamps(
+ values.map(({ at_ms }) => at_ms),
+ ), name, "timestamps must be ordered");
+ // Interactions are dispatched in sequence, so their recorded dispatch
+ // times cannot decrease; comparison ranks a changed interaction by it.
+ require(orderedTimestamps(
+ capture.interaction_events.map(({ dispatched_at_ms }) => dispatched_at_ms),
+ ), "interaction_events", "dispatch timestamps must be ordered");
};
const validateEventJournal = (
@@ -242,7 +247,7 @@ const validateLifecycle = (
require(capture.filesystem_checkpoints.map(({ name }) => name).join(",") ===
"before,after_settlement", "filesystem_checkpoints", "capture must include initial and final filesystem snapshots");
require(orderedTimestamps(
- capture.filesystem_checkpoints,
+ capture.filesystem_checkpoints.map(({ at_ms }) => at_ms),
), "filesystem_checkpoints", "filesystem snapshot times must be ordered");
require(!capture.filesystem_checkpoints.some(({ truncated }) => truncated) ||
capture.truncated, "truncated", "filesystem snapshot truncation must propagate to the capture");
diff --git a/src/domain/process/processComparison.ts b/src/domain/process/processComparison.ts
index 21c7bd80b..eafcd4fd8 100644
--- a/src/domain/process/processComparison.ts
+++ b/src/domain/process/processComparison.ts
@@ -165,17 +165,25 @@ type DivergenceDimension = Exclude<
{ readonly status: "none" | "unknown" }
>["dimension"];
-const eventTime = (value: unknown): number | null => {
- if (
- typeof value === "object" &&
- value !== null &&
- "at_ms" in value &&
- typeof value.at_ms === "number" &&
- Number.isSafeInteger(value.at_ms) &&
- value.at_ms >= 0
- )
- return value.at_ms;
- return null;
+// Interactions record when their outcome was observed as the dispatch time;
+// every other timed observation uses at_ms.
+const EVENT_TIME_FIELD: Readonly> = {
+ terminal: "at_ms",
+ interaction: "dispatched_at_ms",
+ exit: "at_ms",
+ filesystem: "at_ms",
+ process: "at_ms",
+};
+
+const eventTime = (
+ dimension: DivergenceDimension,
+ value: unknown,
+): number | null => {
+ if (typeof value !== "object" || value === null) return null;
+ const time: unknown = Reflect.get(value, EVENT_TIME_FIELD[dimension]);
+ return typeof time === "number" && Number.isSafeInteger(time) && time >= 0
+ ? time
+ : null;
};
const firstCollectionDivergence = (
@@ -195,8 +203,8 @@ const firstCollectionDivergence = (
status: "found",
dimension,
index,
- left_at_ms: eventTime(leftValue),
- right_at_ms: eventTime(rightValue),
+ left_at_ms: eventTime(dimension, leftValue),
+ right_at_ms: eventTime(dimension, rightValue),
left: jsonValueSchema.parse(leftValue),
right: jsonValueSchema.parse(rightValue),
};
diff --git a/src/domain/reconstructionCoverage.fixture.ts b/src/domain/reconstructionCoverage.fixture.ts
index 0a51546b6..2d1e37a19 100644
--- a/src/domain/reconstructionCoverage.fixture.ts
+++ b/src/domain/reconstructionCoverage.fixture.ts
@@ -143,3 +143,13 @@ export const completeReconstructionCoverageData = () => {
],
});
};
+
+/** Complete workspace with its first verifier result and boundary narrowed once. */
+export const completeReconstructionCoverageFixture = () => {
+ const workspace = completeReconstructionCoverageData();
+ const result = workspace.verifier_results[0];
+ const boundary = workspace.boundaries[0];
+ if (result === undefined || boundary === undefined)
+ throw new Error("Expected complete reconstruction coverage fixture");
+ return { workspace, result, boundary };
+};
diff --git a/src/domain/reconstructionCoverage.test.ts b/src/domain/reconstructionCoverage.test.ts
index ac7552571..628ad67c1 100644
--- a/src/domain/reconstructionCoverage.test.ts
+++ b/src/domain/reconstructionCoverage.test.ts
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
completeReconstructionCoverageData,
+ completeReconstructionCoverageFixture,
RECONSTRUCTION_COVERAGE_NOW,
reconstructionCoverageEvidenceId,
} from "./reconstructionCoverage.fixture.js";
@@ -288,11 +289,8 @@ describe("reconstruction coverage verifier selection", () => {
describe("reconstruction coverage conflicting latest verifier observations", () => {
it("keeps conflicting tied assessments unknown regardless of input order", () => {
- const workspace = completeReconstructionCoverageData();
- const result = workspace.verifier_results[0];
- const boundary = workspace.boundaries[0];
- if (result === undefined || boundary === undefined)
- throw new Error("Expected complete reconstruction coverage fixture");
+ const { workspace, result, boundary } =
+ completeReconstructionCoverageFixture();
const failed = {
...result,
status: "fail" as const,
@@ -338,11 +336,8 @@ describe("reconstruction coverage conflicting latest verifier observations", ()
describe("reconstruction coverage equivalent latest verifier observations", () => {
it("coalesces equivalent equal-instant observations and retains all evidence", () => {
- const workspace = completeReconstructionCoverageData();
- const result = workspace.verifier_results[0];
- const boundary = workspace.boundaries[0];
- if (result === undefined || boundary === undefined)
- throw new Error("Expected complete reconstruction coverage fixture");
+ const { workspace, result, boundary } =
+ completeReconstructionCoverageFixture();
const first = {
...result,
observed_at: "2026-07-16T11:00:00.123456789012345678900Z",
@@ -388,11 +383,8 @@ describe("reconstruction coverage equivalent latest verifier observations", () =
describe("reconstruction coverage verifier timestamp selection", () => {
it("lets a later unambiguous observation supersede an older tie", () => {
- const workspace = completeReconstructionCoverageData();
- const result = workspace.verifier_results[0];
- const boundary = workspace.boundaries[0];
- if (result === undefined || boundary === undefined)
- throw new Error("Expected complete reconstruction coverage fixture");
+ const { workspace, result, boundary } =
+ completeReconstructionCoverageFixture();
const failed = { ...result, status: "fail" as const };
const latestPass = {
...result,
@@ -412,50 +404,14 @@ describe("reconstruction coverage verifier timestamp selection", () => {
).toMatchObject({ status: "ready", reasons: [] });
});
- it("orders offset-bearing verifier timestamps chronologically", () => {
- const workspace = completeReconstructionCoverageData();
- const result = workspace.verifier_results[0];
- if (result === undefined)
- throw new Error("Expected fixture verifier result");
- const semantic = workspace;
- const changed = createReconstructionCoverageData({
- ...semantic,
- verifier_results: [
- { ...result, observed_at: "2026-07-16T08:00:00.000Z" },
- {
- ...result,
- observed_at: "2026-07-16T04:00:00.000-05:00",
- status: "fail",
- },
- ],
- });
-
- expect(
- evaluateReconstructionClosure(
- changed,
- "replacement.cli",
- RECONSTRUCTION_COVERAGE_NOW,
- ),
- ).toMatchObject({
- status: "failed",
- reasons: [expect.objectContaining({ code: "verifier-failed" })],
- });
- });
-
- it("selects the later result when timestamps differ below millisecond precision", () => {
- const workspace = completeReconstructionCoverageData();
- const result = workspace.verifier_results[0];
- if (result === undefined)
- throw new Error("Expected fixture verifier result");
+ /** Evaluate a workspace whose newest verifier observation fails the closure. */
+ const expectNewestObservationFails = (passedAt: string, failedAt: string) => {
+ const { workspace, result } = completeReconstructionCoverageFixture();
const changed = createReconstructionCoverageData({
...workspace,
verifier_results: [
- { ...result, observed_at: "2026-07-16T11:00:00.1231Z" },
- {
- ...result,
- observed_at: "2026-07-16T11:00:00.1239Z",
- status: "fail",
- },
+ { ...result, observed_at: passedAt },
+ { ...result, observed_at: failedAt, status: "fail" },
],
});
@@ -469,15 +425,26 @@ describe("reconstruction coverage verifier timestamp selection", () => {
status: "failed",
reasons: [expect.objectContaining({ code: "verifier-failed" })],
});
+ };
+
+ it("orders offset-bearing verifier timestamps chronologically", () => {
+ expectNewestObservationFails(
+ "2026-07-16T08:00:00.000Z",
+ "2026-07-16T04:00:00.000-05:00",
+ );
+ });
+
+ it("selects the later result when timestamps differ below millisecond precision", () => {
+ expectNewestObservationFails(
+ "2026-07-16T11:00:00.1231Z",
+ "2026-07-16T11:00:00.1239Z",
+ );
});
});
describe("reconstruction coverage verifier future timestamps", () => {
it("treats an observation beyond millisecond-precision now as future", () => {
- const workspace = completeReconstructionCoverageData();
- const result = workspace.verifier_results[0];
- if (result === undefined)
- throw new Error("Expected fixture verifier result");
+ const { workspace, result } = completeReconstructionCoverageFixture();
const changed = createReconstructionCoverageData({
...workspace,
verifier_results: [
@@ -505,10 +472,7 @@ describe("reconstruction coverage verifier future timestamps", () => {
});
it("rejects verifier observations from the future", () => {
- const workspace = completeReconstructionCoverageData();
- const result = workspace.verifier_results[0];
- if (result === undefined)
- throw new Error("Expected fixture verifier result");
+ const { workspace, result } = completeReconstructionCoverageFixture();
const semantic = workspace;
const changed = createReconstructionCoverageData({
...semantic,
diff --git a/src/domain/reconstructionVerification.functionSources.test.ts b/src/domain/reconstructionVerification.functionSources.test.ts
index 5dc8db589..e23d37d74 100644
--- a/src/domain/reconstructionVerification.functionSources.test.ts
+++ b/src/domain/reconstructionVerification.functionSources.test.ts
@@ -53,6 +53,7 @@ const source = (
digit: string,
observation: Partial = {},
hasSubject = true,
+ hasCoverage = true,
): Evidence =>
createEvidence(
hasSubject
@@ -69,7 +70,9 @@ const source = (
result: dossier,
confidence: "derived",
authority: "shipped-artifact",
- relationshipCoverage: { status: "complete" },
+ ...(hasCoverage
+ ? { relationshipCoverage: { status: "complete" as const } }
+ : {}),
...observation,
},
);
@@ -197,6 +200,118 @@ describe("function reconstruction source parameters", () => {
});
});
+describe("historical calls-only reconstruction changes", () => {
+ it.each(["missing", "unknown", "partial", "unresolved"] as const)(
+ "keeps historical calls-only changes unknown with %s call coverage",
+ (coverage) => {
+ const changedDossier = functionDossierSchema.parse({
+ ...functionDossierSchema.parse(dossier),
+ callees: [
+ {
+ address: "0x2000",
+ name: "puts",
+ classification: null,
+ body: { available: false, reason: "External function." },
+ },
+ ],
+ });
+ const complete = source("8", {
+ result: jsonValueSchema.parse(changedDossier),
+ });
+ const incomplete = source(
+ "9",
+ {
+ result: jsonValueSchema.parse({
+ ...changedDossier,
+ ...(coverage === "unresolved"
+ ? {
+ unresolved_calls: [
+ {
+ address: "0x1004",
+ reason: "Computed target unresolved.",
+ },
+ ],
+ }
+ : {}),
+ }),
+ ...(coverage === "missing"
+ ? {}
+ : {
+ relationshipCoverage:
+ coverage === "unknown"
+ ? { status: "unknown", reason: "Call graph unavailable." }
+ : coverage === "partial"
+ ? {
+ status: "partial",
+ limitations: ["Additional edges omitted."],
+ }
+ : { status: "complete" },
+ }),
+ },
+ true,
+ coverage !== "missing",
+ );
+ const historicalResult = compareFunctions(left, complete);
+ expect(historicalResult).toMatchObject({
+ status: "changed",
+ summary: { changed: 1 },
+ });
+ expect(historicalResult.changes).toMatchObject([
+ { dimension: "calls", status: "changed" },
+ ]);
+ const completeComparison = comparison(
+ {
+ left_evidence_id: left.evidence_id,
+ right_evidence_id: complete.evidence_id,
+ },
+ {},
+ [left, complete],
+ {},
+ historicalResult,
+ );
+ expect(
+ admittedVerification(completeComparison, [left, complete], "overall")()
+ .status,
+ ).toBe("fail");
+ const evidence = comparison(
+ {
+ left_evidence_id: left.evidence_id,
+ right_evidence_id: incomplete.evidence_id,
+ },
+ {},
+ [left, incomplete],
+ {},
+ historicalResult,
+ );
+ const before = JSON.stringify(evidence);
+ for (const dimension of ["calls", "overall"] as const) {
+ expect(
+ admittedVerification(evidence, [left, incomplete], dimension)(),
+ ).toMatchObject({
+ status: "unknown",
+ summary: { total: 1, passed: 0, failed: 0, unknown: 1 },
+ claims: {
+ items: [
+ expect.objectContaining({
+ status: "unknown",
+ observed_status: "changed",
+ limitations: expect.arrayContaining([
+ expect.stringContaining(`Source ${incomplete.evidence_id}:`),
+ ]),
+ }),
+ ],
+ },
+ });
+ }
+ expect(
+ admittedVerification(evidence, [left, incomplete], "pseudocode")()
+ .status,
+ ).toBe("pass");
+ expect(JSON.stringify(evidence)).toBe(before);
+ },
+ );
+});
+
describe("function reconstruction call coverage", () => {
it("rechecks old unchanged comparisons without affecting unrelated finite dimensions", () => {
const incomplete = source("7", {
diff --git a/src/domain/reconstructionVerification.ts b/src/domain/reconstructionVerification.ts
index dd300a0a2..6f693a158 100644
--- a/src/domain/reconstructionVerification.ts
+++ b/src/domain/reconstructionVerification.ts
@@ -144,10 +144,15 @@ const evaluateClaim = (
return source;
});
const functionSourceFacts = readValidatedSourceFacts(claim, sources);
- const observed = observedStatus(claim, comparison);
+ const observation = comparisonObservation(claim, comparison);
+ const observed = observation.status;
const limitations = [
...authorityLimitations(claim, sources, sides),
- ...sourceCallCoverageLimitations(claim, functionSourceFacts, observed),
+ ...sourceCallCoverageLimitations(
+ claim,
+ functionSourceFacts,
+ observation.hasIndependentFunctionChange === true,
+ ),
];
const relatedUnknowns = reconstructionClaimUnknowns(
heads,
@@ -235,21 +240,40 @@ const validateComparisonIdentity = (claim: Claim, evidence: Evidence): void => {
throw new TypeError("Comparison Evidence identity or authority disagrees");
};
-const observedStatus = (claim: Claim, evidence: Evidence): ObservedStatus => {
+const comparisonObservation = (
+ claim: Claim,
+ evidence: Evidence,
+): {
+ readonly status: ObservedStatus;
+ readonly hasIndependentFunctionChange?: boolean;
+} => {
if (claim.kind === "behavioral") {
const result = processCaptureComparisonSchema.parse(
evidence.normalized_result,
);
validateProcessStatus(result);
- return claim.dimension === "overall"
- ? result.status
- : result[claim.dimension];
+ return {
+ status:
+ claim.dimension === "overall" ? result.status : result[claim.dimension],
+ };
}
if (claim.kind === "structural-function") {
const result = functionComparisonResultSchema.parse(
evidence.normalized_result,
);
- if (claim.dimension === "overall") return result.status;
+ if (claim.dimension === "overall")
+ return {
+ status: result.status,
+ // Historical overall changes may come solely from incomplete call sets.
+ hasIndependentFunctionChange:
+ result.function_match.status === "mismatched" ||
+ (result.function_match.status === "matched" &&
+ result.dimensions.some(
+ (dimension) =>
+ dimension.dimension !== "calls" &&
+ dimension.status === "changed",
+ )),
+ };
const dimension = result.dimensions.find(
(item) => item.dimension === claim.dimension,
);
@@ -263,7 +287,7 @@ const observedStatus = (claim: Claim, evidence: Evidence): ObservedStatus => {
throw new TypeError(
"Function dimension cites Evidence outside comparison closure",
);
- return dimension.status;
+ return { status: dimension.status };
}
const result = artifactComparisonResultSchema.parse(
evidence.normalized_result,
@@ -276,7 +300,7 @@ const observedStatus = (claim: Claim, evidence: Evidence): ObservedStatus => {
throw new TypeError(
"Artifact change cites Evidence outside comparison closure",
);
- return result.status;
+ return { status: result.status };
};
const validateProcessStatus = (
@@ -340,12 +364,12 @@ const authorityLimitations = (
const sourceCallCoverageLimitations = (
claim: Claim,
functionSources: readonly FunctionSourceFacts[],
- observed: ObservedStatus,
+ hasIndependentFunctionChange: boolean,
): string[] => {
if (
claim.kind !== "structural-function" ||
(claim.dimension !== "calls" &&
- !(claim.dimension === "overall" && observed !== "changed"))
+ !(claim.dimension === "overall" && !hasIndependentFunctionChange))
)
return [];
return functionSources.flatMap(({ evidence_id, callCoverageLimitations }) =>
diff --git a/src/domain/referenceSourceSummary.test.ts b/src/domain/referenceSourceSummary.test.ts
new file mode 100644
index 000000000..a0d23b0e1
--- /dev/null
+++ b/src/domain/referenceSourceSummary.test.ts
@@ -0,0 +1,104 @@
+import { describe, expect, it } from "vitest";
+
+import {
+ createHistoricalSourceGraph,
+ createHistoricalSourceManifest,
+ type HistoricalSourceGraphInput,
+} from "./referenceSourceGraph.js";
+import { createHistoricalSourceSummary } from "./referenceSourceSummary.js";
+
+const input = (
+ entries: HistoricalSourceGraphInput["entries"],
+): HistoricalSourceGraphInput => ({
+ schema: "HistoricalSourceGraph",
+ authority: "historical-reference",
+ root_alias: "$REFERENCE_ROOT",
+ inventory_state: "unknown",
+ entries,
+ relationships: [],
+ parse_failures: [],
+ exclusions: [],
+ languages: [],
+ manifests: [],
+ vcs: null,
+ provenance: { importer: "fixture", importer_version: null, caller: "test" },
+ limitations: ["Inventory coverage is unknown"],
+});
+
+describe("committed historical-source summaries", () => {
+ it("preserves unknown coverage, sizes, directories and missing symlinks without mutating the graph", () => {
+ const graph = createHistoricalSourceGraph(
+ input([
+ {
+ path: "a",
+ kind: "directory",
+ tree_state: "unknown",
+ classifications: ["unknown"],
+ limitations: ["Not enumerated"],
+ },
+ {
+ path: "b",
+ kind: "file",
+ sha256: null,
+ size: null,
+ content_state: "unreadable",
+ language: null,
+ classifications: ["unknown"],
+ limitations: ["Not readable"],
+ },
+ {
+ path: "c",
+ kind: "symlink",
+ target: "missing",
+ target_state: "missing",
+ classifications: ["unknown"],
+ limitations: [],
+ },
+ ]),
+ );
+ const original = structuredClone(graph);
+ const summary = createHistoricalSourceSummary(graph, 1);
+ expect(summary).toMatchObject({
+ manifest: createHistoricalSourceManifest(original),
+ inventory_state: "unknown",
+ vcs: null,
+ provenance: original.provenance,
+ file_sizes: { known_bytes: "0", unknown_file_count: 1 },
+ counts: { entries: 3, files: 1, directories: 1, symlinks: 1 },
+ directory_tree_states: { unknown: 1, enumerated: 0 },
+ file_content_states: { unreadable: 1, hashed: 0 },
+ limitations: original.limitations,
+ outline: {
+ entries: [
+ original.entries[0],
+ expect.objectContaining({ size: null, content_state: "unreadable" }),
+ original.entries[2],
+ ],
+ omitted_entry_count: 0,
+ },
+ });
+ expect(summary.outline.entries[1]).not.toHaveProperty("sha256");
+ expect(graph).toEqual(original);
+ });
+
+ it("sums known bytes losslessly beyond the JSON safe integer range", () => {
+ const graph = createHistoricalSourceGraph(
+ input(
+ ["a", "b"].map((path) => ({
+ path,
+ kind: "file",
+ sha256: "a".repeat(64),
+ size: Number.MAX_SAFE_INTEGER,
+ content_state: "hashed",
+ language: null,
+ classifications: ["unknown"],
+ limitations: [],
+ })),
+ ),
+ );
+ expect(createHistoricalSourceSummary(graph, 0)).toMatchObject({
+ file_sizes: { known_bytes: "18014398509481982", unknown_file_count: 0 },
+ outline: { entries: [], omitted_entry_count: 2 },
+ });
+ });
+});
diff --git a/src/domain/referenceSourceSummary.ts b/src/domain/referenceSourceSummary.ts
new file mode 100644
index 000000000..e7e99e3fa
--- /dev/null
+++ b/src/domain/referenceSourceSummary.ts
@@ -0,0 +1,93 @@
+import {
+ createHistoricalSourceManifest,
+ type HistoricalSourceGraph,
+} from "./referenceSourceGraph.js";
+
+type Entry = HistoricalSourceGraph["entries"][number];
+
+const outlineEntry = (entry: Entry) => {
+ if (entry.kind !== "file") return entry;
+ return {
+ path: entry.path,
+ kind: entry.kind,
+ language: entry.language,
+ size: entry.size,
+ content_state: entry.content_state,
+ classifications: entry.classifications,
+ limitations: entry.limitations,
+ };
+};
+
+/** Project one committed inventory without rereading or reinterpreting source. */
+export const createHistoricalSourceSummary = (
+ graph: HistoricalSourceGraph,
+ outlineDepth: number,
+) => {
+ const manifest = createHistoricalSourceManifest(graph);
+ const counts = {
+ entries: graph.entries.length,
+ files: 0,
+ directories: 0,
+ symlinks: 0,
+ relationships: graph.relationships.length,
+ parse_failures: graph.parse_failures.length,
+ exclusions: graph.exclusions.length,
+ };
+ const contentStates = {
+ hashed: 0,
+ "redacted-secret": 0,
+ excluded: 0,
+ unreadable: 0,
+ unknown: 0,
+ };
+ const treeStates = {
+ enumerated: 0,
+ partial: 0,
+ excluded: 0,
+ unreadable: 0,
+ unknown: 0,
+ };
+ let knownBytes = 0n;
+ let unknownSizeFiles = 0;
+ const outline: ReturnType[] = [];
+ for (const entry of graph.entries) {
+ if (entry.kind === "file") {
+ counts.files += 1;
+ contentStates[entry.content_state] += 1;
+ if (entry.size === null) unknownSizeFiles += 1;
+ else knownBytes += BigInt(entry.size);
+ } else if (entry.kind === "directory") {
+ counts.directories += 1;
+ treeStates[entry.tree_state] += 1;
+ } else counts.symlinks += 1;
+ // Canonical entries already use Unicode code-point order; filtering preserves it.
+ if (entry.path.split("/").length <= outlineDepth)
+ outline.push(outlineEntry(entry));
+ }
+ return {
+ schema: "HistoricalSourceSummary" as const,
+ authority: graph.authority,
+ root_alias: graph.root_alias,
+ manifest,
+ inventory_state: graph.inventory_state,
+ provenance: graph.provenance,
+ vcs: graph.vcs,
+ languages: graph.languages,
+ manifests: graph.manifests,
+ counts,
+ file_content_states: contentStates,
+ directory_tree_states: treeStates,
+ file_sizes: {
+ // Decimal text keeps large totals exact; this covers only known inventoried sizes.
+ known_bytes: knownBytes.toString(),
+ unknown_file_count: unknownSizeFiles,
+ },
+ exclusions: graph.exclusions,
+ limitations: graph.limitations,
+ outline: {
+ depth: outlineDepth,
+ entries: outline,
+ omitted_entry_count: graph.entries.length - outline.length,
+ },
+ };
+};
diff --git a/src/domain/sourceMapContents.test.ts b/src/domain/sourceMapContents.test.ts
index 06f07197b..dd80f39fb 100644
--- a/src/domain/sourceMapContents.test.ts
+++ b/src/domain/sourceMapContents.test.ts
@@ -2,10 +2,18 @@ import { expect, it } from "vitest";
import { hasValidSourceMapContents } from "./sourceMapContents.js";
-it("accepts absent map-level source contents and aligned nullable slots", () => {
- expect(hasValidSourceMapContents(2, undefined)).toBe(true);
- expect(hasValidSourceMapContents(2, null)).toBe(true);
- expect(hasValidSourceMapContents(2, [null, "retained source"])).toBe(true);
- expect(hasValidSourceMapContents(2, ["retained source"])).toBe(false);
- expect(hasValidSourceMapContents(2, [null, 42])).toBe(false);
+it("accepts absent map-level source contents and nullable string slots", () => {
+ expect(hasValidSourceMapContents(undefined)).toBe(true);
+ expect(hasValidSourceMapContents(null)).toBe(true);
+ expect(hasValidSourceMapContents([null, "retained source"])).toBe(true);
+ expect(hasValidSourceMapContents([null, 42])).toBe(false);
+ expect(hasValidSourceMapContents("retained source")).toBe(false);
+});
+
+it("leaves sourcesContent length to source decoding (ECMA-426 DecodeSourceMapSources)", () => {
+ expect(hasValidSourceMapContents([])).toBe(true);
+ expect(hasValidSourceMapContents(["retained source"])).toBe(true);
+ expect(
+ hasValidSourceMapContents(["a", null, "unused trailing content"]),
+ ).toBe(true);
});
diff --git a/src/domain/sourceMapContents.ts b/src/domain/sourceMapContents.ts
index 5d55810a9..c6a621f1a 100644
--- a/src/domain/sourceMapContents.ts
+++ b/src/domain/sourceMapContents.ts
@@ -1,12 +1,15 @@
-/** Validate optional source-map content against its source inventory. */
-export const hasValidSourceMapContents = (
- sourceCount: number,
- contents: unknown,
-): boolean =>
+/**
+ * Validate optional source-map content against its source inventory.
+ *
+ * ECMA-426 DecodeSourceMapSources reads a content slot only while its index is
+ * within `sourcesContent`, so a shorter array leaves trailing sources without
+ * content and a longer array contributes no extra sources. Only the entry types
+ * are checked here.
+ */
+export const hasValidSourceMapContents = (contents: unknown): boolean =>
contents === undefined ||
contents === null ||
(Array.isArray(contents) &&
- contents.length === sourceCount &&
contents.every(
(content: unknown) => content === null || typeof content === "string",
));
diff --git a/src/domain/wasm/wasmArtifact.ts b/src/domain/wasm/wasmArtifact.ts
index d8c89ddf3..a15735125 100644
--- a/src/domain/wasm/wasmArtifact.ts
+++ b/src/domain/wasm/wasmArtifact.ts
@@ -1,3 +1,4 @@
+import { wasmInterfaceSchema } from "./wasmInterface.js";
import { z } from "zod";
import { createHash } from "node:crypto";
import type { EvidenceSubjectTarget } from "../evidence.js";
@@ -32,6 +33,7 @@ export const wasmArtifactSchema = z
description: z.string(),
}),
),
+ interface: wasmInterfaceSchema.exactOptional(),
imports: z.array(z.string()),
exports: z.array(z.string()),
headers: z.string(),
diff --git a/src/domain/wasm/wasmInterface.ts b/src/domain/wasm/wasmInterface.ts
new file mode 100644
index 000000000..f5559269c
--- /dev/null
+++ b/src/domain/wasm/wasmInterface.ts
@@ -0,0 +1,109 @@
+import { z } from "zod";
+
+const kind = z.enum(["func", "table", "memory", "global", "tag", "unknown"]);
+const valueType = z.enum([
+ "i32",
+ "i64",
+ "f32",
+ "f64",
+ "v128",
+ "funcref",
+ "externref",
+]);
+const limit = z.string().regex(/^(?:0|[1-9][0-9]*)$/u);
+const signature = z.discriminatedUnion("kind", [
+ z.object({
+ kind: z.literal("func"),
+ params: z.array(valueType),
+ results: z.array(valueType),
+ }),
+ z.object({
+ kind: z.literal("tag"),
+ params: z.array(valueType),
+ results: z.array(valueType),
+ }),
+ z.object({
+ kind: z.literal("table"),
+ element: z.enum(["funcref", "externref"]),
+ address_type: z.enum(["i32", "i64"]),
+ minimum: limit,
+ maximum: limit.nullable(),
+ shared: z.boolean(),
+ }),
+ z.object({
+ kind: z.literal("memory"),
+ page_size: limit,
+ address_type: z.enum(["i32", "i64"]),
+ minimum: limit,
+ maximum: limit.nullable(),
+ shared: z.boolean(),
+ }),
+ z.object({
+ kind: z.literal("global"),
+ value_type: valueType,
+ mutable: z.boolean(),
+ }),
+]);
+
+/** Typed observations decoded once at the WABT producer boundary. */
+export const wasmInterfaceSchema = z.object({
+ state: z.enum(["complete", "partial"]),
+ entries: z.array(
+ z
+ .object({
+ direction: z.enum(["import", "export"]),
+ module: z.string().nullable(),
+ name: z.string(),
+ kind,
+ index: z.number().int().nonnegative().max(0xffff_ffff).nullable(),
+ signature: signature.nullable(),
+ wat_form: z.string(),
+ issue: z.string().nullable(),
+ })
+ .superRefine((entry, context) => {
+ if (
+ (entry.direction === "export" && entry.module !== null) ||
+ (entry.direction === "import" && entry.module === null)
+ )
+ context.addIssue({
+ code: "custom",
+ message: "Interface direction does not match module-name presence",
+ });
+ if (entry.signature !== null && entry.signature.kind !== entry.kind)
+ context.addIssue({
+ code: "custom",
+ message: "Interface kind does not match its typed signature",
+ });
+ }),
+ ),
+ issues: z.array(z.string()),
+});
+export type WasmInterface = z.infer;
+export type WasmInterfaceEntry = WasmInterface["entries"][number];
+export type WasmInterfaceSignature = NonNullable<
+ WasmInterfaceEntry["signature"]
+>;
+
+export type WasmInterfaceValueType = z.infer;
+
+/** Bound eager typed output and repeated signature expansion independently of WAT bytes. */
+export const WASM_INTERFACE_REPRESENTATION_BYTES = 32 * 1024 * 1024;
+
+/** Conservative JSON/retention charge; shared arrays still expand at every occurrence. */
+export const wasmInterfaceEntryRepresentationBytes = (
+ entry: WasmInterfaceEntry,
+): number => {
+ let bytes =
+ 1024 +
+ [entry.module, entry.name, entry.wat_form, entry.issue].reduce