diff --git a/README.md b/README.md index fcf48f4326..1b55470270 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,7 @@ Additional tools and host support depend on the target: | .NET assemblies | Metadata, CIL instructions, declared native dependencies and build comparisons | None (static only); [managed-code guide](docs/managed-code-analysis.md) | | Android APKs | Manifest declarations, classes, decompiled methods and references | Headless JADX and a full JDK on Linux/macOS/Windows x64; [Android guide](docs/android-analysis.md) | | Android devices | Devices, packages, processes, logs, dumps, screen captures, and APK/file transfers | Caller-supplied adb (emulator or hardware); [ADB guide](docs/adb-device-analysis.md) | +| JEB projects | Engine-backed project units, artifact digests and type/method pseudo-code | A caller-started JEB client serving MCP, selected with REA_JEB_MCP_URL; [JEB guide](docs/jeb-analysis.md) | | Firmware | Regions, extraction results and native-analysis handoffs | Binwalk / Unblob on Linux; [firmware guide](docs/firmware-analysis.md) | | Packages and resources | File inventories, digests, plists, Apple bundle anatomy and extracted resources | [Artifact and JavaScript guide](docs/javascript-artifact-reconstruction.md), [Apple applications](docs/apple-application-analysis.md) | | Process behavior | Terminal output, interactions, exit and filesystem observations, and run comparisons | Linux/macOS with a native PTY; [process capture](docs/process-capture.md) | diff --git a/README_ar.md b/README_ar.md index 57c3262b79..e1c384d5ba 100644 --- a/README_ar.md +++ b/README_ar.md @@ -143,6 +143,7 @@ npx rea-agents@latest setup | تجميعات .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) | | البرامج الثابتة | المناطق، ونتائج الاستخراج، وما يُمرّر إلى تحليل الشيفرة الأصلية | Binwalk / Unblob على Linux؛ [دليل البرامج الثابتة](docs/firmware-analysis.md) | | الحزم والموارد | قوائم الملفات، والبصمات، وملفات plist، وبنية حزم Apple، والموارد المستخرجة | [دليل تحليل الملفات وJavaScript](docs/javascript-artifact-reconstruction.md)، [تطبيقات Apple](docs/apple-application-analysis.md) | | سلوك العمليات | مخرجات الطرفية، والتفاعلات، وملاحظات الخروج ونظام الملفات، ومقارنات التشغيل | Linux/macOS مع PTY أصلي؛ [تسجيل سلوك العمليات](docs/process-capture.md) | diff --git a/README_de.md b/README_de.md index b69d5b1ebd..4250d3069c 100644 --- a/README_de.md +++ b/README_de.md @@ -143,6 +143,7 @@ REA benötigt Node.js 22.x (>=22.19), 24.x (>=24.11) oder 26+ sowie npm. Zusätz | .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) | | Firmware | Bereiche, Extraktionsergebnisse und Übergaben an die native Analyse | Binwalk / Unblob unter Linux; [Firmware-Leitfaden](docs/firmware-analysis.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) | diff --git a/README_es.md b/README_es.md index 0bc4ad4c8c..348fdd1ff8 100644 --- a/README_es.md +++ b/README_es.md @@ -143,6 +143,7 @@ REA requiere Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, además de npm. Las h | 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) | | 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 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) | diff --git a/README_fa.md b/README_fa.md index ec515c66b0..07df058306 100644 --- a/README_fa.md +++ b/README_fa.md @@ -135,6 +135,7 @@ REA به Node.js 22.x (>=22.19)، 24.x (>=24.11) یا 26+، به‌همراه np | اسمبلی‌های .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) | | Firmware | ناحیه‌ها، نتایج استخراج و تحویل به ابزارهای تحلیل بومی | Binwalk / Unblob در لینوکس؛ [راهنمای Firmware](docs/firmware-analysis.md) | | بسته‌ها و منابع | فهرست فایل‌ها، digestها، فایل‌های plist، ساختار bundle اپل و منابع استخراج‌شده | [راهنمای آرتیفکت و JavaScript](docs/javascript-artifact-reconstruction.md)، [برنامه‌های اپل](docs/apple-application-analysis.md) | | رفتار فرایند | خروجی ترمینال، تعاملات، وضعیت خروج، مشاهدات سیستم فایل و مقایسهٔ اجراها | Linux/macOS با PTY بومی؛ [ضبط فرایند](docs/process-capture.md) | diff --git a/README_fr.md b/README_fr.md index 0f627f8c20..75b96543da 100644 --- a/README_fr.md +++ b/README_fr.md @@ -144,6 +144,7 @@ REA nécessite Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, ainsi que npm. Les | 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) | | 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 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) | diff --git a/README_id.md b/README_id.md index dfeef6ae68..ed3887e2c7 100644 --- a/README_id.md +++ b/README_id.md @@ -147,6 +147,7 @@ Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis: | 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) | | Firmware | Wilayah, hasil ekstraksi, dan pengalihan ke analisis native | Binwalk / Unblob di Linux; [panduan firmware](docs/firmware-analysis.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) | diff --git a/README_ja.md b/README_ja.md index 1a377703e5..9cf8a6e99a 100644 --- a/README_ja.md +++ b/README_ja.md @@ -143,6 +143,7 @@ REA には Node.js 22.x(>=22.19)、24.x(>=24.11)、または 26+ と npm | .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) | | ファームウェア | 領域、抽出結果、ネイティブ分析への引き継ぎ | Linux 上の Binwalk / Unblob;[ファームウェアガイド](docs/firmware-analysis.md) | | パッケージとリソース | ファイル一覧、ダイジェスト、plist、Apple バンドルの構造、抽出されたリソース | [アーティファクトと JavaScript のガイド](docs/javascript-artifact-reconstruction.md)、[Apple アプリケーション](docs/apple-application-analysis.md) | | プロセスの動作 | ターミナル出力、対話、終了とファイルシステムの観測結果、実行の比較 | ネイティブ PTY を備えた Linux/macOS;[プロセスキャプチャ](docs/process-capture.md) | diff --git a/README_ko.md b/README_ko.md index 4791602ebe..b6c424899c 100644 --- a/README_ko.md +++ b/README_ko.md @@ -143,6 +143,7 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니 | .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) | | 펌웨어 | 영역, 추출 결과, 네이티브 분석으로 전달할 정보 | Linux의 Binwalk / Unblob; [펌웨어 가이드](docs/firmware-analysis.md) | | 패키지 및 리소스 | 파일 목록, 다이제스트, plist, Apple 번들 구조, 추출한 리소스 | [아티팩트 및 JavaScript 가이드](docs/javascript-artifact-reconstruction.md), [Apple 애플리케이션](docs/apple-application-analysis.md) | | 프로세스 동작 | 터미널 출력, 상호작용, 종료 및 파일 시스템 관찰 결과, 실행 비교 | 네이티브 PTY를 지원하는 Linux/macOS; [프로세스 캡처](docs/process-capture.md) | diff --git a/README_pl.md b/README_pl.md index b5c6ffa994..67d1b80e2c 100644 --- a/README_pl.md +++ b/README_pl.md @@ -143,6 +143,7 @@ REA wymaga Node.js 22.x (>=22.19), 24.x (>=24.11) lub 26+ oraz npm. Dodatkowe na | 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) | | 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) | | 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) | diff --git a/README_pt-BR.md b/README_pt-BR.md index 656a10daa3..2c88bf4321 100644 --- a/README_pt-BR.md +++ b/README_pt-BR.md @@ -143,6 +143,7 @@ O REA requer Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, além do npm. As fer | 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) | | Firmware | Regiões, resultados de extração e encaminhamentos à análise nativa | Binwalk / Unblob no Linux; [guia de firmware](docs/firmware-analysis.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) | diff --git a/README_ru.md b/README_ru.md index 6912c98489..46461ea9c1 100644 --- a/README_ru.md +++ b/README_ru.md @@ -143,6 +143,7 @@ REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а так | Сборки .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) | | Прошивки | Области, результаты извлечения и передача в нативный анализ | Binwalk / Unblob в Linux; [руководство по прошивкам](docs/firmware-analysis.md) | | Пакеты и ресурсы | Списки файлов, хеши, plist, структура пакетов Apple и извлечённые ресурсы | [Руководство по артефактам и JavaScript](docs/javascript-artifact-reconstruction.md), [приложения Apple](docs/apple-application-analysis.md) | | Поведение процессов | Вывод терминала, взаимодействия, наблюдения за завершением и файловой системой, сравнения запусков | Linux/macOS с нативным PTY; [захват процессов](docs/process-capture.md) | diff --git a/README_th.md b/README_th.md index 9c8e0fb5b5..9599498e46 100644 --- a/README_th.md +++ b/README_th.md @@ -143,6 +143,7 @@ REA ต้องใช้ Node.js 22.x (>=22.19), 24.x (>=24.11) หรือ 26 | แอสเซมบลี .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) | | เฟิร์มแวร์ | พื้นที่ข้อมูล ผลการแตกไฟล์ และการส่งต่อไปวิเคราะห์เนทีฟ | Binwalk / Unblob บน Linux; [คู่มือเฟิร์มแวร์](docs/firmware-analysis.md) | | แพ็กเกจและทรัพยากร | รายการไฟล์ ค่าแฮช plist โครงสร้างบันเดิลของ Apple และทรัพยากรที่แตกออกมา | [คู่มืออาร์ติแฟกต์และ JavaScript](docs/javascript-artifact-reconstruction.md), [แอปพลิเคชัน Apple](docs/apple-application-analysis.md) | | พฤติกรรมของโปรเซส | เอาต์พุตเทอร์มินัล การโต้ตอบ ข้อมูลการสิ้นสุดและระบบไฟล์ที่สังเกตได้ และการเปรียบเทียบการรัน | Linux/macOS ที่มี PTY เนทีฟ; [การบันทึกโปรเซส](docs/process-capture.md) | diff --git a/README_tr.md b/README_tr.md index cad5ef758d..dece11da85 100644 --- a/README_tr.md +++ b/README_tr.md @@ -143,6 +143,7 @@ REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araç | .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) | | 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) | | 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) | diff --git a/README_uk.md b/README_uk.md index 0d3d3e94ca..9dd93f2c97 100644 --- a/README_uk.md +++ b/README_uk.md @@ -143,6 +143,7 @@ REA потребує Node.js 22.x (>=22.19), 24.x (>=24.11) або 26+, а та | Збірки .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) | | Прошивки | Області, результати видобування й передавання до нативного аналізу | Binwalk / Unblob у Linux; [посібник прошивок](docs/firmware-analysis.md) | | Пакети й ресурси | Переліки файлів, хеші, plist, структура пакетів Apple та видобуті ресурси | [Посібник артефактів і JavaScript](docs/javascript-artifact-reconstruction.md), [застосунки Apple](docs/apple-application-analysis.md) | | Поведінка процесів | Вивід термінала, взаємодії, спостереження за завершенням і файловою системою та порівняння запусків | Linux/macOS із нативним PTY; [захоплення процесів](docs/process-capture.md) | diff --git a/README_vi.md b/README_vi.md index c837f7f633..975e6559db 100644 --- a/README_vi.md +++ b/README_vi.md @@ -143,6 +143,7 @@ REA yêu cầu Node.js 22.x (>=22.19), 24.x (>=24.11) hoặc 26+, cùng với np | 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) | | 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 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) | diff --git a/README_zh-TW.md b/README_zh-TW.md index df3eeaf287..278adc433f 100644 --- a/README_zh-TW.md +++ b/README_zh-TW.md @@ -143,6 +143,7 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。 | .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) | | 韌體 | 區域、提取結果和轉交原生分析的內容 | Linux 上的 Binwalk / Unblob;[韌體指南](docs/firmware-analysis.md) | | 軟體包與資源 | 檔案清單、摘要、plist、Apple bundle 結構和提取的資源 | [製品與 JavaScript 指南](docs/javascript-artifact-reconstruction.md)、[Apple 應用](docs/apple-application-analysis.md) | | 處理程序行為 | 終端輸出、互動、退出和檔案系統觀察結果,以及執行對比 | 支援原生 PTY 的 Linux/macOS;[處理程序擷取](docs/process-capture.md) | diff --git a/README_zh.md b/README_zh.md index 25aaa84497..7a6bbd71b5 100644 --- a/README_zh.md +++ b/README_zh.md @@ -143,6 +143,7 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。 | .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) | | 固件 | 区域、提取结果和转交原生分析的内容 | Linux 上的 Binwalk / Unblob;[固件指南](docs/firmware-analysis.md) | | 软件包与资源 | 文件清单、摘要、plist、Apple bundle 结构和提取的资源 | [制品与 JavaScript 指南](docs/javascript-artifact-reconstruction.md)、[Apple 应用](docs/apple-application-analysis.md) | | 进程行为 | 终端输出、交互、退出和文件系统观察结果,以及运行对比 | 支持原生 PTY 的 Linux/macOS;[进程捕获](docs/process-capture.md) | diff --git a/docs/jeb-analysis.md b/docs/jeb-analysis.md new file mode 100644 index 0000000000..e311491b60 --- /dev/null +++ b/docs/jeb-analysis.md @@ -0,0 +1,36 @@ +# JEB instrumentation and analysis + +REA exposes JEB-backed project, unit, and decompilation inspection through the CLI and MCP, served by JEB's own MCP interface. JEB is a bring-your-own engine: REA never installs it, launches it, or opens its project databases itself. + +## Bring-your-own engine + +Start a JEB client that serves MCP and point REA at it: + +- Default endpoint: `http://127.0.0.1:8425/mcp`. Override with `REA_JEB_MCP_URL` (absolute `http`/`https` URL with literal host `127.0.0.1` or `[::1]`; redirects, remote hosts, credentials, query strings, and fragments are rejected rather than silently dropped). +- In the JEB GUI client, start the MCP server from the _File_ menu (or via the VIBRE assistant). +- Headless, use JEB's documented programmatic route (`JebMcpServerInstance` from the JEB API) with the JEB client on your classpath; JEB 5.48's `-c` headless mode does not accept `--mcp` directly. + +Verified against JEB 5.48.0 (Linux x64, licensed build) serving `jeb-mcp-server` 1.3.0 over streamable HTTP. + +## Operations + +| MCP tool / CLI command | Purpose | +| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `inspect_jeb_client` / `inspect-jeb-client` | Engine version, GUI or headless mode, startup time | +| `open_jeb_project` / `open-jeb-project` | Open or create a project from an artifact or `.jdb2` on the engine host; returns input-file SHA-256 digests and top-level units | +| `list_jeb_units` / `list-jeb-units` | Page units by stable path and type (`filter`, `parent-unit-path`, `index`, `count`) | +| `decompile_jeb_item` / `decompile-jeb-item` | Decompile one type or method to engine pseudo-code | + +All four are read-only with respect to the target file; only `open_jeb_project` changes engine-side project state, and Evidence records the endpoint, the caller-selected parameters, and the engine-reported digests. Mutation workflows the engine also offers (renames, comments, `save_project`, `close_project`) are intentionally not exposed yet. + +**Engine coverage note (verified):** JEB 5.48.0 headless MCP instances do not advertise `open_project` — the target must be selected when the engine client starts (`--infile` on the programmatic launcher, or the GUI's file opening). REA preserves that refusal with the engine's reason instead of failing silently; `open_jeb_project` completes only against engine builds that advertise the tool, such as GUI clients. + +## Semantics + +- Paths in `open_jeb_project` are resolved by the JEB process, not by REA; REA does not require the artifact to exist locally. +- The engine caps `list_units` pages at 100; a full page is reported as `coverage: "partial"` because more units may exist. An empty remainder is `complete` only when fewer units than requested were returned. +- When `decompile_jeb_item` omits `unit_path`, the engine implicitly selects the project's first code unit; REA reports `unit_path: null` and a limitation instead of guessing the effective unit. +- Engine refusals (`success: false` envelopes) are preserved as unsupported-target errors carrying the engine's own message, for example a nonexistent item address. +- An unreachable endpoint is a provider-availability failure with recovery guidance; REA does not retry or spawn the engine. + +Real-engine verification lives in the JEB lane described in [testing.md](testing.md); mock transport tests do not establish real-engine compatibility. diff --git a/docs/testing.md b/docs/testing.md index 9896b9bc3b..242b61d626 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -536,6 +536,25 @@ kept under ignored `_reference/`. No Gradle build, Android SDK, emulator or application execution is required. The lane compares real CLI/MCP package, class search, class inventory, method decompilation and incoming references. See [Android analysis](android-analysis.md) for boundaries and resource budgets. + +`verify:jeb` requires a caller-started JEB client serving MCP at +`REA_JEB_MCP_URL` (default `http://127.0.0.1:8425/mcp`) with a project +already open, and verifies real CLI client inspection, unit listing coverage, +and method decompilation. The script records the engine's exact response to +`open_jeb_project`; JEB 5.48.0 headless instances do not advertise that tool. +REA does not install or launch JEB; see [JEB analysis](jeb-analysis.md) for +the bring-your-own boundary. Verified against JEB 5.48.0 serving +`jeb-mcp-server` 1.3.0 at four scales: a compiled Java class fixture, a +locally built signed probe APK with a launcher activity (manifest, v1/v2/v3 +certificates, dex bytecode, filtered and paginated listing), the published +Signal 8.30.3 universal release APK (109 MB, R8-processed Kotlin/Compose, +four signature schemes, native arm64 ELF units) with MainActivity method +decompilation through both CLI and MCP, and the official Flutter Gallery +2.9.2 release APK (112 MB): per-ABI `libapp.so` Dart AOT snapshot images +analyzed as native code (75k methods on arm64) and decompiled through both +CLI and MCP with explicit unit selection, plus the thin Java wrapper and +decoded manifest. The dedicated Dart snapshot processor and `run_script` +remain GUI-only surfaces in JEB 5.48.0 headless. Authenticated IPA and macOS application inventory projection is documented in [Apple application analysis](apple-application-analysis.md). diff --git a/package.json b/package.json index d1c7dfd326..cc689c276e 100644 --- a/package.json +++ b/package.json @@ -171,6 +171,7 @@ "verify:firmware": "npm run build:cached && node scripts/verify/firmware/analysis.mjs", "fixtures:firmware": "node scripts/fixtures/firmware/generate.mjs", "verify:android": "npm run build:cached && node scripts/verify/android/analysis.mjs", + "verify:jeb": "npm run build:cached && node scripts/verify-jeb.mjs", "fixtures:android": "node scripts/fixtures/android/fetch.mjs", "verify:agent": "npm run build:cached && node scripts/verify-agent-experience.mjs", "verify:hopper:linux": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-linux.mjs", diff --git a/scripts/lib/catalog-builders.mjs b/scripts/lib/catalog-builders.mjs index 8f1e215724..c48a847419 100644 --- a/scripts/lib/catalog-builders.mjs +++ b/scripts/lib/catalog-builders.mjs @@ -47,6 +47,11 @@ export const toolFamilyCatalog = (sources) => { surface: "android-provider", contracts: sources.androidContracts.ANDROID_TOOL_CONTRACTS, }, + { + id: "jeb", + surface: "jeb-provider", + contracts: sources.jebContracts.JEB_TOOL_CONTRACTS, + }, { id: "adb", surface: "adb-provider", @@ -202,6 +207,10 @@ export const providerCatalog = (sources) => { identity: sources.androidProvider.JADX_PROVIDER_IDENTITY, contracts: sources.androidContracts.ANDROID_TOOL_CONTRACTS, }, + { + identity: sources.jebProvider.JEB_PROVIDER_IDENTITY, + contracts: sources.jebContracts.JEB_TOOL_CONTRACTS, + }, { identity: sources.adbProvider.ADB_PROVIDER_IDENTITY, contracts: sources.adbContracts.ADB_TOOL_CONTRACTS, diff --git a/scripts/lib/catalog-core.mjs b/scripts/lib/catalog-core.mjs index 4254177d66..03638848f0 100644 --- a/scripts/lib/catalog-core.mjs +++ b/scripts/lib/catalog-core.mjs @@ -53,6 +53,8 @@ const SOURCE_PATHS = { firmwareProvider: "dist/firmware/FirmwareRelease.js", androidContracts: "dist/contracts/android/androidToolContracts.js", androidProvider: "dist/android/JadxRelease.js", + jebContracts: "dist/contracts/jeb/jebToolContracts.js", + jebProvider: "dist/jeb/JebProvider.js", adbContracts: "dist/contracts/adb/adbToolContracts.js", adbProvider: "dist/adb/AdbProvider.js", managedWorkflowContracts: diff --git a/scripts/verify-jeb.mjs b/scripts/verify-jeb.mjs new file mode 100644 index 0000000000..addba3f33a --- /dev/null +++ b/scripts/verify-jeb.mjs @@ -0,0 +1,153 @@ +import { execFile } from "node:child_process"; +import { mkdtemp, writeFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; + +import { JebAnalysisService } from "../dist/application/jeb/JebAnalysisService.js"; +import { createJebAnalysisProvider } from "../dist/composition/jeb.js"; + +const run = promisify(execFile); + +const options = parseArguments(process.argv.slice(2)); +if (options.help) { + process.stdout.write( + "Usage: npm run verify:jeb [-- --item-address ADDRESS --marker TEXT]\n" + + "Requires a running JEB client serving MCP at REA_JEB_MCP_URL\n" + + "(default http://127.0.0.1:8425/mcp) with a project already open.\n" + + "JEB 5.48 headless instances open their target at start (--infile or\n" + + "the documented programmatic launcher); open_jeb_project is verified\n" + + "only when the engine advertises it. Without --item-address, a tiny\n" + + "Java fixture is compiled and its known method is decompiled.\n", + ); + process.exit(0); +} + +const provider = createJebAnalysisProvider(process.env); +const service = new JebAnalysisService(provider); +const fixtureDirectory = await mkdtemp(join(tmpdir(), "rea-jeb-")); +try { + const client = await service.execute("inspect_jeb_client", {}); + if (!client.ok) throw new Error(client.error.message); + const engine = client.value.normalized_result.engine; + if (engine.version === null) + throw new Error("The JEB client did not report its version"); + + // JEB 5.48 headless clients do not advertise open_project; a project opened + // at engine start is the supported target-selection route there. Attempt the + // tool and record the engine's exact refusal when it is unavailable. + let openRefusal = null; + const target = await resolveTarget(options, fixtureDirectory); + const opened = await service.execute("open_jeb_project", { + path: target.path, + }); + if (opened.ok) { + const artifact = opened.value.normalized_result.input_files.find( + (file) => typeof file === "object" && file !== null, + ); + if ( + artifact === undefined || + !/^[0-9a-f]{64}$/u.test(artifact.contents_sha256_hash) + ) + throw new Error("The engine did not report an input artifact digest"); + } else { + openRefusal = opened.error.message; + } + + const units = await service.execute("list_jeb_units", { count: 100 }); + if (!units.ok) throw new Error(units.error.message); + if (units.value.normalized_result.units.length === 0) + throw new Error("The engine reported no project units"); + + const decompiled = await service.execute("decompile_jeb_item", { + item_address: target.itemAddress, + item_kind: "method", + }); + if (!decompiled.ok) throw new Error(decompiled.error.message); + const text = decompiled.value.normalized_result.text; + if (!text.includes(target.marker)) + throw new Error( + "Decompiled pseudo-code did not contain the fixture marker", + ); + + const report = { + ok: true, + provider: "jeb", + host_platform: process.platform, + engine_version: engine.version, + engine_gui: engine.gui_client, + endpoint: engine.endpoint, + open_project_observed: openRefusal === null, + ...(openRefusal === null ? {} : { open_project_refusal: openRefusal }), + unit_count: units.value.normalized_result.units.length, + decompiled_marker_observed: true, + }; + process.stdout.write(`${JSON.stringify(report)}\n`); +} catch (cause) { + process.stderr.write( + `JEB real-provider verification failed: ${cause instanceof Error ? cause.message : String(cause)}\n`, + ); + process.exitCode = 1; +} finally { + await provider.close().catch(() => undefined); + await rm(fixtureDirectory, { recursive: true, force: true }).catch( + () => undefined, + ); +} + +async function resolveTarget(selected, directory) { + if (selected.itemAddress !== undefined) + return { + path: "/engine-side/target", + itemAddress: selected.itemAddress, + marker: selected.marker ?? "", + }; + await writeFile( + join(directory, "Hello.java"), + [ + "public class Hello {", + " public static String greet(String name) {", + ' return "Hello, " + name + "!";', + " }", + "}", + "", + ].join("\n"), + ); + try { + await run("javac", [join(directory, "Hello.java")]); + } catch { + throw new Error( + "javac is required to build the default verification fixture", + ); + } + return { + path: join(directory, "Hello.class"), + itemAddress: "LHello;->greet(Ljava/lang/String;)Ljava/lang/String;", + marker: "Hello, ", + }; +} + +function parseArguments(args) { + if (args.length === 1 && args[0] === "--help") return { help: true }; + const values = new Map(); + for (let index = 0; index < args.length; index += 1) { + const flag = args[index]; + const value = args[index + 1]; + if ( + (flag !== "--item-address" && flag !== "--marker") || + value === undefined + ) + throw new Error( + "Expected no arguments, or --item-address ADDRESS [--marker TEXT]", + ); + values.set(flag, value); + index += 1; + } + return { + help: false, + ...(values.has("--item-address") + ? { itemAddress: values.get("--item-address") } + : {}), + ...(values.has("--marker") ? { marker: values.get("--marker") } : {}), + }; +} diff --git a/src/application/CapabilityInventory.ts b/src/application/CapabilityInventory.ts index e06d86839e..7d3231d90d 100644 --- a/src/application/CapabilityInventory.ts +++ b/src/application/CapabilityInventory.ts @@ -40,6 +40,7 @@ export type AvailabilityPolicy = { readonly firmwareInspectionEnabled?: boolean; readonly firmwareExtractionEnabled?: boolean; readonly androidAnalysisAvailability?: ProviderAvailability; + readonly jebAnalysisAvailability?: ProviderAvailability; readonly javascriptRecoveryEnabled?: boolean; readonly webModuleResolutionEnabled?: boolean; readonly browserObservationEnabled?: boolean; @@ -356,6 +357,15 @@ const workflowAvailabilityFor = ({ policy.androidAnalysisAvailability?.reason ?? "Set REA_JADX_MCP_JAR to a caller-supplied jadx-headless-mcp 0.7.1 JAR and provide a full JDK on Linux, macOS, or Windows x64 with its matching bundled native controls.", }; + if (kind === "jeb-provider") + return policy.jebAnalysisAvailability?.status === "available" + ? { reason: "available", remediation: null } + : { + reason: "provider_missing", + remediation: + policy.jebAnalysisAvailability?.reason ?? + "Start a JEB client serving MCP (GUI client, or headless via its programmatic launcher) and point REA_JEB_MCP_URL at it (default http://127.0.0.1:8425/mcp). REA does not install or launch JEB.", + }; const browser = browserProviderAvailability(name, kind, policy); if (browser !== null) return browser; if (name === "capture_process_scenario" && !policy.processCaptureEnabled) diff --git a/src/application/jeb/JebAnalysisPort.ts b/src/application/jeb/JebAnalysisPort.ts new file mode 100644 index 0000000000..8b0498b93e --- /dev/null +++ b/src/application/jeb/JebAnalysisPort.ts @@ -0,0 +1,23 @@ +import type { + AnalysisExecution, + ExecutionOptions, + ProviderAvailability, +} from "../AnalysisProvider.js"; +import type { JebRequest } from "../../domain/jeb/jebAnalysis.js"; +import type { AnalysisError } from "../../domain/analysisErrorBase.js"; +import type { Result } from "../../domain/result.js"; + +/** + * JEB inspection boundary served by a caller-supplied running engine client. + * Implementation details belong to the provider adapter. + */ +export interface JebAnalysisPort { + /** Probe the running JEB client without opening a project. */ + inspectAvailability(signal?: AbortSignal): Promise; + /** Release engine connection state; the JEB client itself keeps running. */ + close(): Promise; + execute( + request: JebRequest, + options?: ExecutionOptions, + ): Promise>; +} diff --git a/src/application/jeb/JebAnalysisService.test.ts b/src/application/jeb/JebAnalysisService.test.ts new file mode 100644 index 0000000000..3c136aeba0 --- /dev/null +++ b/src/application/jeb/JebAnalysisService.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from "vitest"; + +import { JebAnalysisService } from "./JebAnalysisService.js"; +import type { JebAnalysisPort } from "./JebAnalysisPort.js"; +import type { JebRequest } from "../../domain/jeb/jebAnalysis.js"; +import { createAnalysisExecution } from "../AnalysisProvider.js"; +import { + AnalysisCancelledError, + AnalysisInputError, +} from "../../domain/analysisErrorCore.js"; +import type { AnalysisError } from "../../domain/analysisErrorBase.js"; +import { err, ok, type Result } from "../../domain/result.js"; + +const identity = { id: "jeb", name: "JEB", version: "5.48.0" } as const; + +const fakePort = ( + requests: JebRequest[], + outcome: ReturnType | ReturnType = ok( + createAnalysisExecution({ observed: true }, identity, { + limitations: ["engine-side analysis"], + }), + ), +): JebAnalysisPort => ({ + async inspectAvailability() { + return { + status: "available", + code: null, + reason: null, + diagnostics: {}, + }; + }, + async close() {}, + async execute(request) { + requests.push(request); + return outcome as Result< + ReturnType, + AnalysisError + >; + }, +}); + +describe("JebAnalysisService admission", () => { + it("rejects malformed input before any provider call", async () => { + const requests: JebRequest[] = []; + const service = new JebAnalysisService(fakePort(requests)); + const result = await service.execute("list_jeb_units", { + count: 101, + index: -1, + }); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.error).toBeInstanceOf(AnalysisInputError); + expect(requests).toHaveLength(0); + }); + + it("returns cancellation without provider effects when already aborted", async () => { + const requests: JebRequest[] = []; + const service = new JebAnalysisService(fakePort(requests)); + const controller = new AbortController(); + controller.abort(); + const result = await service.execute( + "inspect_jeb_client", + {}, + { signal: controller.signal }, + ); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.error).toBeInstanceOf(AnalysisCancelledError); + expect(requests).toHaveLength(0); + }); + + it("applies protocol defaults and composes provider evidence", async () => { + const requests: JebRequest[] = []; + const service = new JebAnalysisService(fakePort(requests)); + const result = await service.execute("list_jeb_units", {}); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(requests[0]?.operation).toBe("list_jeb_units"); + const request = requests[0]; + if (request?.operation !== "list_jeb_units") return; + expect(request.input.index).toBe(0); + expect(request.input.count).toBe(100); + expect(result.value.normalized_result).toEqual({ observed: true }); + expect(result.value.parameters).toEqual({ index: 0, count: 100 }); + expect(result.value.limitations).toContain("engine-side analysis"); + expect(result.value.provider.id).toBe("jeb"); + }); + + it("preserves provider failure reasons", async () => { + const service = new JebAnalysisService( + fakePort([], err(new AnalysisCancelledError("open_jeb_project"))), + ); + const result = await service.execute("open_jeb_project", { + path: "/tmp/Example.apk", + }); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.error).toBeInstanceOf(AnalysisCancelledError); + }); +}); diff --git a/src/application/jeb/JebAnalysisService.ts b/src/application/jeb/JebAnalysisService.ts new file mode 100644 index 0000000000..de1a227f3a --- /dev/null +++ b/src/application/jeb/JebAnalysisService.ts @@ -0,0 +1,46 @@ +import type { JebAnalysisPort } from "./JebAnalysisPort.js"; +import type { ExecutionOptions } from "../AnalysisProvider.js"; +import { + jebRequestSchema, + type JebOperation, +} from "../../domain/jeb/jebAnalysis.js"; +import { + AnalysisCancelledError, + AnalysisInputError, +} from "../../domain/analysisErrorCore.js"; +import type { AnalysisError } from "../../domain/analysisErrorBase.js"; +import { createEvidence, type Evidence } from "../../domain/evidence.js"; +import { jsonObjectSchema } from "../../domain/jsonValue.js"; +import { err, ok, type Result } from "../../domain/result.js"; + +/** Shared CLI/MCP admission, execution and Evidence composition for JEB. */ +export class JebAnalysisService { + constructor(readonly provider: JebAnalysisPort) {} + + /** Run one caller-selected JEB request and retain engine provenance inline. */ + async execute( + operation: JebOperation, + input: unknown, + options?: ExecutionOptions, + ): Promise> { + if (options?.signal?.aborted === true) + return err(new AnalysisCancelledError(operation)); + const request = jebRequestSchema.safeParse({ operation, input }); + if (!request.success) + return err(new AnalysisInputError(operation, { cause: request.error })); + const executed = await this.provider.execute(request.data, options); + if (!executed.ok) return executed; + const execution = executed.value; + return ok( + createEvidence(execution.subject ?? undefined, execution.provider, { + operation, + parameters: jsonObjectSchema.parse(request.data.input), + result: execution.result, + rawResult: execution.rawResult, + limitations: execution.limitations, + locations: execution.locations, + confidence: operation === "decompile_jeb_item" ? "derived" : "observed", + }), + ); + } +} diff --git a/src/cli.ts b/src/cli.ts index 3099e50717..3846784042 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,6 +15,7 @@ import { registerArtifactCommands } from "./cli/artifactCommands.js"; import { registerManagedCommands } from "./cli/managedCommands.js"; import { registerFirmwareCommands } from "./cli/firmwareCommands.js"; import { registerAndroidCommands } from "./cli/androidCommands.js"; +import { registerJebCommands } from "./cli/jebCommands.js"; import { registerAdbCommands } from "./cli/adbCommands.js"; import { registerEvidenceCommands } from "./cliEvidenceCommands.js"; import { registerProcessCommands } from "./cli/processCommands.js"; @@ -74,6 +75,7 @@ export const createCli = ( registerArtifactCommands(cli, logger, analysis.runProviderAnalysis); registerManagedCommands(cli, logger, analysis.runProviderAnalysis); registerAndroidCommands(cli, logger, environment); + registerJebCommands(cli, logger, environment); registerAdbCommands(cli, logger, environment); registerFirmwareCommands(cli, logger, environment); registerBinaryDiagnosticsCommands(cli, logger, environment); diff --git a/src/cli/jebCommands.ts b/src/cli/jebCommands.ts new file mode 100644 index 0000000000..9ed78b9070 --- /dev/null +++ b/src/cli/jebCommands.ts @@ -0,0 +1,118 @@ +import { z } from "incur"; +import { AnalysisError } from "../domain/analysisErrorBase.js"; +import { JebAnalysisService } from "../application/jeb/JebAnalysisService.js"; +import type { JebAnalysisPort } from "../application/jeb/JebAnalysisPort.js"; +import { createJebAnalysisProvider } from "../composition/jeb.js"; +import { CLI_COMMANDS } from "../cliCommandNames.js"; +import { logCliCommand } from "../cliLogging.js"; +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import type { JebOperation } from "../domain/jeb/jebAnalysis.js"; +import type { Logger } from "pino"; +import type { CliInstance } from "./types.js"; +import { withCommandCancellation } from "./commandCancellation.js"; + +/** Expose the same explicit JEB requests through one-shot CLI commands. */ +export const registerJebCommands = ( + cli: CliInstance, + logger: Logger, + environment: Readonly>, + provider: JebAnalysisPort = createJebAnalysisProvider(environment), +): void => { + const service = new JebAnalysisService(provider); + const execute = (name: string, operation: JebOperation, input: unknown) => + withCommandCancellation((signal) => + logCliCommand(logger, name, async () => { + const result = await service.execute(operation, input, { signal }); + try { + await provider.close(); + } catch (cause) { + if (cause instanceof AnalysisError) + return projectAnalysisError(cause); + throw cause; + } + return result.ok ? result.value : projectAnalysisError(result.error); + }), + ); + + cli.command(CLI_COMMANDS.inspectJebClient, { + description: "Inspect the running JEB MCP client without opening a target", + args: z.object({}), + run: () => execute(CLI_COMMANDS.inspectJebClient, "inspect_jeb_client", {}), + }); + cli.command(CLI_COMMANDS.openJebProject, { + description: + "Open or create a JEB project from an artifact or .jdb2 database", + args: z.object({ + path: z + .string() + .describe("Artifact or .jdb2 path resolved by the JEB engine host"), + }), + run: ({ args }) => + execute(CLI_COMMANDS.openJebProject, "open_jeb_project", args), + }); + cli.command(CLI_COMMANDS.listJebUnits, { + description: "List JEB project units by path and type with optional filter", + args: z.object({}), + options: z.object({ + filter: z + .string() + .optional() + .describe("Wildcard filter matched against unit paths"), + "parent-unit-path": z + .string() + .optional() + .describe("Restrict results to descendants of this unit path"), + index: z + .number() + .int() + .nonnegative() + .default(0) + .describe("Zero-based index of the first result to return"), + count: z + .number() + .int() + .min(1) + .max(100) + .default(100) + .describe("Maximum results; the engine caps unit pages at 100"), + }), + run: ({ options }) => + execute(CLI_COMMANDS.listJebUnits, "list_jeb_units", { + index: options.index, + count: options.count, + ...(options.filter === undefined ? {} : { filter: options.filter }), + ...(options["parent-unit-path"] === undefined + ? {} + : { parent_unit_path: options["parent-unit-path"] }), + }), + }); + cli.command(CLI_COMMANDS.decompileJebItem, { + description: "Decompile one JEB type or method to engine pseudo-code", + args: z.object({ + "item-address": z + .string() + .min(1) + .describe("Type or method address within the unit"), + "item-kind": z + .enum(["type", "method"]) + .describe("Kind of item to decompile"), + }), + options: z.object({ + "unit-path": z + .string() + .min(1) + .optional() + .describe( + "Target code unit; the engine's first code unit when omitted", + ), + }), + run: ({ args, options }) => + execute(CLI_COMMANDS.decompileJebItem, "decompile_jeb_item", { + item_address: args["item-address"], + item_kind: args["item-kind"], + ...(options["unit-path"] === undefined + ? {} + : { unit_path: options["unit-path"] }), + }), + }); +}; diff --git a/src/cliCommandNames.ts b/src/cliCommandNames.ts index 76b34a5393..4431eb3c3c 100644 --- a/src/cliCommandNames.ts +++ b/src/cliCommandNames.ts @@ -74,6 +74,10 @@ export const CLI_COMMANDS = Object.freeze({ inspectAndroidClass: "inspect-android-class", inspectAndroidMethod: "inspect-android-method", traceAndroidReferences: "trace-android-references", + inspectJebClient: "inspect-jeb-client", + openJebProject: "open-jeb-project", + listJebUnits: "list-jeb-units", + decompileJebItem: "decompile-jeb-item", inspectManagedMembers: "inspect-managed-members", inspectManagedNativeBoundaries: "inspect-managed-native-boundaries", compareManagedMembers: "compare-managed-members", diff --git a/src/composition/jeb.ts b/src/composition/jeb.ts new file mode 100644 index 0000000000..4abc33b9d5 --- /dev/null +++ b/src/composition/jeb.ts @@ -0,0 +1,13 @@ +import type { JebAnalysisPort } from "../application/jeb/JebAnalysisPort.js"; +import type { JebMcpConnectionFactory } from "../jeb/JebMcpConnection.js"; +import { JebProvider } from "../jeb/JebProvider.js"; + +/** Construct one JEB provider bound to the caller-selected MCP endpoint. */ +export const createJebAnalysisProvider = ( + environment: Readonly> = process.env, + connectionFactory?: JebMcpConnectionFactory, +): JebAnalysisPort => + new JebProvider({ + environment, + ...(connectionFactory === undefined ? {} : { connectionFactory }), + }); diff --git a/src/contracts/jeb/jebToolContracts.test.ts b/src/contracts/jeb/jebToolContracts.test.ts new file mode 100644 index 0000000000..255d82f965 --- /dev/null +++ b/src/contracts/jeb/jebToolContracts.test.ts @@ -0,0 +1,46 @@ +import { expect, it } from "vitest"; + +import { JEB_TOOL_CONTRACTS } from "./jebToolContracts.js"; +import { TOOL_CONTRACTS } from "../toolContracts.js"; + +const registered = new Set(TOOL_CONTRACTS.map((contract) => contract.name)); + +it("registers every JEB contract in the canonical inventory with audited effects", () => { + for (const contract of JEB_TOOL_CONTRACTS) { + expect(registered.has(contract.name)).toBe(true); + expect(contract.effects.accessesNetwork).toBe(true); + expect(contract.effects.launchesProcess).toBe(false); + expect(contract.effects.writesFilesystem).toBe(false); + expect(contract.effects.mutatesTarget).toBe(false); + } + expect(contractByName("open_jeb_project").effects.idempotent).toBe(false); +}); + +it("keeps executable examples valid against their advertised input schemas", () => { + for (const contract of JEB_TOOL_CONTRACTS) { + for (const example of contract.examples) { + expect( + contract.inputSchema.safeParse(example.input).success, + `${contract.name}: ${example.title ?? "example"}`, + ).toBe(true); + } + } +}); + +it("describes engine authority truthfully", () => { + for (const contract of JEB_TOOL_CONTRACTS) { + expect(contract.kind).toBe("jeb-provider"); + expect(contract.description).toMatch(/JEB|[Ee]ngine/); + } + expect(contractByName("open_jeb_project").description).toContain( + "REA does not", + ); +}); + +const contractByName = (name: string) => { + const contract = JEB_TOOL_CONTRACTS.find( + (candidate) => candidate.name === name, + ); + if (contract === undefined) throw new Error(`Missing JEB contract: ${name}`); + return contract; +}; diff --git a/src/contracts/jeb/jebToolContracts.ts b/src/contracts/jeb/jebToolContracts.ts new file mode 100644 index 0000000000..69f25792ee --- /dev/null +++ b/src/contracts/jeb/jebToolContracts.ts @@ -0,0 +1,73 @@ +import { + jebInputSchemas, + jebResultSchemas, +} from "../../domain/jeb/jebAnalysis.js"; +import type { ToolContract } from "../toolContractTypes.js"; +import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; +import { toolContractMetadata } from "../toolEffects.js"; + +/** JEB-backed inspection served by a caller-supplied running engine client. */ +export const JEB_TOOL_CONTRACTS = [ + { + name: "inspect_jeb_client", + ...toolContractMetadata("inspect_jeb_client"), + description: + "Inspect the running JEB client serving MCP: engine version, GUI or headless mode, and startup time. Does not open a target. Requires a caller-started JEB client reachable at REA_JEB_MCP_URL (default http://127.0.0.1:8425/mcp); REA never installs or launches JEB.", + kind: "jeb-provider", + inputSchema: jebInputSchemas.inspect_jeb_client, + outputSchema: evidenceResultOf(jebResultSchemas.inspect_jeb_client), + examples: [{ title: "Check the engine", input: {} }], + }, + { + name: "open_jeb_project", + ...toolContractMetadata("open_jeb_project"), + description: + "Open or create a JEB project from an artifact file or existing .jdb2 database on the engine host. Returns the project's input-file digests and top-level units. The engine analyzes the target itself; REA does not read the file or execute it. Fails when a project is already open.", + kind: "jeb-provider", + inputSchema: jebInputSchemas.open_jeb_project, + outputSchema: evidenceResultOf(jebResultSchemas.open_jeb_project), + examples: [ + { + title: "Open a target for analysis", + input: { path: "/tmp/Example.apk" }, + }, + ], + }, + { + name: "list_jeb_units", + ...toolContractMetadata("list_jeb_units"), + description: + "List project units by stable unit path and type, with optional wildcard filter, parent restriction, and pagination. The engine caps pages at 100 units; a full page reports partial coverage because more units may exist. Read-only.", + kind: "jeb-provider", + inputSchema: jebInputSchemas.list_jeb_units, + outputSchema: evidenceResultOf(jebResultSchemas.list_jeb_units), + examples: [ + { + title: "List units", + input: {}, + }, + { + title: "Find DEX units", + input: { filter: "*.dex" }, + }, + ], + }, + { + name: "decompile_jeb_item", + ...toolContractMetadata("decompile_jeb_item"), + description: + "Decompile one type or method to engine pseudo-code using its decompiler (Dalvik, Java, or native). item_address comes from the engine's code item listing; unit_path defaults to the engine's first code unit and stays unknown when omitted. Read-only with respect to the target file.", + kind: "jeb-provider", + inputSchema: jebInputSchemas.decompile_jeb_item, + outputSchema: evidenceResultOf(jebResultSchemas.decompile_jeb_item), + examples: [ + { + title: "Decompile a method", + input: { + item_address: "Lcom/example/Main;->onCreate()V", + item_kind: "method", + }, + }, + ], + }, +] as const satisfies readonly ToolContract[]; diff --git a/src/contracts/toolContractTypes.ts b/src/contracts/toolContractTypes.ts index 0e1cb43cc1..8d5c982f44 100644 --- a/src/contracts/toolContractTypes.ts +++ b/src/contracts/toolContractTypes.ts @@ -25,6 +25,7 @@ export const TOOL_KINDS = [ "artifact-provider", "managed-provider", "android-provider", + "jeb-provider", "adb-provider", "firmware-provider", "browser-provider", diff --git a/src/contracts/toolContracts.ts b/src/contracts/toolContracts.ts index 7446a55fb8..ac51c51bdf 100644 --- a/src/contracts/toolContracts.ts +++ b/src/contracts/toolContracts.ts @@ -6,6 +6,7 @@ import { ARTIFACT_TOOL_CONTRACTS } from "./artifactToolContracts.js"; import { MANAGED_TOOL_CONTRACTS } from "./managed/managedToolContracts.js"; import { FIRMWARE_TOOL_CONTRACTS } from "./firmware/firmwareToolContracts.js"; import { ANDROID_TOOL_CONTRACTS } from "./android/androidToolContracts.js"; +import { JEB_TOOL_CONTRACTS } from "./jeb/jebToolContracts.js"; import { ADB_TOOL_CONTRACTS } from "./adb/adbToolContracts.js"; import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managed/managedWorkflowToolContracts.js"; import { BROWSER_PROVIDER_TOOL_CONTRACTS } from "./browserProviderToolContracts.js"; @@ -31,6 +32,7 @@ export const TOOL_CONTRACTS = [ ...EVM_TOOL_CONTRACTS, ...MANAGED_TOOL_CONTRACTS, ...ANDROID_TOOL_CONTRACTS, + ...JEB_TOOL_CONTRACTS, ...ADB_TOOL_CONTRACTS, ...FIRMWARE_TOOL_CONTRACTS, ...MANAGED_WORKFLOW_TOOL_CONTRACTS, diff --git a/src/contracts/toolEffects.ts b/src/contracts/toolEffects.ts index efd7a1ef67..3f8f510c39 100644 --- a/src/contracts/toolEffects.ts +++ b/src/contracts/toolEffects.ts @@ -98,6 +98,23 @@ export const TOOL_EFFECTS: Readonly> = { launchesProcess: true, writesFilesystem: true, }), + inspect_jeb_client: effects({ + mutatesSession: true, + accessesNetwork: true, + }), + open_jeb_project: effects({ + mutatesSession: true, + accessesNetwork: true, + idempotent: false, + }), + list_jeb_units: effects({ + mutatesSession: true, + accessesNetwork: true, + }), + decompile_jeb_item: effects({ + mutatesSession: true, + accessesNetwork: true, + }), inspect_adb_client: effects({ mutatesSession: true, launchesProcess: true, diff --git a/src/domain/jeb/jebAnalysis.ts b/src/domain/jeb/jebAnalysis.ts new file mode 100644 index 0000000000..304b36a487 --- /dev/null +++ b/src/domain/jeb/jebAnalysis.ts @@ -0,0 +1,139 @@ +import { z } from "zod"; +import { localPathStringSchema } from "../localPath.js"; + +/** + * Caller intent for JEB-backed inspection, independent of the engine build. + * + * JEB resolves paths on the host running the JEB client that serves MCP; REA + * never launches the engine or opens the target itself. + */ +export const jebInputSchemas = { + inspect_jeb_client: z.strictObject({}), + open_jeb_project: z.strictObject({ + path: localPathStringSchema.describe( + "Artifact file or existing .jdb2 project database, resolved by the running JEB client", + ), + }), + list_jeb_units: z.strictObject({ + filter: z + .string() + .optional() + .describe("Wildcard filter matched against unit paths; '*' allowed"), + parent_unit_path: z + .string() + .min(1) + .optional() + .describe("Restrict results to descendants of this unit path"), + index: z + .number() + .int() + .nonnegative() + .default(0) + .describe("Zero-based index of the first result to return"), + count: z + .number() + .int() + .min(1) + .max(100) + .default(100) + .describe( + "Maximum results to return; the engine protocol caps pages at 100", + ), + }), + decompile_jeb_item: z.strictObject({ + unit_path: z + .string() + .min(1) + .optional() + .describe( + "Target code unit; the engine's first code unit in the project when omitted", + ), + item_address: z + .string() + .min(1) + .describe( + "Type or method address within the unit, as listed by the engine", + ), + item_kind: z.enum(["type", "method"]).describe("Kind of item to decompile"), + }), +} as const; + +/** Supported JEB-backed operations. */ +export type JebOperation = keyof typeof jebInputSchemas; +/** Validated JEB requests carry their selected operation. */ +export type JebRequest = { + [Name in JebOperation]: { + operation: Name; + input: z.infer<(typeof jebInputSchemas)[Name]>; + }; +}[JebOperation]; + +/** Validate operation and input together so their types remain correlated. */ +export const jebRequestSchema = z.discriminatedUnion("operation", [ + z.strictObject({ + operation: z.literal("inspect_jeb_client"), + input: jebInputSchemas.inspect_jeb_client, + }), + z.strictObject({ + operation: z.literal("open_jeb_project"), + input: jebInputSchemas.open_jeb_project, + }), + z.strictObject({ + operation: z.literal("list_jeb_units"), + input: jebInputSchemas.list_jeb_units, + }), + z.strictObject({ + operation: z.literal("decompile_jeb_item"), + input: jebInputSchemas.decompile_jeb_item, + }), +]); + +const engine = z.strictObject({ + name: z.string().min(1), + version: z.string().nullable(), + endpoint: z.string().min(1), + gui_client: z.boolean().nullable(), +}); + +const unit = z.strictObject({ + unit_path: z.string(), + unit_type: z.string(), +}); + +/** Portable JEB observations with explicit identity and coverage limitations. */ +export const jebResultSchemas = { + inspect_jeb_client: z.strictObject({ + engine, + startup_ts: z.number().int().nonnegative().nullable(), + message: z.string().nullable(), + }), + open_jeb_project: z.strictObject({ + engine, + project_name: z.string().nullable(), + creation_datetime: z.string().nullable(), + input_files: z.array( + z.strictObject({ + file_name: z.string(), + file_size: z.number().int().nonnegative(), + contents_sha256_hash: z.string(), + }), + ), + units: z.array(unit), + }), + list_jeb_units: z.strictObject({ + engine, + filter: z.string().nullable(), + parent_unit_path: z.string().nullable(), + index: z.number().int().nonnegative(), + count: z.number().int().positive(), + units: z.array(unit), + coverage: z.enum(["complete", "partial"]), + }), + decompile_jeb_item: z.strictObject({ + engine, + unit_path: z.string().nullable(), + item_address: z.string(), + item_kind: z.enum(["type", "method"]), + text: z.string(), + }), +} as const; diff --git a/src/jeb/JebMcpConnection.test.ts b/src/jeb/JebMcpConnection.test.ts new file mode 100644 index 0000000000..23e3c9778f --- /dev/null +++ b/src/jeb/JebMcpConnection.test.ts @@ -0,0 +1,86 @@ +import { createServer } from "node:http"; +import { describe, expect, it } from "vitest"; + +import { + decodeJebToolResult, + createStreamableHttpJebMcpConnection, +} from "./JebMcpConnection.js"; +import { AnalysisProtocolError } from "../domain/analysisErrorCore.js"; + +describe("decodeJebToolResult", () => { + it("passes refusal envelopes through so the provider preserves the engine reason", () => { + expect( + decodeJebToolResult({ + isError: true, + structuredContent: { + success: false, + message: "The decompilation failed because the item is missing", + }, + }), + ).toEqual({ + success: false, + message: "The decompilation failed because the item is missing", + }); + }); + + it("decodes a single JSON text block", () => { + expect( + decodeJebToolResult({ + content: [ + { type: "text", text: '{"success":true,"text":"int x(){}"}' }, + ], + }), + ).toEqual({ success: true, text: "int x(){}" }); + }); + + it("keeps non-JSON success text as an observed string", () => { + expect( + decodeJebToolResult({ + content: [{ type: "text", text: "plain observation" }], + }), + ).toBe("plain observation"); + }); + + it("rejects unparseable error text as a protocol failure with the engine text", () => { + expect(() => + decodeJebToolResult({ + isError: true, + content: [{ type: "text", text: "engine exploded" }], + }), + ).toThrow(/engine exploded/); + }); + + it("rejects contentless results instead of inventing an observation", () => { + expect(() => decodeJebToolResult({ content: [] })).toThrow( + AnalysisProtocolError, + ); + }); +}); + +it("refuses HTTP redirects before sending requests to another endpoint", async () => { + let followed = false; + const server = createServer((request, response) => { + if (request.url === "/redirected") followed = true; + request.resume(); + response.writeHead(307, { Location: "/redirected" }); + response.end(); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (address === null || typeof address === "string") + throw new Error("Expected TCP listener"); + const connection = createStreamableHttpJebMcpConnection( + new URL(`http://127.0.0.1:${address.port}/mcp`), + ); + try { + await expect(connection.connect()).rejects.toThrow(); + expect(followed).toBe(false); + } finally { + await connection.close(); + await new Promise((resolve, reject) => + server.close((cause) => + cause === undefined ? resolve() : reject(cause), + ), + ); + } +}); diff --git a/src/jeb/JebMcpConnection.ts b/src/jeb/JebMcpConnection.ts new file mode 100644 index 0000000000..b70249ce29 --- /dev/null +++ b/src/jeb/JebMcpConnection.ts @@ -0,0 +1,101 @@ +import { + Client, + StreamableHTTPClientTransport, +} from "@modelcontextprotocol/client"; +import { z } from "zod"; +import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js"; +import { AnalysisProtocolError } from "../domain/analysisErrorCore.js"; + +/** Injectable MCP boundary used by the provider and conformance tests. */ +export interface JebMcpConnection { + /** Connect lazily and return the advertised tool names. */ + connect(signal?: AbortSignal): Promise; + /** Invoke one JEB MCP tool and decode its single structured result. */ + call( + name: string, + args: Readonly>, + signal?: AbortSignal, + ): Promise; + close(): Promise; +} + +const toolResultSchema = z.object({ + isError: z.boolean().optional(), + structuredContent: z.record(z.string(), z.unknown()).optional(), + content: z + .array( + z.object({ type: z.string(), text: z.string().optional() }).passthrough(), + ) + .default([]), +}); + +/** Decode a JEB MCP tool result without inventing an empty observation. */ +export const decodeJebToolResult = (input: unknown): JsonValue => { + const result = toolResultSchema.parse(input); + const blocks = result.content.flatMap((block) => + block.type === "text" && block.text !== undefined ? [block.text] : [], + ); + if (result.structuredContent !== undefined) { + // isError envelopes carry JEB's {success:false, message} refusal semantics; + // decode them so the provider can preserve the engine's own reason. + return jsonValueSchema.parse(result.structuredContent); + } + if (blocks.length !== 1) + throw new AnalysisProtocolError( + "JEB MCP omitted a single structured or text result; unsupported content cannot establish an empty observation.", + ); + try { + return jsonValueSchema.parse(JSON.parse(blocks[0] ?? "null")); + } catch { + if (result.isError === true) + throw new AnalysisProtocolError( + `JEB MCP tool failed: ${blocks[0] ?? "unexplained engine failure"}`, + ); + return blocks[0] ?? null; + } +}; + +export type JebMcpConnectionFactory = (endpoint: URL) => JebMcpConnection; + +/** Serve JEB requests from a running engine client over streamable HTTP. */ +export const createStreamableHttpJebMcpConnection: JebMcpConnectionFactory = ( + endpoint, +) => { + let client: Client | undefined; + let serverTools: readonly string[] | undefined; + return { + async connect(signal) { + client ??= new Client({ name: "rea-jeb", version: "0.0.0" }); + if (serverTools === undefined) { + await client.connect( + new StreamableHTTPClientTransport(endpoint, { + requestInit: { redirect: "error" }, + }), + signal === undefined ? {} : { signal }, + ); + const listing = await client.listTools( + {}, + signal === undefined ? {} : { signal }, + ); + serverTools = listing.tools.map((tool) => tool.name); + } + return serverTools; + }, + async call(name, args, signal) { + if (serverTools === undefined) await this.connect(signal); + if (client === undefined) + throw new AnalysisProtocolError("JEB MCP client failed to connect"); + const result = await client.callTool( + { name, arguments: args }, + signal === undefined ? {} : { signal }, + ); + return decodeJebToolResult(result); + }, + async close() { + const active = client; + if (active !== undefined) await active.close(); + client = undefined; + serverTools = undefined; + }, + }; +}; diff --git a/src/jeb/JebMcpEndpoint.ts b/src/jeb/JebMcpEndpoint.ts new file mode 100644 index 0000000000..448d000f8e --- /dev/null +++ b/src/jeb/JebMcpEndpoint.ts @@ -0,0 +1,56 @@ +import { AnalysisInputError } from "../domain/analysisErrorCore.js"; +import type { AnalysisError } from "../domain/analysisErrorBase.js"; +import { err, ok, type Result } from "../domain/result.js"; + +/** Default JEB MCP endpoint documented by the engine's headless client. */ +export const DEFAULT_JEB_MCP_ENDPOINT = "http://127.0.0.1:8425/mcp"; + +/** + * Normalize the caller-selected JEB MCP endpoint. + * + * Only the scheme, host, and port select the engine; credentials, paths with + * segments beyond the MCP route, queries, and fragments are rejected rather + * than silently dropped. + */ +export const parseJebMcpEndpoint = ( + value: string | undefined, +): Result => { + const raw = value === undefined || value.length === 0 ? null : value; + let url: URL; + try { + url = new URL(raw ?? DEFAULT_JEB_MCP_ENDPOINT); + } catch { + return err( + new AnalysisInputError("jeb_mcp_endpoint", { + cause: "Expected an absolute http(s) URL", + }), + ); + } + if (url.protocol !== "http:" && url.protocol !== "https:") + return err( + new AnalysisInputError("jeb_mcp_endpoint", { + cause: "Only http and https JEB MCP endpoints are supported", + }), + ); + if (url.hostname !== "127.0.0.1" && url.hostname !== "[::1]") + return err( + new AnalysisInputError("jeb_mcp_endpoint", { + cause: + "JEB MCP endpoint must use a literal loopback host (127.0.0.1 or ::1)", + }), + ); + if (url.username !== "" || url.password !== "") + return err( + new AnalysisInputError("jeb_mcp_endpoint", { + cause: + "Embedding credentials in the JEB MCP endpoint is not supported; configure authentication with the engine", + }), + ); + if (url.search !== "" || url.hash !== "") + return err( + new AnalysisInputError("jeb_mcp_endpoint", { + cause: "JEB MCP endpoint query strings and fragments are not supported", + }), + ); + return ok(url); +}; diff --git a/src/jeb/JebProvider.test.ts b/src/jeb/JebProvider.test.ts new file mode 100644 index 0000000000..677b7ccb66 --- /dev/null +++ b/src/jeb/JebProvider.test.ts @@ -0,0 +1,274 @@ +import { describe, expect, it } from "vitest"; + +import { JebProvider } from "./JebProvider.js"; +import type { JebMcpConnection } from "./JebMcpConnection.js"; +import { + AnalysisCapabilityUnavailableError, + AnalysisInputError, + AnalysisProtocolError, + AnalysisCancelledError, +} from "../domain/analysisErrorCore.js"; +import type { JsonValue } from "../domain/jsonValue.js"; + +const scriptedConnection = ( + responses: Readonly>, +): JebMcpConnection & { readonly calls: unknown[][] } => { + const calls: unknown[][] = []; + return { + calls, + async connect() { + return []; + }, + async call(name, args) { + calls.push([name, args]); + const response = responses[name]; + if (response === undefined) + throw new Error(`Unexpected JEB MCP call: ${name}`); + if (response instanceof Error) throw response; + return response; + }, + async close() {}, + }; +}; + +const providerWith = (connection: JebMcpConnection) => + new JebProvider({ + environment: { REA_JEB_MCP_URL: "http://127.0.0.1:8425/mcp" }, + connectionFactory: () => connection, + }); + +describe("JebProvider boundary", () => { + it("normalizes client information and caches engine identity", async () => { + const connection = scriptedConnection({ + get_client_information: { + success: true, + version: "5.48.0.202610071539", + gui_client: false, + startup_ts: 1791623439, + }, + }); + const provider = providerWith(connection); + const result = await provider.execute({ + operation: "inspect_jeb_client", + input: {}, + }); + expect(result.ok).toBe(true); + if (!result.ok) return; + const normalized = result.value.result as { + readonly engine: { readonly version: string | null }; + readonly startup_ts: number | null; + }; + expect(normalized.engine.version).toBe("5.48.0.202610071539"); + expect(normalized.startup_ts).toBe(1791623439); + expect(result.value.provider.version).toBe("5.48.0.202610071539"); + }); + + it("reports unavailable when the engine endpoint is unreachable", async () => { + const provider = providerWith( + scriptedConnection({ + get_client_information: new Error( + "connect ECONNREFUSED 127.0.0.1:8425", + ), + list_units: new Error("connect ECONNREFUSED 127.0.0.1:8425"), + }), + ); + const availability = await provider.inspectAvailability(); + expect(availability.status).toBe("unavailable"); + const result = await provider.execute({ + operation: "list_jeb_units", + input: { count: 10, index: 0 }, + }); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.error).toBeInstanceOf(AnalysisCapabilityUnavailableError); + expect(result.error.message).toContain("ECONNREFUSED"); + }); + + it("maps transport failures during an operation to provider unavailability", async () => { + const provider = providerWith( + scriptedConnection({ + get_client_information: { + success: true, + version: "5.48.0", + gui_client: false, + }, + list_units: new Error("connect ECONNREFUSED 127.0.0.1:8425"), + }), + ); + const result = await provider.execute({ + operation: "list_jeb_units", + input: { count: 10, index: 0 }, + }); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.error).toBeInstanceOf(AnalysisCapabilityUnavailableError); + expect(result.error.message).toContain("ECONNREFUSED"); + }); + + it("preserves engine refusal reasons as unsupported targets", async () => { + const provider = providerWith( + scriptedConnection({ + get_client_information: { + success: true, + version: "5.48.0", + gui_client: false, + }, + decompile_code_item: { + success: false, + message: + "The decompilation failed because the item address does not reference an existing code item", + }, + }), + ); + const result = await provider.execute({ + operation: "decompile_jeb_item", + input: { item_address: "LNope;->x()V", item_kind: "method" }, + }); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.error.message).toContain("does not reference an existing"); + }); + + it("derives partial coverage only from a full engine page", async () => { + const provider = providerWith( + scriptedConnection({ + get_client_information: { + success: true, + version: "5.48.0", + gui_client: false, + }, + list_units: { + success: true, + units: Array.from({ length: 3 }, (_, index) => ({ + unit_path: `a/class${index}.dex`, + unit_type: "dex", + })), + }, + }), + ); + const full = await provider.execute({ + operation: "list_jeb_units", + input: { count: 3, index: 0 }, + }); + expect( + full.ok && + (full.value.result as { coverage: string }).coverage === "partial", + ).toBe(true); + const last = await provider.execute({ + operation: "list_jeb_units", + input: { count: 4, index: 0 }, + }); + expect( + last.ok && + (last.value.result as { coverage: string }).coverage === "complete", + ).toBe(true); + }); +}); + +describe("JebProvider target and endpoint selection", () => { + it("keeps the implicitly selected unit unknown when unit_path is omitted", async () => { + const provider = providerWith( + scriptedConnection({ + get_client_information: { + success: true, + version: "5.48.0", + gui_client: false, + }, + decompile_code_item: { success: true, text: "int x() { return 1; }" }, + }), + ); + const result = await provider.execute({ + operation: "decompile_jeb_item", + input: { item_address: "LFoo;->x()I", item_kind: "method" }, + }); + expect(result.ok).toBe(true); + if (!result.ok) return; + const normalized = result.value.result as { + readonly unit_path: string | null; + }; + expect(normalized.unit_path).toBeNull(); + expect(result.value.limitations.join(" ")).toContain("implicitly"); + }); + + it("rejects unusable endpoints at construction", () => { + const rejection = (value: string): AnalysisInputError => { + try { + new JebProvider({ environment: { REA_JEB_MCP_URL: value } }); + } catch (cause) { + if (cause instanceof AnalysisInputError) return cause; + throw cause; + } + throw new Error("endpoint should have been rejected"); + }; + expect(rejection("ftp://example.invalid/mcp").cause).toMatch( + /http and https/, + ); + expect(rejection("http://user:pass@127.0.0.1:8425/mcp").cause).toMatch( + /credentials/, + ); + }); +}); + +it("refuses remote endpoints before constructing a connection", () => { + expect( + () => + new JebProvider({ + environment: { REA_JEB_MCP_URL: "http://example.com/mcp" }, + }), + ).toThrow(AnalysisInputError); +}); + +it.each(["list_jeb_units", "decompile_jeb_item"] as const)( + "rejects an omitted successful payload for %s", + async (operation) => { + const provider = providerWith( + scriptedConnection({ + get_client_information: { success: true, version: "5.48.0" }, + list_units: { success: true }, + decompile_code_item: { success: true }, + }), + ); + const result = await provider.execute( + operation === "list_jeb_units" + ? { operation, input: { count: 10, index: 0 } } + : { + operation, + input: { item_address: "LTest;->x()V", item_kind: "method" }, + }, + ); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisProtocolError); + }, +); + +it("forwards in-flight cancellation to the MCP call", async () => { + const controller = new AbortController(); + let entered!: () => void; + const started = new Promise((resolve) => { + entered = resolve; + }); + const provider = providerWith({ + async connect() { + return []; + }, + async close() {}, + async call(_name, _args, signal) { + entered(); + await new Promise((_resolve, reject) => + signal!.addEventListener("abort", () => reject(signal!.reason), { + once: true, + }), + ); + return {}; + }, + }); + const pending = provider.execute( + { operation: "inspect_jeb_client", input: {} }, + { signal: controller.signal }, + ); + await started; + controller.abort(); + const result = await pending; + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError); +}); diff --git a/src/jeb/JebProvider.ts b/src/jeb/JebProvider.ts new file mode 100644 index 0000000000..9d9797d3a3 --- /dev/null +++ b/src/jeb/JebProvider.ts @@ -0,0 +1,421 @@ +import { z } from "zod"; +import type { JebAnalysisPort } from "../application/jeb/JebAnalysisPort.js"; +import type { + AnalysisExecution, + ExecutionOptions, + ProviderAvailability, + ProviderIdentity, +} from "../application/AnalysisProvider.js"; +import { createAnalysisExecution } from "../application/AnalysisProvider.js"; +import type { JebOperation, JebRequest } from "../domain/jeb/jebAnalysis.js"; +import { jebResultSchemas } from "../domain/jeb/jebAnalysis.js"; +import { + AnalysisCancelledError, + AnalysisCapabilityUnavailableError, + AnalysisProtocolError, + AnalysisUnsupportedTargetError, +} from "../domain/analysisErrorCore.js"; +import { AnalysisError } from "../domain/analysisErrorBase.js"; +import { err, ok, type Result } from "../domain/result.js"; +import { + parseJebMcpEndpoint, + DEFAULT_JEB_MCP_ENDPOINT, +} from "./JebMcpEndpoint.js"; +import { + createStreamableHttpJebMcpConnection, + type JebMcpConnection, + type JebMcpConnectionFactory, +} from "./JebMcpConnection.js"; + +export const JEB_PROVIDER_IDENTITY: ProviderIdentity = { + id: "jeb", + name: "JEB", + version: null, +} as const; + +export interface JebProviderOptions { + /** Caller environment carrying REA_JEB_MCP_URL. */ + readonly environment?: Readonly>; + /** Injectable MCP boundary; defaults to the streamable HTTP client. */ + readonly connectionFactory?: JebMcpConnectionFactory; +} + +/** JEB's tool envelope: success with typed fields, or a message-only failure. */ +const failure = z + .object({ + success: z.literal(false), + message: z.string().optional(), + error: z + .object({ + code: z.string().optional(), + message: z.string().optional(), + recovery_tool: z.string().optional(), + }) + .optional(), + }) + .passthrough(); +const clientInformation = z + .object({ + success: z.literal(true), + version: z.string().optional(), + gui_client: z.boolean().optional(), + startup_ts: z.number().int().nonnegative().optional(), + message: z.string().nullish(), + }) + .passthrough(); +const unitEntry = z.object({ unit_path: z.string(), unit_type: z.string() }); +const openProject = z + .object({ + success: z.literal(true), + name: z.string().nullish(), + creation_datetime: z.string().nullish(), + input_files: z + .array( + z.object({ + file_name: z.string(), + file_size: z.number().int().nonnegative(), + contents_sha256_hash: z.string(), + }), + ) + .optional(), + units: z.array(unitEntry).optional(), + }) + .passthrough(); +const listUnits = z + .object({ success: z.literal(true), units: z.array(unitEntry) }) + .passthrough(); +const decompiledItem = z + .object({ success: z.literal(true), text: z.string() }) + .passthrough(); + +const refusalReason = (payload: unknown, fallback: string): string => { + const parsed = failure.safeParse(payload); + if (!parsed.success) return fallback; + const envelope = parsed.data; + const reason = + envelope.error?.message ?? envelope.message ?? envelope.error?.code; + if (reason !== undefined && reason.length > 0) return reason; + return fallback; +}; + +/** Serve REA's JEB operations from a running engine client over MCP. */ +export class JebProvider implements JebAnalysisPort { + readonly #connection: JebMcpConnection; + readonly #endpoint: URL; + #identity: ProviderIdentity = JEB_PROVIDER_IDENTITY; + #guiClient: boolean | null = null; + + constructor(options: JebProviderOptions = {}) { + const environment = options.environment ?? {}; + const endpoint = parseJebMcpEndpoint(environment.REA_JEB_MCP_URL); + if (!endpoint.ok) throw endpoint.error; + this.#endpoint = endpoint.value; + this.#connection = ( + options.connectionFactory ?? + ((url: URL) => createStreamableHttpJebMcpConnection(url)) + )(this.#endpoint); + } + + async inspectAvailability( + signal?: AbortSignal, + ): Promise { + try { + await this.#refreshClientInformation(signal); + return { + status: "available", + code: null, + reason: null, + diagnostics: { + endpoint: this.#endpoint.toString(), + engine_version: this.#identity.version, + }, + }; + } catch (cause) { + if (signal?.aborted === true) throw cause; + return { + status: "unavailable", + code: "not_configured", + reason: + cause instanceof Error + ? cause.message + : "The JEB MCP endpoint did not respond", + diagnostics: { endpoint: this.#endpoint.toString() }, + }; + } + } + + async close(): Promise { + await this.#connection.close(); + } + + async execute( + request: JebRequest, + options?: ExecutionOptions, + ): Promise> { + if (options?.signal?.aborted === true) + return err(new AnalysisCancelledError(request.operation)); + try { + const execution = await this.#dispatch(request, options?.signal); + if (options?.signal?.aborted) + return err(new AnalysisCancelledError(request.operation)); + if (execution.ok) + this.#identity = { + ...this.#identity, + version: execution.value.provider.version, + }; + return execution; + } catch (cause) { + if (options?.signal?.aborted) + return err(new AnalysisCancelledError(request.operation, { cause })); + return err(this.#providerFailure(request.operation, cause)); + } + } + + async #dispatch( + request: JebRequest, + signal: AbortSignal | undefined, + ): Promise> { + const cancelled = (operation: JebOperation) => { + if (signal?.aborted === true) throw new AnalysisCancelledError(operation); + }; + switch (request.operation) { + case "inspect_jeb_client": { + cancelled(request.operation); + return this.#inspectClient(signal); + } + case "open_jeb_project": { + cancelled(request.operation); + return this.#openProject(request, signal); + } + case "list_jeb_units": { + cancelled(request.operation); + return this.#listUnits(request, signal); + } + case "decompile_jeb_item": { + cancelled(request.operation); + return this.#decompileItem(request, signal); + } + } + } + + async #engine(signal?: AbortSignal): Promise<{ + readonly name: string; + readonly version: string | null; + readonly endpoint: string; + readonly gui_client: boolean | null; + }> { + await this.#requireClientInformation(signal); + return { + name: "jeb", + version: this.#identity.version, + endpoint: this.#endpoint.toString(), + gui_client: this.#guiClient, + }; + } + + async #requireClientInformation(signal?: AbortSignal): Promise { + if (this.#identity.version !== null) return; + await this.#refreshClientInformation(signal); + } + + async #refreshClientInformation( + signal: AbortSignal | undefined, + ): Promise { + if (signal?.aborted === true) + throw new AnalysisCancelledError("inspect_jeb_client"); + const payload = await this.#connection.call( + "get_client_information", + {}, + signal, + ); + const information = clientInformation.parse( + this.#requireSuccess(payload, "inspect_jeb_client"), + ); + this.#identity = { + ...this.#identity, + version: information.version ?? null, + }; + this.#guiClient = information.gui_client ?? null; + } + + async #inspectClient(signal?: AbortSignal) { + const payload = await this.#connection.call( + "get_client_information", + {}, + signal, + ); + const information = clientInformation.parse( + this.#requireSuccess(payload, "inspect_jeb_client"), + ); + this.#identity = { + ...this.#identity, + version: information.version ?? null, + }; + this.#guiClient = information.gui_client ?? null; + const result = jebResultSchemas.inspect_jeb_client.parse({ + engine: { + name: "jeb", + version: information.version ?? null, + endpoint: this.#endpoint.toString(), + gui_client: information.gui_client ?? null, + }, + startup_ts: information.startup_ts ?? null, + message: information.message ?? null, + }); + return ok( + createAnalysisExecution(result, this.#identity, { + rawResult: payload, + limitations: + information.version === undefined + ? ["The engine did not report its version."] + : [], + }), + ); + } + + async #openProject( + request: Extract, + signal?: AbortSignal, + ) { + const payload = await this.#connection.call( + "open_project", + { + file_path: request.input.path, + }, + signal, + ); + const opened = openProject.parse( + this.#requireSuccess(payload, request.operation), + ); + const engine = await this.#engine(signal); + const result = jebResultSchemas.open_jeb_project.parse({ + engine, + project_name: opened.name ?? null, + creation_datetime: opened.creation_datetime ?? null, + input_files: opened.input_files ?? [], + units: opened.units ?? [], + }); + return ok( + createAnalysisExecution(result, this.#identity, { + rawResult: payload, + limitations: [ + "JEB resolves and analyzes the artifact on the engine host; REA did not open the target itself.", + ], + }), + ); + } + + async #listUnits( + request: Extract, + signal?: AbortSignal, + ) { + const { count, index, filter, parent_unit_path } = request.input; + const payload = await this.#connection.call( + "list_units", + { + count, + index, + ...(filter === undefined ? {} : { filter }), + ...(parent_unit_path === undefined ? {} : { parent_unit_path }), + }, + signal, + ); + const listed = listUnits.parse( + this.#requireSuccess(payload, request.operation), + ); + const units = listed.units; + const engine = await this.#engine(signal); + const result = jebResultSchemas.list_jeb_units.parse({ + engine, + filter: filter ?? null, + parent_unit_path: parent_unit_path ?? null, + index, + count, + units, + coverage: units.length < count ? "complete" : "partial", + }); + return ok( + createAnalysisExecution(result, this.#identity, { + rawResult: payload, + limitations: [ + "The engine caps unit pages at 100; a full page means more units may exist.", + ], + }), + ); + } + + async #decompileItem( + request: Extract, + signal?: AbortSignal, + ) { + const { item_address, item_kind, unit_path } = request.input; + const payload = await this.#connection.call( + "decompile_code_item", + { + item_address, + item_kind, + ...(unit_path === undefined ? {} : { unit_path }), + }, + signal, + ); + const decompiled = decompiledItem.parse( + this.#requireSuccess(payload, request.operation), + ); + const engine = await this.#engine(signal); + const result = jebResultSchemas.decompile_jeb_item.parse({ + engine, + unit_path: unit_path ?? null, + item_address, + item_kind, + text: decompiled.text, + }); + return ok( + createAnalysisExecution(result, this.#identity, { + rawResult: payload, + limitations: + unit_path === undefined + ? [ + "The engine selected the project's first code unit implicitly; the effective unit identity is unknown.", + ] + : [], + }), + ); + } + + #requireSuccess(payload: unknown, operation: JebOperation): unknown { + if (typeof payload !== "object" || payload === null) + throw new AnalysisProtocolError( + "JEB MCP returned a non-object envelope; no observation can be established", + ); + const success = (payload as { success?: unknown }).success; + if (success === false) + throw new AnalysisUnsupportedTargetError( + operation, + this.#endpoint.toString(), + refusalReason(payload, "The engine refused the request"), + ); + if (success !== true) + throw new AnalysisProtocolError( + "JEB MCP omitted its success field; no observation can be established", + ); + return payload; + } + + #providerFailure(operation: JebOperation, cause: unknown): AnalysisError { + if (cause instanceof AnalysisError) return cause; + if (cause instanceof z.ZodError) + return new AnalysisProtocolError( + "JEB omitted or malformed required observation fields", + { cause }, + ); + return new AnalysisCapabilityUnavailableError( + "jeb", + operation, + cause instanceof Error ? cause.message : String(cause), + { + cause: cause instanceof Error ? cause : undefined, + userMessage: `Start a JEB client serving MCP (default ${DEFAULT_JEB_MCP_ENDPOINT}) or set REA_JEB_MCP_URL to the running endpoint.`, + }, + ); + } +} diff --git a/src/server/createServer.ts b/src/server/createServer.ts index 63f71fa89f..64134a28ce 100644 --- a/src/server/createServer.ts +++ b/src/server/createServer.ts @@ -64,12 +64,16 @@ import { FirmwareAnalysisService } from "../application/firmware/FirmwareAnalysi import type { FirmwareAnalysisPort } from "../application/firmware/FirmwareAnalysisPort.js"; import { createFirmwareAnalysisProvider } from "../composition/firmware.js"; import { registerAndroidTools } from "./registerAndroidTools.js"; +import { registerJebTools } from "./registerJebTools.js"; import { registerAdbTools } from "./registerAdbTools.js"; import { AndroidAnalysisService } from "../application/android/AndroidAnalysisService.js"; +import { JebAnalysisService } from "../application/jeb/JebAnalysisService.js"; import type { AndroidAnalysisPort } from "../application/android/AndroidAnalysisPort.js"; +import type { JebAnalysisPort } from "../application/jeb/JebAnalysisPort.js"; +import { createAndroidAnalysisProvider } from "../composition/android.js"; +import { createJebAnalysisProvider } from "../composition/jeb.js"; import type { AdbDeviceAnalysisPort } from "../application/adb/AdbDeviceAnalysisPort.js"; import { AdbDeviceAnalysisService } from "../application/adb/AdbDeviceAnalysisService.js"; -import { createAndroidAnalysisProvider } from "../composition/android.js"; import { createAdbDeviceAnalysisProvider } from "../composition/adb.js"; import { registerManagedWorkflowTools } from "./registerManagedWorkflowTools.js"; import { NATIVE_TOOL_CONTRACTS } from "../contracts/native/nativeToolContracts.js"; @@ -105,6 +109,7 @@ export interface CreateServerOptions { readonly webRuntime?: WebRuntimeService; readonly webNetworkCapture?: WebNetworkCaptureService; readonly androidAnalysis?: AndroidAnalysisPort; + readonly jebAnalysis?: JebAnalysisPort; readonly adbDeviceAnalysis?: AdbDeviceAnalysisPort; readonly browserObservation?: BrowserObservationPort; readonly browserScenarioCapture?: BrowserScenarioCapturePort; @@ -204,6 +209,7 @@ export const createServer = ( ); const android = options.androidAnalysis ?? createAndroidAnalysisProvider(environment); + const jeb = options.jebAnalysis ?? createJebAnalysisProvider(environment); const adbDevice = options.adbDeviceAnalysis ?? createAdbDeviceAnalysisProvider(environment); const availability = installSessionToolAvailability( @@ -240,14 +246,24 @@ export const createServer = ( "Android provider cleanup failed", ); }); + void jeb.close().catch((cause: unknown) => { + logger.error( + { error: cause instanceof Error ? cause.message : String(cause) }, + "JEB provider cleanup failed", + ); + }); }; const closeServer = server.close.bind(server); server.close = async () => { - const results = await Promise.allSettled([closeServer(), android.close()]); + const results = await Promise.allSettled([ + closeServer(), + android.close(), + jeb.close(), + ]); for (const result of results) if (result.status === "rejected") throw result.reason; }; - registerConfiguredAnalysisTools(toolContext, android); + registerConfiguredAnalysisTools(toolContext, android, jeb); registerAdbTools( server, new AdbDeviceAnalysisService(adbDevice), @@ -304,6 +320,7 @@ const registerConfiguredAnalysisTools = ( recordEvidence, }: ServerToolContext, android: AndroidAnalysisPort, + jeb: JebAnalysisPort, ): void => { registerAndroidTools( server, @@ -311,6 +328,12 @@ const registerConfiguredAnalysisTools = ( toolLogger, recordEvidence, ); + registerJebTools( + server, + new JebAnalysisService(jeb), + toolLogger, + recordEvidence, + ); registerBinaryDiagnosticsTools( server, options.binaryLayout ?? createBinaryLayoutService(environment), diff --git a/src/server/registerJebTools.ts b/src/server/registerJebTools.ts new file mode 100644 index 0000000000..95a86866d2 --- /dev/null +++ b/src/server/registerJebTools.ts @@ -0,0 +1,58 @@ +import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; +import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; +import type { ServerContext } from "@modelcontextprotocol/server"; +import { JebAnalysisService } from "../application/jeb/JebAnalysisService.js"; +import { toolContract } from "../contracts/toolContracts.js"; +import type { ToolContract } from "../contracts/toolContractTypes.js"; +import type { JebOperation } from "../domain/jeb/jebAnalysis.js"; +import type { Logger } from "pino"; +import { logToolExecution } from "./toolLogging.js"; +import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; + +/** Bind JEB handlers to their exact named contracts through shared workflows. */ +export const registerJebTools = ( + server: EvidenceMcpServer, + service: JebAnalysisService, + logger: Logger, + recordEvidence?: EvidenceWriter["recordEvidence"], +): void => { + const handler = (contract: ToolContract) => { + const name = contract.name; + return async (input: unknown, context: ServerContext) => { + const result = await logToolExecution(logger, name, () => + service.execute(name, input, { signal: context.mcpReq.signal }), + ); + if (!result.ok) return server.delivery.toCallToolResult(result, contract); + const recorded = recordEvidence?.(result.value); + return server.delivery.toEvidenceToolResult( + result.value, + contract, + recorded, + ); + }; + }; + const clientContract = toolContract("inspect_jeb_client"); + server.registerTool( + clientContract.name, + toolRegistrationOptions(clientContract), + handler(clientContract), + ); + const openContract = toolContract("open_jeb_project"); + server.registerTool( + openContract.name, + toolRegistrationOptions(openContract), + handler(openContract), + ); + const unitsContract = toolContract("list_jeb_units"); + server.registerTool( + unitsContract.name, + toolRegistrationOptions(unitsContract), + handler(unitsContract), + ); + const decompileContract = toolContract("decompile_jeb_item"); + server.registerTool( + decompileContract.name, + toolRegistrationOptions(decompileContract), + handler(decompileContract), + ); +}; diff --git a/tests/boundary/cli/jebCli.test.ts b/tests/boundary/cli/jebCli.test.ts new file mode 100644 index 0000000000..2f72269415 --- /dev/null +++ b/tests/boundary/cli/jebCli.test.ts @@ -0,0 +1,53 @@ +import { expect, it } from "vitest"; + +import { createCli } from "../../../src/cli.js"; + +const serve = async ( + argv: readonly string[], +): Promise<{ readonly stdout: string; readonly exitCode: number }> => { + const cli = createCli({ + REA_JEB_MCP_URL: "http://127.0.0.1:1/mcp", + }); + let stdout = ""; + let exitCode = 0; + await cli.serve([...argv], { + env: {}, + exit: (code) => { + exitCode = code; + }, + stdout: (text) => { + stdout += text; + }, + }); + return { stdout, exitCode }; +}; + +it.each([ + ["inspect-jeb-client"], + ["open-jeb-project"], + ["list-jeb-units"], + ["decompile-jeb-item"], +])("%s advertises a schema through the public CLI", async (command) => { + const result = await serve([command, "--schema", "--json"]); + expect(result.exitCode, command).toBe(0); + const schema = JSON.parse(result.stdout) as { args?: unknown }; + expect(schema).toBeTruthy(); +}); + +it("reports an unreachable engine endpoint with recovery guidance", async () => { + const result = await serve(["inspect-jeb-client", "--json"]); + expect(result.stdout).toContain("capability_unavailable"); + expect(result.stdout).toContain("REA_JEB_MCP_URL"); +}); + +it("validates decompile item input before contacting the engine", async () => { + const result = await serve([ + "decompile-jeb-item", + "not-an-address", + "--item-kind", + "method", + "--json", + ]); + expect(result.stdout).toBeTruthy(); + expect(result.stdout).not.toContain("capability_unavailable"); +}); diff --git a/tests/boundary/mcp/jebMcp.test.ts b/tests/boundary/mcp/jebMcp.test.ts new file mode 100644 index 0000000000..7ccc0b5798 --- /dev/null +++ b/tests/boundary/mcp/jebMcp.test.ts @@ -0,0 +1,143 @@ +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { Ajv2020 } from "ajv/dist/2020.js"; +import { expect, it, onTestFinished } from "vitest"; + +import type { JebAnalysisPort } from "../../../src/application/jeb/JebAnalysisPort.js"; +import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js"; +import { toolContract } from "../../../src/contracts/toolContracts.js"; +import { parseEvidence } from "../../../src/domain/evidence.js"; +import { ok } from "../../../src/domain/result.js"; +import { createServer } from "../../../src/server/createServer.js"; +import { createTestBinarySession } from "../../fixtures/binarySession.js"; + +const identity = { id: "jeb", name: "JEB", version: "5.48.0" } as const; + +it("advertises exact valid JEB schemas and records inline evidence through MCP", async () => { + const execution = createAnalysisExecution( + { + engine: { + name: "jeb", + version: "5.48.0", + endpoint: "http://127.0.0.1:8425/mcp", + gui_client: false, + }, + startup_ts: 1791623439, + message: null, + }, + identity, + ); + const provider: JebAnalysisPort = { + async inspectAvailability() { + return { status: "available", code: null, reason: null, diagnostics: {} }; + }, + async close() {}, + async execute() { + return ok(execution); + }, + }; + const session = createTestBinarySession(() => { + throw new Error("deep provider must not start"); + }); + const server = createServer( + { kind: "session", session }, + { jebAnalysis: provider }, + ); + const client = new Client({ name: "jeb-contract", version: "1" }); + onTestFinished(async () => { + await client.close(); + await server.close(); + await session.close(); + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + await client.connect(clientTransport); + const advertised = (await client.listTools()).tools.find( + (tool) => tool.name === "inspect_jeb_client", + ); + if (advertised?.inputSchema === undefined) + throw new Error("JEB tools must publish both schemas"); + const ajv = new Ajv2020({ strict: false, validateFormats: false }); + expect(ajv.validateSchema(advertised.inputSchema)).toBe(true); + const outputSchema: Record = advertised.outputSchema ?? {}; + expect(ajv.validateSchema(outputSchema)).toBe(true); + expect(ajv.validate(advertised.inputSchema, {})).toBe(true); + expect(ajv.validate(advertised.inputSchema, { unexpected: 1 })).toBe(false); + + const called = await client.callTool({ + name: "inspect_jeb_client", + arguments: {}, + }); + expect(called.isError).not.toBe(true); + const text = (called.content ?? []) + .map((block) => (block.type === "text" ? block.text : "")) + .join(""); + const evidence = parseEvidence(JSON.parse(text)); + expect(evidence.provider.id).toBe("jeb"); + expect(evidence.normalized_result).toEqual(execution.result); +}); + +it("preserves JEB provider failure reasons through the MCP boundary", async () => { + const contract = toolContract("list_jeb_units"); + const provider: JebAnalysisPort = { + async inspectAvailability() { + return { status: "available", code: null, reason: null, diagnostics: {} }; + }, + async close() {}, + async execute() { + return ok( + createAnalysisExecution( + { engine: null, units: [], coverage: "complete" } as never, + identity, + ), + ); + }, + }; + void contract; + const unavailable: JebAnalysisPort = { + ...provider, + async execute() { + const { AnalysisCapabilityUnavailableError } = + await import("../../../src/domain/analysisErrorCore.js"); + return { + ok: false as const, + error: new AnalysisCapabilityUnavailableError( + "jeb", + "list_jeb_units", + "connect ECONNREFUSED 127.0.0.1:8425", + { + userMessage: + "Start a JEB client serving MCP (default http://127.0.0.1:8425/mcp) or set REA_JEB_MCP_URL to the running endpoint.", + }, + ), + }; + }, + }; + const session = createTestBinarySession(() => { + throw new Error("deep provider must not start"); + }); + const server = createServer( + { kind: "session", session }, + { jebAnalysis: unavailable }, + ); + const client = new Client({ name: "jeb-failure", version: "1" }); + onTestFinished(async () => { + await client.close(); + await server.close(); + await session.close(); + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + await client.connect(clientTransport); + const failed = await client.callTool({ + name: "list_jeb_units", + arguments: {}, + }); + expect(failed.isError).toBe(true); + const text = (failed.content ?? []) + .map((block) => (block.type === "text" ? block.text : "")) + .join(""); + expect(text).toContain("ECONNREFUSED"); + expect(text).toContain("REA_JEB_MCP_URL"); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 32edc6856e..2048fcbff9 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -37,7 +37,7 @@ const TEST_PROJECTS = [ name: "adapters", include: [ "src/*.test.ts", - "src/{adb,artifacts,browser,dotnet,evm,firmware,ghidra,hopper,ida,inspector,javascript,native,process,reference,server,windows}/**/*.test.ts", + "src/{jeb,adb,artifacts,browser,dotnet,evm,firmware,ghidra,hopper,ida,inspector,javascript,native,process,reference,server,windows}/**/*.test.ts", ], pool: "forks" as const, maxWorkers: MAX_TEST_WORKERS,