mirror of
https://github.com/morluto/rea.git
synced 2026-10-11 21:39:55 +00:00
feat(jeb): add JEB engine provider over its MCP interface (#1536)
* feat(jeb): add JEB engine provider over its MCP interface
Add a bring-your-own JEB provider that serves project, unit, and
decompilation inspection from a caller-started JEB client speaking MCP
(streamable HTTP, selected with REA_JEB_MCP_URL). Expose four provider-neutral
operations through shared CLI/MCP workflows: inspect_jeb_client,
open_jeb_project, list_jeb_units, and decompile_jeb_item, each returning
Evidence with engine identity, endpoint, and preserved refusal reasons.
The adapter keeps JEB's envelope semantics: success fields are parsed
format-aware at the boundary, message-only refusals become unsupported-target
errors with the engine's own reason, transport failures become availability
failures with recovery guidance, and unit pagination derives coverage from the
engine's 100-item page cap. Explicit unknowns (implicitly selected unit,
omitted filters) are reported instead of guessed.
Verified against a real JEB 5.48.0 licensed build serving jeb-mcp-server 1.3.0
headless: client inspection, unit listing, and method decompilation were
exercised through the CLI, MCP boundary, and scripts/verify-jeb.mjs.
JEB 5.48.0 headless instances do not advertise open_project; the refusal is
preserved and documented.
* fix(jeb): verify against a real signed APK and fix CLI option surface
Verify the provider against a locally built, v1/v2/v3-signed probe APK
(binary manifest, classes.dex with a launcher activity) on JEB 5.48.0: the
full unit tree (apk, manifest, three certificate units, dex bytecode,
decompiler), wildcard-filtered and paginated unit listing with derived
coverage, parent-restricted listing, and launcher-activity method
decompilation through both CLI and MCP, including dexdec's inline
constant folding of the probe's obfuscated tag.
Move list-jeb-units filter/pagination and decompile-jeb-item unit selection
from positional arguments to options; the MCP schemas are unchanged.
* fix(jeb): preserve engine refusals through the real MCP boundary
Testing against the published Signal 8.30.3 universal APK exposed a boundary
bug the scripted provider tests missed: decodeJebToolResult raised a protocol
error for isError results before the provider could map JEB's
{success:false, message} envelope, so real engine refusals surfaced as
unreadable output instead of the engine's own reason. Decode refusal
envelopes like any structured result and let the provider preserve the
reason; cover the decoder directly, including contentless and unparseable
results.
Signal verification: full 109 MB R8/Kotlin/Compose APK with four signature
schemes and native arm64 ELF units parsed; MainActivity.onCreate and R8
synthetic lambdas decompiled through CLI and MCP; paginated, filtered, and
parent-restricted unit listing; refusal path confirmed on the live engine.
* docs(jeb): record Flutter Gallery verification on the real-engine lane
Verify the provider against the official Flutter Gallery 2.9.2 release APK
(112 MB): per-ABI libapp.so Dart AOT snapshot images analyzed as native code
with 75k methods on arm64, snapshot internals and wrapper methods decompiled
through both CLI and MCP with explicit unit selection, decoded manifest, and
filtered native-library unit discovery. Note that JEB 5.48.0 headless does
not expose the dedicated Dart snapshot processor or run_script; both remain
GUI-only engine surfaces.
* docs(jeb): translate the README JEB row for every supported language
The JEB row was inserted in English across all translated READMEs; each now
carries its own label, capability summary, requirements, and guide link
following that language's Android-row conventions.
* fix(jeb): enforce local endpoints and preserve cancellation
* fix(jeb): refuse redirects across the local MCP boundary
---------
Co-authored-by: REA Contributor <[email protected]>
Co-authored-by: morluto <[email protected]>
This commit is contained in:
co-authored by
REA Contributor
morluto
parent
ded59689ce
commit
749e58772a
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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") } : {}),
|
||||
};
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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<ProviderAvailability>;
|
||||
/** Release engine connection state; the JEB client itself keeps running. */
|
||||
close(): Promise<void>;
|
||||
execute(
|
||||
request: JebRequest,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<AnalysisExecution, AnalysisError>>;
|
||||
}
|
||||
@@ -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<typeof ok> | ReturnType<typeof err> = 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<typeof createAnalysisExecution>,
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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<Result<Evidence, AnalysisError>> {
|
||||
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",
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Record<string, string | undefined>>,
|
||||
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"] }),
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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<Record<string, string | undefined>> = process.env,
|
||||
connectionFactory?: JebMcpConnectionFactory,
|
||||
): JebAnalysisPort =>
|
||||
new JebProvider({
|
||||
environment,
|
||||
...(connectionFactory === undefined ? {} : { connectionFactory }),
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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[];
|
||||
@@ -25,6 +25,7 @@ export const TOOL_KINDS = [
|
||||
"artifact-provider",
|
||||
"managed-provider",
|
||||
"android-provider",
|
||||
"jeb-provider",
|
||||
"adb-provider",
|
||||
"firmware-provider",
|
||||
"browser-provider",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -98,6 +98,23 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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<void>((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<void>((resolve, reject) =>
|
||||
server.close((cause) =>
|
||||
cause === undefined ? resolve() : reject(cause),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -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<readonly string[]>;
|
||||
/** Invoke one JEB MCP tool and decode its single structured result. */
|
||||
call(
|
||||
name: string,
|
||||
args: Readonly<Record<string, JsonValue>>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<JsonValue>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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<URL, AnalysisError> => {
|
||||
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);
|
||||
};
|
||||
@@ -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<Record<string, JsonValue | Error>>,
|
||||
): 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:[email protected]: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<void>((resolve) => {
|
||||
entered = resolve;
|
||||
});
|
||||
const provider = providerWith({
|
||||
async connect() {
|
||||
return [];
|
||||
},
|
||||
async close() {},
|
||||
async call(_name, _args, signal) {
|
||||
entered();
|
||||
await new Promise<void>((_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);
|
||||
});
|
||||
@@ -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<Record<string, string | undefined>>;
|
||||
/** 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<ProviderAvailability> {
|
||||
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<void> {
|
||||
await this.#connection.close();
|
||||
}
|
||||
|
||||
async execute(
|
||||
request: JebRequest,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<AnalysisExecution, AnalysisError>> {
|
||||
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<Result<AnalysisExecution, AnalysisError>> {
|
||||
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<void> {
|
||||
if (this.#identity.version !== null) return;
|
||||
await this.#refreshClientInformation(signal);
|
||||
}
|
||||
|
||||
async #refreshClientInformation(
|
||||
signal: AbortSignal | undefined,
|
||||
): Promise<void> {
|
||||
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<JebRequest, { operation: "open_jeb_project" }>,
|
||||
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<JebRequest, { operation: "list_jeb_units" }>,
|
||||
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<JebRequest, { operation: "decompile_jeb_item" }>,
|
||||
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.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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<JebOperation>) => {
|
||||
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),
|
||||
);
|
||||
};
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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<string, unknown> = 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");
|
||||
});
|
||||
+1
-1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user