feat(evm): inspect offline bytecode interfaces through EVMole (#1051)

* feat(evm): inspect offline bytecode interfaces through evmole

* fix(evm): distinguish unsupported bytecode targets

* style(evm): format verifier string-prefix checks
This commit is contained in:
Ysopwnable 2026-10-08 15:32:05 +08:00 committed by GitHub
parent c5d165bd03
commit 7af7bc88ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 3250 additions and 63 deletions

View File

@ -0,0 +1,66 @@
name: Real offline EVM interface
on:
workflow_dispatch:
pull_request:
paths:
- ".github/workflows/real-evm-interface.yml"
- "src/evm/**"
- "src/domain/evm/**"
- "src/application/evm/**"
- "src/contracts/evm/**"
- "src/composition/evm.ts"
- "src/cli/evmCommands.ts"
- "src/server/registerEvmTools.ts"
- "src/server/createServer.ts"
- "src/cli.ts"
- "src/cliCommandNames.ts"
- "src/contracts/toolContracts.ts"
- "src/contracts/toolEffects.ts"
- "src/process/OwnedCommand.ts"
- "src/process/ProviderProcess.ts"
- "src/process/ProcessOwnership*.ts"
- "src/process/ProviderDeadline.ts"
- "src/process/PrivateRuntimeRoot.ts"
- "src/artifacts/readStableArtifact*.ts"
- "src/artifacts/ArtifactReader.ts"
- "scripts/verify-evm-interface.mjs"
- "scripts/fixtures/evm-interface.sol"
- "third_party/evmole/**"
- "package.json"
- "package-lock.json"
permissions:
contents: read
concurrency:
group: real-evm-interface-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
NODE_OPTIONS: --max-old-space-size=768
HUSKY: "0"
jobs:
real-evm-interface:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- name: Acquire unchanged fixture compiler in isolated runner storage
shell: bash
run: |
evm_fixture_root="${RUNNER_TEMP}/rea-evm-fixture-compiler"
mkdir -p "${evm_fixture_root}"
npm install --prefix "${evm_fixture_root}" --ignore-scripts --no-audit --no-fund --save-exact solc@0.8.30
command -v prlimit
command -v strace
echo "REA_VERIFY_STRACE_COMMAND=$(command -v strace)" >> "${GITHUB_ENV}"
echo "REA_VERIFY_SOLC_MODULE=${evm_fixture_root}/node_modules/solc/index.js" >> "${GITHUB_ENV}"
- name: Verify source-owned compiled bytecode through public CLI and MCP
run: npm run verify:evm:interface

View File

@ -151,6 +151,7 @@ Additional tools and host support depend on the target:
| ---------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](https://morluto.github.io/rea/guides/native/) |
| Offline ELF layout | Sections, segments, original symbols/relocations and static mitigation candidates | Caller-supplied pwntools on Linux x64; [binary diagnostics](docs/binary-diagnostics.md) |
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier; [offline EVM guide](docs/evm-bytecode.md) |
| JavaScript / Electron | Modules, imports, source maps, routes, IPC and native add-on relationships | Node.js and npm; [application analysis](https://morluto.github.io/rea/guides/javascript/) |
| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](https://morluto.github.io/rea/guides/browser/) |
| Saved network captures | Requests, responses, exposed payloads and source locations | HAR; mitmdump on Linux for native mitmproxy captures; [capture guide](docs/web-network-captures.md) |

View File

@ -218,7 +218,7 @@ rea setup
| الفحص الأصلي | 41 | الدوال والشيفرة شبه المصدرية والتعليمات والسلاسل والرموز والاستدعاءات والمراجع والتعليقات وقراءة البايتات ومواضع الملفات |
| سير عمل التحقيق | 14 | نظرات عامة على التطبيقات وملفات الدوال وواجهات API الأصلية والتوزيع وفك الترجمة الدفعي وتتبع الميزات ومسارات الاستدعاء ومخططاته واكتشاف Swift وObjective-C |
| أدوات الملفات الثنائية | 9 | بيانات Mach-O والتوقيعات وplist والمعماريات وأسماء Swift ومراقبة الاستدعاءات عبر LLDB على macOS؛ تخطيط ELF وأدلة الحماية الثابتة على Linux |
| مخطط المخرجات | 6 | جرد الدلائل والحزم وملفات Interface Builder المترجمة وكتالوجات موارد Apple وحل تحميل مكتبات Mach-O الديناميكية والاستخراج |
| مخطط المخرجات | 7 | جرد الدلائل والحزم وملفات Interface Builder المترجمة وكتالوجات موارد Apple وتحليل تحميل Mach-O dylib والاستخراج واستدلال واجهات EVM دون اتصال |
| PE/CLI المُدار | 7 | هوية تجميعات .NET وبياناتها الوصفية وتعليمات CIL والاعتماديات الأصلية واستيراد إعادة البناء ومقارنة الإصدارات |
| البرامج الثابتة | 2 | فحص مناطق البرامج الثابتة على Linux واستخراجها الصريح |
| Android APK | 5 | بيانات الحزمة وmanifest والبحث عن الأصناف وقوائم الأعضاء وفك ترجمة الأساليب والمراجع الثابتة |

View File

@ -236,20 +236,20 @@ TypeScript と SQLite を使って私のプロジェクト向けに実装して
## 調査ツールカタログ
| ツール分類 | 数 | 用途 |
| -------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ネイティブ検査 | 41 | 関数、疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照、注釈、バイト読み取り、ファイルオフセット |
| 調査ワークフロー | 14 | アプリ概要、関数ドシエ、ネイティブ API とディスパッチ、一括逆コンパイル、機能トレース、コールパス、コールグラフ、Swift と Objective-C の検出 |
| ネイティブバイナリ | 9 | macOS の Mach-O メタデータ、署名、plist、アーキテクチャ、Swift シンボル、LLDB による呼び出し観測;Linux の ELF レイアウトと静的な保護機構の証拠 |
| 成果物グラフ | 6 | ディレクトリとパッケージの目録、コンパイル済み Interface Builder ファイル、Apple アセットカタログ、Mach-O dylib 読み込みの解決、抽出 |
| マネージド PE/CLI | 7 | .NET の識別情報、メタデータ、CIL 命令、ネイティブ依存関係、再構築結果のインポート、ビルド比較 |
| ファームウェア | 2 | Linux ファームウェア領域の検査と明示的な展開 |
| Android APK | 5 | パッケージと manifest の宣言、クラス検索、メンバー一覧、メソッドの逆コンパイル、静的な被参照 |
| ブラウザー観察 | 12 | ページ構成、ネットワークメタデータ、スクリプト、ソースマップ、WebMCP の検出、スクリーンショット、キャプチャ比較 |
| Electron 解析 | 5 | レンダラーの観察、静的なアプリのマッピング、静的結果と実行時結果の照合 |
| JavaScript 実行時 | 2 | Node/Electron Inspector の対象検出、スクリプトの位置、実行コンテキストのイベント |
| アプリワークフロー | 13 | 取得した Web スクリプトのエクスポート、Android/Apple の目録の投影、層をまたぐ機能トレース、ビルド比較、過去のソースとの対応付け、静的な戻り値形状の比較、再構築の検証 |
| ワークスペースと観察 | 21 | セッション、Evidence バンドル、ナビゲーションの文脈、プロセス・成果物・関数の比較、未解決事項の記録 |
| ツール分類 | 数 | 用途 |
| -------------------- | --: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ネイティブ検査 | 41 | 関数、疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照、注釈、バイト読み取り、ファイルオフセット |
| 調査ワークフロー | 14 | アプリ概要、関数ドシエ、ネイティブ API とディスパッチ、一括逆コンパイル、機能トレース、コールパス、コールグラフ、Swift と Objective-C の検出 |
| ネイティブバイナリ | 9 | macOS の Mach-O メタデータ、署名、plist、アーキテクチャ、Swift シンボル、LLDB による呼び出し観測;Linux の ELF レイアウトと静的な保護機構の証拠 |
| 成果物グラフ | 7 | ディレクトリとパッケージの目録、コンパイル済み Interface Builder ファイル、Apple アセットカタログ、Mach-O dylib のロード解決、抽出、オフライン EVM インターフェース推定 |
| マネージド PE/CLI | 7 | .NET の識別情報、メタデータ、CIL 命令、ネイティブ依存関係、再構築結果のインポート、ビルド比較 |
| ファームウェア | 2 | Linux ファームウェア領域の検査と明示的な展開 |
| Android APK | 5 | パッケージと manifest の宣言、クラス検索、メンバー一覧、メソッドの逆コンパイル、静的な被参照 |
| ブラウザー観察 | 12 | ページ構成、ネットワークメタデータ、スクリプト、ソースマップ、WebMCP の検出、スクリーンショット、キャプチャ比較 |
| Electron 解析 | 5 | レンダラーの観察、静的なアプリのマッピング、静的結果と実行時結果の照合 |
| JavaScript 実行時 | 2 | Node/Electron Inspector の対象検出、スクリプトの位置、実行コンテキストのイベント |
| アプリワークフロー | 13 | 取得した Web スクリプトのエクスポート、Android/Apple の目録の投影、層をまたぐ機能トレース、ビルド比較、過去のソースとの対応付け、静的な戻り値形状の比較、再構築の検証 |
| ワークスペースと観察 | 21 | セッション、Evidence バンドル、ナビゲーションの文脈、プロセス・成果物・関数の比較、未解決事項の記録 |
## ロードマップ

View File

@ -203,20 +203,20 @@ REA는 1~5단계의 바이너리 분석을 처리합니다. 6단계는 에이전
## 조사 도구 카탈로그
| 도구 분류 | 수 | 용도 |
| ----------------- | --: | ------------------------------------------------------------------------------------------------------------------------- |
| 네이티브 검사 | 41 | 함수, 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조, 주석, 바이트 읽기, 파일 오프셋 |
| 조사 워크플로 | 14 | 앱 개요, 함수 기록서, 네이티브 API와 디스패치, 일괄 디컴파일, 기능 추적, 호출 경로, 호출 그래프, Swift와 Objective-C 탐색 |
| 네이티브 바이너리 | 9 | macOS의 Mach-O 메타데이터, 서명, plist, 아키텍처, Swift 심볼과 LLDB 호출 관찰; Linux의 ELF 레이아웃과 정적 보호 증거 |
| 아티팩트 그래프 | 6 | 디렉터리와 패키지 목록, 컴파일된 Interface Builder 파일, Apple 애셋 카탈로그, Mach-O dylib 로드 해석, 추출 |
| 관리 PE/CLI | 7 | .NET 식별, 메타데이터, CIL 명령어, 네이티브 종속성, 재구성 가져오기, 빌드 비교 |
| 펌웨어 | 2 | Linux 펌웨어 영역 검사 및 명시적 추출 |
| Android APK | 5 | 패키지와 manifest 선언, 클래스 검색, 멤버 목록, 메서드 디컴파일, 정적 참조 |
| 브라우저 관찰 | 12 | 페이지 구조, 네트워크 메타데이터, 스크립트, 소스 맵, WebMCP 탐색, 스크린샷, 캡처 비교 |
| Electron 분석 | 5 | 렌더러 관찰, 정적 앱 매핑, 정적·런타임 결과 연결 |
| JavaScript 런타임 | 2 | Node/Electron Inspector 대상 탐색, 스크립트 위치, 실행 컨텍스트 이벤트 |
| 앱 워크플로 | 13 | 캡처한 웹 스크립트 내보내기, 계층 간 기능 추적, 빌드 비교, 히스토리 소스 매핑, 정적 반환 구조 비교, 재구현 검증 |
| 작업 공간과 관찰 | 21 | 세션, 근거 번들, 탐색 컨텍스트, 프로세스·아티팩트·함수 비교, 미해결 항목 기록 |
| 도구 분류 | 수 | 용도 |
| ----------------- | --: | ---------------------------------------------------------------------------------------------------------------------------------------- |
| 네이티브 검사 | 41 | 함수, 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조, 주석, 바이트 읽기, 파일 오프셋 |
| 조사 워크플로 | 14 | 앱 개요, 함수 기록서, 네이티브 API와 디스패치, 일괄 디컴파일, 기능 추적, 호출 경로, 호출 그래프, Swift와 Objective-C 탐색 |
| 네이티브 바이너리 | 9 | macOS의 Mach-O 메타데이터, 서명, plist, 아키텍처, Swift 심볼과 LLDB 호출 관찰; Linux의 ELF 레이아웃과 정적 보호 증거 |
| 아티팩트 그래프 | 7 | 디렉터리와 패키지 목록, 컴파일된 Interface Builder 파일, Apple 애셋 카탈로그, Mach-O dylib 로드 해석, 추출, 오프라인 EVM 인터페이스 추론 |
| 관리 PE/CLI | 7 | .NET 식별, 메타데이터, CIL 명령어, 네이티브 종속성, 재구성 가져오기, 빌드 비교 |
| 펌웨어 | 2 | Linux 펌웨어 영역 검사 및 명시적 추출 |
| Android APK | 5 | 패키지와 manifest 선언, 클래스 검색, 멤버 목록, 메서드 디컴파일, 정적 참조 |
| 브라우저 관찰 | 12 | 페이지 구조, 네트워크 메타데이터, 스크립트, 소스 맵, WebMCP 탐색, 스크린샷, 캡처 비교 |
| Electron 분석 | 5 | 렌더러 관찰, 정적 앱 매핑, 정적·런타임 결과 연결 |
| JavaScript 런타임 | 2 | Node/Electron Inspector 대상 탐색, 스크립트 위치, 실행 컨텍스트 이벤트 |
| 앱 워크플로 | 13 | 캡처한 웹 스크립트 내보내기, 계층 간 기능 추적, 빌드 비교, 히스토리 소스 매핑, 정적 반환 구조 비교, 재구현 검증 |
| 작업 공간과 관찰 | 21 | 세션, 근거 번들, 탐색 컨텍스트, 프로세스·아티팩트·함수 비교, 미해결 항목 기록 |
## 로드맵

View File

@ -202,20 +202,20 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
## 调查工具目录
| 工具类别 | 数量 | 用途 |
| ----------------- | ---: | ------------------------------------------------------------------------------------------------------ |
| 原生检查 | 41 | 函数、伪代码、汇编、字符串、符号、调用、引用、注释、字节读取与文件偏移 |
| 调查工作流 | 14 | 应用概览、函数档案、原生 API 与分发、批量反编译、功能追踪、调用路径与调用图、Swift 与 Objective-C 发现 |
| 原生二进制工具 | 9 | macOS 的 Mach-O 元数据、签名、plist、架构、Swift 符号与 LLDB 调用观察;Linux 的 ELF 布局和静态保护证据 |
| 产物图 | 6 | 目录与软件包清单、编译后的 Interface Builder 文件、Apple 资源目录、Mach-O dylib 加载解析与提取 |
| 托管 PE/CLI | 7 | .NET 程序集身份、元数据、CIL 指令、原生依赖声明、重建导入与构建比较 |
| 固件 | 2 | Linux 固件区域检查与显式提取 |
| Android APK | 5 | 包与 manifest 声明、类搜索、成员清单、方法反编译与静态引用 |
| 浏览器观察 | 12 | 页面结构、网络元数据、脚本、来源映射、WebMCP 发现、截图与捕获比较 |
| Electron 分析 | 5 | 渲染进程观察、静态应用映射、静态/运行时结果关联 |
| JavaScript 运行时 | 2 | Node/Electron Inspector 目标发现、脚本位置与执行上下文事件 |
| 应用工作流 | 13 | 捕获的网站脚本导出、跨层功能追踪、构建比较、历史源码映射、静态返回结构比较与重建验证 |
| 工作区与观察 | 21 | 会话、证据包、导航上下文、进程/产物/函数比较与待解决问题跟踪 |
| 工具类别 | 数量 | 用途 |
| ----------------- | ---: | ----------------------------------------------------------------------------------------------------------------- |
| 原生检查 | 41 | 函数、伪代码、汇编、字符串、符号、调用、引用、注释、字节读取与文件偏移 |
| 调查工作流 | 14 | 应用概览、函数档案、原生 API 与分发、批量反编译、功能追踪、调用路径与调用图、Swift 与 Objective-C 发现 |
| 原生二进制工具 | 9 | macOS 的 Mach-O 元数据、签名、plist、架构、Swift 符号与 LLDB 调用观察;Linux 的 ELF 布局和静态保护证据 |
| 产物图 | 7 | 目录与软件包清单、编译后的 Interface Builder 文件、Apple 资源目录、Mach-O dylib 加载解析、提取与离线 EVM 接口推断 |
| 托管 PE/CLI | 7 | .NET 程序集身份、元数据、CIL 指令、原生依赖声明、重建导入与构建比较 |
| 固件 | 2 | Linux 固件区域检查与显式提取 |
| Android APK | 5 | 包与 manifest 声明、类搜索、成员清单、方法反编译与静态引用 |
| 浏览器观察 | 12 | 页面结构、网络元数据、脚本、来源映射、WebMCP 发现、截图与捕获比较 |
| Electron 分析 | 5 | 渲染进程观察、静态应用映射、静态/运行时结果关联 |
| JavaScript 运行时 | 2 | Node/Electron Inspector 目标发现、脚本位置与执行上下文事件 |
| 应用工作流 | 13 | 捕获的网站脚本导出、跨层功能追踪、构建比较、历史源码映射、静态返回结构比较与重建验证 |
| 工作区与观察 | 21 | 会话、证据包、导航上下文、进程/产物/函数比较与待解决问题跟踪 |
## 路线图

View File

@ -27,6 +27,7 @@ graph TD
subgraph "Provider Layers"
PWNTOOLS["src/native/pwntools/<br/>BYO offline ELF adapter<br/>owned Python process, unchanged upstream tables/heuristics"]
EVM["src/evm/<br/>unchanged EVMole/WASM in bounded owned worker<br/>explicit carrier parsing and inferred interface adapter"]
HOPPER["src/hopper/<br/>Hopper launch & Unix-socket protocol<br/>BridgeLauncher, HopperClient, protocol"]
GHIDRA["src/ghidra/<br/>Ghidra discovery, profile & function analysis<br/>snapshot, Unix/Windows transport, bounded lifecycle"]
IDA["src/ida/<br/>Upstream MCP contract adaptation<br/>live GUI binding or owned headless database"]
@ -144,3 +145,8 @@ graph TD
TESTS -.-> BROWSER
TESTS -.-> EVALUATION
TESTS -.-> ERRORS
COMPOSITION --> EVM
APP -->|EVM interface port| EVM
EVM --> PROCESS
EVM --> DOMAIN

View File

@ -10,6 +10,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -45,6 +46,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -80,6 +82,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -115,6 +118,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -150,6 +154,43 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
},
"message": { "type": "string", "minLength": 1 },
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"code": { "type": "string", "const": "unsupported_target" },
"category": {
"type": "string",
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -185,6 +226,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -220,6 +262,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -255,6 +298,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -290,6 +334,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -325,6 +370,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -382,6 +428,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -417,6 +464,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -452,6 +500,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -487,6 +536,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -522,6 +572,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -557,6 +608,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -592,6 +644,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -627,6 +680,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -662,6 +716,7 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",

84
docs/evm-bytecode.md Normal file
View File

@ -0,0 +1,84 @@
# Offline EVM bytecode
`inspect_evm_interface` / `inspect-evm-interface` infers interface candidates
from one explicitly selected local bytecode carrier. The capability is general:
contract investigation, compatibility, audit and CTF showcases use the same
portable primitive. It does not require an active native binary target.
```sh
rea inspect-evm-interface ./runtime.hex hex --json
rea inspect-evm-interface ./runtime.bin raw --json
```
```json
{
"name": "inspect_evm_interface",
"arguments": { "path": "/artifacts/runtime.hex", "encoding": "hex" }
}
```
Initial real verification covers flat legacy bytecode on Linux x64 and unchanged
bundled EVMole 0.9.3. EOF-style EF00 containers return `unsupported_target`,
with the selected path and format reason. Provider/configuration failures retain
their distinct unavailable-provider or capability classification.
The host must already provide util-linux `prlimit`, normally `/usr/bin/prlimit`;
`REA_EVM_PRLIMIT_COMMAND` can select another absolute executable. REA installs
no system tools and changes no user configuration. [Upstream provenance and
license](../third_party/evmole/README.md) are packaged.
## Evidence semantics
- `encoding` is required: raw bytes or UTF-8 hex, with optional `0x` prefix and
outer ASCII whitespace. Odd hex, interior separators, BOM, non-ASCII whitespace
and invalid UTF-8 are rejected. Empty bytecode is valid and has unknown coverage.
- Carrier SHA-256 identifies the exact original file, including prefix/casing
and whitespace. Decoded-byte SHA-256 identifies actual selected EVM bytes.
Neither digest is the Ethereum Keccak code hash.
- Full bytecode, upstream function offsets and original producer representation
are returned inline. Offsets are byte positions in decoded bytecode.
- Upstream also returns terminal metadata during selector recovery. Its reported
values remain in `raw_result`; integer precision and non-text-key coverage are
unknown in the JS binding. Full bytecode retains the original encoded metadata.
- Selectors, argument strings and state mutability are **inferences**. Function
names/signatures are not looked up. Optimized code may produce different
mutability candidates; recovered types are not a verified source ABI.
- Runtime/init-code identity, deployed authenticity, hardfork and discovery
completeness remain unknown. Empty candidates do not prove no callable interface.
- Analysis is offline: no target runtime execution, implicit chain/RPC/explorer
lookup, wallet action, transaction broadcast or network request.
## Resource and lifecycle limits
Inputs are limited to 4 MiB, complete replies to 16 MiB and combined diagnostic
output to 1 MiB. An owned worker has a 256 MiB JavaScript heap, 3 GiB virtual
address-space limit, 30 CPU seconds and a 30-second command deadline. Virtual
address space differs from resident memory. The worker retains inherited tighter
soft/hard limits, reports its configured soft limits inline and leaves hard limits
unchanged. WASM trap reservations are disabled
so the engine can run within that virtual limit; actual memory is not equated to
its 3 GiB allowance. Limits fail without partial success. Stable snapshots,
private files and owned processes are cleaned independently of caller cancellation.
Invalid-input, unsupported, cancellation, timeout and malformed-reply failures
retain bounded stdout/stderr under `details.captured_output`, including whether
diagnostic output was truncated. Cleanup failures preserve the
original error projection and any captured output.
Successful results expose `diagnostics.truncated` from the supervisor as well.
Excess diagnostic output fails the complete-output contract even when the worker
has written a valid reply.
SIGXCPU retains a CPU resource diagnostic and CPU-specific guidance. Observed
file-size write failures (EFBIG) and SIGXFSZ likewise retain a file-size resource
diagnostic. Exact signal/write causes remain unknown. Configured
soft limits are reported separately from unknown effective limits; the received
signal alone does not establish its exact cause.
Reserved file-size exits require a matching private marker written by the
worker's EFBIG branch. Bare launcher exits or unwritable markers retain process
diagnostics with the resource cause unverified.
Source/metadata, proxy resolution, storage/CFG and fork-explicit execution/traces
remain separate increments under #972, with materially different result or
execution authority. This primitive makes no deployment or runtime behavior claim.
The real verification lane traces representative raw/hex CLI workflows and checks
that only declared Node/prlimit launchers and process ownership inspection run.
Socket traces permit local stdio socket pairs and reject attempted network requests.
These observations verify those fixtures; they are not a general network sandbox.

View File

@ -7,10 +7,10 @@
"client": "2.3.1",
"core": "2.3.1"
},
"skill_version": "31"
"skill_version": "32"
},
"tools": {
"total": 137,
"total": 138,
"families": [
{
"id": "direct",
@ -100,12 +100,13 @@
{
"id": "artifact",
"surface": "artifact-provider",
"count": 6,
"count": 7,
"tools": [
"decode_interface_builder",
"extract_artifact",
"inspect_artifact",
"inspect_asset_catalog",
"inspect_evm_interface",
"inspect_keyed_archive",
"trace_dylib_resolution"
]
@ -239,6 +240,12 @@
"version": "3.1.0",
"capabilities": ["inspect_firmware_regions"]
},
{
"id": "evmole-interface",
"name": "REA EVM interface adapter",
"version": "evmole@0.9.3",
"capabilities": ["inspect_evm_interface"]
},
{
"id": "ghidra",
"name": "Ghidra",
@ -594,7 +601,7 @@
}
],
"cli": {
"primary_count": 92,
"primary_count": 93,
"commands": [
"address-to-file-offset",
"analyze",
@ -638,6 +645,7 @@
"inspect-asset-catalog",
"inspect-binary-layout",
"inspect-electron-page",
"inspect-evm-interface",
"inspect-firmware-regions",
"inspect-keyed-archive",
"inspect-macho",
@ -698,15 +706,15 @@
},
"runtime_catalog": {
"counts": {
"cli_commands": 92,
"mcp_tools": 137,
"cli_commands": 93,
"mcp_tools": 138,
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "1f9455a546d76d63e9ceba7bfdc6045f6abde8ead8492c55bd0cdfa726061a91",
"tools_sha256": "61b501f81acddb52ea80408c936ceec19b73e08331f8fdd05eb503708373e0fa",
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
"combined_sha256": "ce23d159fbfc07b338a5e2d51569a71597842f0cf3a3c09986c799512752ab02",
"providers_sha256": "6a96ebafaa7c9284661db6c2036524dcba0407e85278c6bb5ac12bbad0d85a6f"
"combined_sha256": "8ed2bd4b71684e3e391f909841edf32b68da20eede1f46f0dd6081e235644260",
"providers_sha256": "44935c127fd040af492f2da591dfcd782d766770d306b91242216674220047ed"
}
}
}

View File

@ -643,6 +643,16 @@ the ordinary lane. Its large MCP transfer is opt-in with
explicit 256 MiB SDK receive buffer and five-minute request timeout. Ordinary
MCP fixtures retain the pinned SDK defaults.
### Offline EVM interface
`npm run verify:evm:interface` requires Linux x64, an absolute
`REA_VERIFY_STRACE_COMMAND`, caller-supplied util-linux `prlimit` and `REA_VERIFY_SOLC_MODULE` selecting the absolute module path for
solc 0.8.30. It compiles source-owned plain/optimized/via-IR Cancun fixtures in
private storage and checks actual CLI/MCP selector evidence, raw/hex identity,
unknowns, malformed carriers and independent cleanup. An optional positional
entrypoint verifies a fresh installed package. It acquires no engine, compiler
or chain dependency and does not execute a contract on a chain.
## Agent evaluation and conformance records
Evaluate native, JavaScript, managed and browser investigation tasks through a

View File

@ -1,5 +1,5 @@
{
"manifest_id": "ecm_2821fc2b686037a57ed7e5847a3927cc95b598d222a54983b34d87402e18bd30",
"manifest_id": "ecm_366b46bcafd1815cf76b4b9dd1799e2230a32692a5d1ebb5de33d1673f7a9264",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "21157bdb015b42cf2fafc15fe150605ca89cc564761bb24e87b5bd6a00da7f19"
"sha256": "2428bbffffdabfe27409cb1f61a4acf062aa11545629f1a1df4dc72c9db364a8"
}
],
"claims": [

13
package-lock.json generated
View File

@ -25,6 +25,7 @@
"canonicalize": "3.0.0",
"cross-spawn": "7.0.6",
"diff": "9.0.0",
"evmole": "0.9.3",
"har-schema": "2.0.0",
"ignore": "7.0.6",
"incur": "0.6.1",
@ -4643,6 +4644,18 @@
"node": ">=18.0.0"
}
},
"node_modules/evmole": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/evmole/-/evmole-0.9.3.tgz",
"integrity": "sha512-UhO5HMAoEPCupSxvBJPlm/L0Qk/KVJ/OJo3r4EyhXADIEHqU5aTkAzsBfzk4lXGjqxVnaYbh1GPSmZtmSHFnQQ==",
"license": "MIT",
"bin": {
"evmole": "dist/evmole_cli.mjs"
},
"engines": {
"node": ">=22"
}
},
"node_modules/expect-type": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",

View File

@ -66,7 +66,9 @@
"scripts/verify-windows-native-artifact.mjs",
"bridge/pwntools/layout.py",
"bridge/pwntools/layout_impl.py",
"third_party/pwntools/README.md"
"third_party/pwntools/README.md",
"third_party/evmole/README.md",
"third_party/evmole/LICENSE"
],
"scripts": {
"deps:check": "node scripts/check-dependency-install.mjs",
@ -199,7 +201,8 @@
"verify:browser:source-maps": "npm run build:cached && node scripts/verify-browser-source-maps.mjs",
"verify:browser:runtime": "npm run build:cached && node scripts/verify-browser-runtime.mjs",
"verify:web:network-captures": "npm run build:cached && node scripts/verify-web-network-captures.mjs",
"verify:binary:layout": "npm run build:cached && node scripts/verify-binary-layout.mjs"
"verify:binary:layout": "npm run build:cached && node scripts/verify-binary-layout.mjs",
"verify:evm:interface": "npm run build:cached && node scripts/verify-evm-interface.mjs"
},
"lint-staged": {
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
@ -239,7 +242,8 @@
"smol-toml": "1.9.0",
"write-file-atomic": "7.0.0",
"ws": "8.21.0",
"zod": "4.4.3"
"zod": "4.4.3",
"evmole": "0.9.3"
},
"overrides": {
"vitepress": {

View File

@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
// Public source-owned fixture; generated runtime bytecode is never tracked.
contract InterfaceFixture {
uint256 public stored;
function set(uint256 value) external { stored = value; }
function read() external view returns (uint256) { return stored; }
function pair(uint32 a, address b) external pure returns (uint32, address) { return (a, b); }
receive() external payable {}
fallback() external {}
}

View File

@ -24,7 +24,10 @@ export const toolFamilyCatalog = (sources) => {
{
id: "artifact",
surface: "artifact-provider",
contracts: sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS,
contracts: [
...sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS,
...sources.evmContracts.EVM_TOOL_CONTRACTS,
],
},
{
id: "managed",
@ -151,6 +154,10 @@ export const providerCatalog = (sources) => {
contracts:
sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS,
},
{
identity: sources.evmProvider.EVMOLE_PROVIDER_IDENTITY,
contracts: sources.evmContracts.EVM_TOOL_CONTRACTS,
},
{
identity: sources.artifactProviders.ARTIFACT_GRAPH_PROVIDER,
contracts: sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS,

View File

@ -44,6 +44,8 @@ const SOURCE_PATHS = {
"dist/contracts/native/binaryDiagnosticsToolContracts.js",
pwntoolsProvider: "dist/native/pwntools/PwntoolsRelease.js",
nativeContracts: "dist/contracts/native/nativeToolContracts.js",
evmContracts: "dist/contracts/evm/evmToolContracts.js",
evmProvider: "dist/evm/EvmoleRelease.js",
artifactContracts: "dist/contracts/artifactToolContracts.js",
managedContracts: "dist/contracts/managed/managedToolContracts.js",
firmwareContracts: "dist/contracts/firmware/firmwareToolContracts.js",

View File

@ -5,6 +5,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
const PURE_LAYERS = new Set(["domain", "contracts"]);
const MIGRATED_PROVIDER_ROOTS = new Set([
"evm",
"android",
"firmware",
"ghidra",

View File

@ -0,0 +1,577 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { access, readFile, readdir, writeFile } from "node:fs/promises";
import { constants } from "node:fs";
import { isAbsolute, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { PrivateRuntimeRoot } from "../dist/process/PrivateRuntimeRoot.js";
import { parseEvidence } from "../dist/domain/evidence.js";
import { mcpTextValue } from "./lib/mcp-verifier-results.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
const solcModule = process.env.REA_VERIFY_SOLC_MODULE;
const strace = process.env.REA_VERIFY_STRACE_COMMAND;
if (
process.platform !== "linux" ||
process.arch !== "x64" ||
!isAbsolute(solcModule ?? "") ||
!isAbsolute(strace ?? "")
)
throw new Error(
"verify:evm:interface requires Linux x64, caller-supplied util-linux prlimit and absolute REA_VERIFY_SOLC_MODULE pointing to unchanged solc 0.8.30 and absolute REA_VERIFY_STRACE_COMMAND. No target or chain is executed.",
);
try {
await access(
process.env.REA_EVM_PRLIMIT_COMMAND ?? "/usr/bin/prlimit",
constants.X_OK,
);
await access(solcModule, constants.R_OK);
await access(strace, constants.X_OK);
} catch (cause) {
throw new Error(
"verify:evm:interface prerequisite unavailable: selected solc module, strace or util-linux prlimit",
{ cause },
);
}
const solc = (await import(pathToFileURL(solcModule).href)).default;
assert.equal(solc.version(), "0.8.30+commit.73712a01.Emscripten.clang");
const source = await readFile(
new URL("./fixtures/evm-interface.sol", import.meta.url),
"utf8",
);
const entrypoint =
process.argv[2] ?? fileURLToPath(new URL("./rea.mjs", import.meta.url));
const execute = promisify(execFile);
const environment = Object.fromEntries(
Object.entries(process.env).filter(([, value]) => typeof value === "string"),
);
const run = createVerifierRun();
const root = await PrivateRuntimeRoot.create({ prefix: "rea-evm-verifier-" });
const client = new Client({ name: "evm-interface-verifier", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env: environment,
stderr: "pipe",
});
const failures = [];
let cases = 0;
try {
await client.connect(transport);
const tools = await client.listTools();
assert.ok(tools.tools.some(({ name }) => name === "inspect_evm_interface"));
const inspect = async (
mode,
path,
encoding,
expectedCategory,
scope = { command: process.execPath, prefix: [], client },
) => {
let value;
if (mode === "cli") {
let stdout;
try {
({ stdout } = await execute(
scope.command,
[
...scope.prefix,
entrypoint,
"inspect-evm-interface",
path,
encoding,
"--json",
],
{
env: scope.environment ?? environment,
timeout: 45_000,
maxBuffer: 32 * 1024 * 1024,
},
));
} catch (cause) {
if (typeof cause.code !== "number" || typeof cause.stdout !== "string")
throw cause;
stdout = cause.stdout;
}
value = JSON.parse(stdout);
} else {
const reply = await scope.client.callTool({
name: "inspect_evm_interface",
arguments: { path, encoding },
});
value = JSON.parse(mcpTextValue(reply));
if (expectedCategory !== undefined) {
assert.equal(reply.isError, true);
value = value.error;
} else assert.notEqual(reply.isError, true, mcpTextValue(reply));
}
if (expectedCategory !== undefined) {
assert.equal(value.category, expectedCategory, JSON.stringify(value));
return value;
}
assert.equal(
value.category,
undefined,
`Unexpected public failure: ${JSON.stringify(value)}`,
);
const evidence = parseEvidence(mode === "cli" ? value : value.evidence);
const report = evidence.normalized_result;
assert.equal(report.diagnostics.truncated, false);
assert.equal(evidence.provider.version, "evmole@0.9.3");
assert.equal(evidence.confidence, "inferred");
assert.equal(report.artifact.path, path);
assert.equal(report.artifact.encoding, encoding);
const carrier = await readFile(path);
assert.equal(
report.artifact.sha256,
createHash("sha256").update(carrier).digest("hex"),
);
assert.equal(evidence.subject.digest.sha256, report.artifact.sha256);
assert.equal(report.artifact.bytes, carrier.length);
assert.equal(
report.bytecode.sha256,
createHash("sha256")
.update(Buffer.from(report.bytecode.hex, "hex"))
.digest("hex"),
);
assert.equal(report.bytecode.bytes, report.bytecode.hex.length / 2);
for (const key of ["kind", "hardfork", "deployment_authenticity"])
assert.equal(report.bytecode[key], "unknown");
assert.equal(report.discovery_completeness, "unknown");
assert.equal(report.runtime_execution, "not-performed");
assert.deepEqual(
report.functions.map((fn) => ({
selector: fn.selector.slice(2),
bytecodeOffset: fn.bytecode_offset,
dispatch: fn.dispatch,
...(fn.inferred_arguments === null
? {}
: { arguments: fn.inferred_arguments }),
...(fn.inferred_state_mutability === null
? {}
: { stateMutability: fn.inferred_state_mutability }),
})),
evidence.raw_result.functions ?? [],
);
if (mode === "mcp") assert.deepEqual(value.result, report);
return report;
};
for (const [name, optimizer, viaIR] of [
["plain", false, false],
["optimized", true, false],
["via-ir", true, true],
]) {
const output = JSON.parse(
solc.compile(
JSON.stringify({
language: "Solidity",
sources: { "Interface.sol": { content: source } },
settings: {
optimizer: { enabled: optimizer, runs: 200 },
viaIR,
evmVersion: "cancun",
outputSelection: {
"*": {
"*": [
"evm.deployedBytecode.object",
"evm.methodIdentifiers",
"abi",
],
},
},
},
}),
),
);
assert.ok(
!output.errors?.some(({ severity }) => severity === "error"),
JSON.stringify(output.errors),
);
const artifact = output.contracts["Interface.sol"].InterfaceFixture;
const hex = artifact.evm.deployedBytecode.object;
const expected = Object.values(artifact.evm.methodIdentifiers)
.map((selector) => "0x" + selector)
.sort();
const forms = [
["hex", ` \t0x${hex.toUpperCase()}\r\n`],
["raw", Buffer.from(hex, "hex")],
];
for (const [encoding, data] of forms) {
const path = join(root.path, `${name}.${encoding}`);
await writeFile(path, data);
const original = await readFile(path);
for (const mode of ["cli", "mcp"]) {
const report = await inspect(mode, path, encoding);
assert.equal(report.bytecode.hex, hex);
assert.deepEqual(
report.functions
.filter(({ dispatch }) => dispatch === "abi")
.map(({ selector }) => selector)
.sort(),
expected,
);
assert.ok(
report.functions.every(
({ bytecode_offset }) => bytecode_offset < report.bytecode.bytes,
),
);
const set = report.functions.find(
({ selector }) =>
selector === "0x" + artifact.evm.methodIdentifiers["set(uint256)"],
);
assert.equal(set.inferred_arguments, "uint256");
assert.equal(set.inferred_state_mutability, "nonpayable");
cases++;
}
assert.deepEqual(await readFile(path), original);
}
}
for (const [name, data, encoding] of [
["empty", "0x", "hex"],
["empty-raw", Buffer.alloc(0), "raw"],
["incomplete-push", Buffer.from([0x7f]), "raw"],
["no-dispatch", "60006000f3", "hex"],
]) {
const path = join(root.path, name);
await writeFile(path, data);
for (const mode of ["cli", "mcp"]) {
const report = await inspect(mode, path, encoding);
assert.equal(report.functions.length, 0);
cases++;
}
}
for (const [name, hex, expected] of [
["zero-selector", "60003560e01c630000000014601057005b00", ["0x00000000"]],
["push-immediate", "63123456785060006000f3", []],
]) {
const path = join(root.path, name);
await writeFile(path, hex);
for (const mode of ["cli", "mcp"]) {
const report = await inspect(mode, path, "hex");
assert.deepEqual(
report.functions.map(({ selector }) => selector),
expected,
);
cases++;
}
}
for (const [encoding, data] of [
["raw", Buffer.from("ef0001", "hex")],
["hex", "0xef0001"],
]) {
const path = join(root.path, "eof." + encoding);
await writeFile(path, data);
for (const mode of ["cli", "mcp"]) {
const failure = await inspect(mode, path, encoding, "unsupported_target");
assert.equal(failure.code, "unsupported_target");
assert.equal(failure.details.path, path);
assert.match(failure.details.reason, /EF00/);
assert.match(failure.remediation.action, /target format/);
cases++;
}
}
for (const [name, data] of [
["odd", "0x1"],
["invalid-hex", "xyz"],
["interior-space", "60 00"],
["bom", "\uFEFF6000"],
["utf8", Buffer.from([0xff])],
]) {
const path = join(root.path, name);
await writeFile(path, data);
for (const mode of ["cli", "mcp"]) {
await inspect(mode, path, "hex", "invalid_input");
cases++;
}
}
for (const mode of ["cli", "mcp"]) {
await inspect(mode, join(root.path, "absent"), "raw", "invalid_input");
cases++;
}
for (const [option, expected] of [
["--as=2147483648", "address_space_bytes=2147483648"],
["--cpu=20", "cpu_seconds=20"],
["--fsize=8388608", "file_size_bytes=8388608"],
]) {
const limiter = environment.REA_EVM_PRLIMIT_COMMAND ?? "/usr/bin/prlimit";
const prefix = [
option,
"--",
process.execPath,
"--disable-wasm-trap-handler",
];
const limitedClient = new Client({
name: "limited-evm-verifier",
version: "1",
});
const limitedTransport = new StdioClientTransport({
command: limiter,
args: [...prefix, entrypoint, "mcp"],
env: environment,
stderr: "pipe",
});
try {
await limitedClient.connect(limitedTransport);
const scope = { command: limiter, prefix, client: limitedClient };
for (const mode of ["cli", "mcp"]) {
const report = await inspect(
mode,
join(root.path, "zero-selector"),
"hex",
undefined,
scope,
);
assert.ok(report.limitations.some((item) => item.includes(expected)));
cases++;
}
} finally {
try {
await limitedClient.close();
} finally {
await limitedTransport.close();
}
}
}
const brokenLimiter = join(root.path, "broken-limiter");
await writeFile(brokenLimiter, "#!/rea-verifier-missing-interpreter\n", {
mode: 0o700,
});
const signalLimiter = join(root.path, "signal-limiter");
await writeFile(signalLimiter, '#!/bin/sh\nulimit -c 0\nkill -XCPU "$$"\n', {
mode: 0o700,
});
const fileLimiter = join(root.path, "file-size-limiter");
const unmarkedLimiter = join(root.path, "unmarked-file-size-limiter");
await writeFile(
unmarkedLimiter,
'#!/bin/sh\nprintf "launcher exit 76\\n" >&2\nexit 76\n',
{ mode: 0o700 },
);
// Fixture-only launcher: run the actual Node/WASM worker with a one-byte
// file allowance, keeping the usual address-space, CPU and core budgets.
await writeFile(
fileLimiter,
'#!/bin/sh\nwhile [ "$#" -gt 0 ] && [ "$1" != "--" ]; do shift; done\n[ "$#" -gt 0 ] || exit 64\nshift\nexec /usr/bin/prlimit --as=3221225472: --cpu=30: --fsize=1: --core=0: -- "$@"\n',
{ mode: 0o700 },
);
for (const [limiter, category, resource] of [
[root.path, "unsupported_provider"],
[brokenLimiter, "unsupported_provider"],
[signalLimiter, "resource_constraint", "cpu"],
[fileLimiter, "resource_constraint", "file-size"],
[unmarkedLimiter, "execution_failure"],
]) {
const selectedEnvironment = {
...environment,
REA_EVM_PRLIMIT_COMMAND: limiter,
};
const selectedClient = new Client({
name: "unavailable-evm-limiter-verifier",
version: "1",
});
const selectedTransport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env: selectedEnvironment,
stderr: "pipe",
});
try {
await selectedClient.connect(selectedTransport);
const scope = {
command: process.execPath,
prefix: [],
client: selectedClient,
environment: selectedEnvironment,
};
for (const mode of ["cli", "mcp"]) {
const error = await inspect(
mode,
join(root.path, "zero-selector"),
"hex",
category,
scope,
);
if (category === "resource_constraint") {
assert.equal(error.code, "resource_constraint");
assert.equal(error.details.resource, resource);
assert.equal(
error.details.reported_limits.effective_soft_limits,
null,
);
assert.equal(
typeof error.details.reported_limits.configured_soft_limits
.cpu_seconds,
"number",
);
assert.ok(
JSON.stringify(error).includes(
resource === "cpu" ? "SIGXCPU" : "EFBIG",
),
);
} else if (category === "execution_failure") {
assert.equal(error.code, "execution_failure");
assert.ok(JSON.stringify(error).includes("launcher exit 76"));
assert.ok(JSON.stringify(error).includes("file_size_failure_marker"));
} else {
assert.equal(error.code, "capability_unavailable");
assert.ok(JSON.stringify(error).includes(limiter));
}
cases++;
}
} finally {
try {
await selectedClient.close();
} finally {
await selectedTransport.close();
}
}
}
for (const [name, encoding] of [
["plain.hex", "hex"],
["via-ir.raw", "raw"],
]) {
await execute(
strace,
[
"-ff",
"-e",
"trace=network,execve,execveat",
"-s",
"4096",
"-o",
join(root.path, `offline.trace.${name}`),
process.execPath,
entrypoint,
"inspect-evm-interface",
join(root.path, name),
encoding,
"--json",
],
{
env: { ...environment, PATH: "/usr/bin:/bin" },
timeout: 45_000,
maxBuffer: 32 * 1024 * 1024,
},
);
}
const observedExecutables = new Set();
for (const file of await readdir(root.path)) {
if (!file.startsWith("offline.trace.")) continue;
const localSocketDescriptors = new Set();
for (const line of (await readFile(join(root.path, file), "utf8")).split(
"\n",
)) {
if (/execve(?:at)?\(/.test(line)) {
const match = /^execve\("([^"]+)"/.exec(line);
assert.notEqual(match, null, `Unresolved executable identity: ${line}`);
assert.ok(
line.endsWith(" = 0") || / = -1 [A-Z]+/.test(line),
`Incomplete exec observation: ${line}`,
);
const ownershipInspection =
["/usr/bin/ps", "/bin/ps"].includes(match[1]) &&
line.includes('["ps", "-axo", "pid=,ppid=,pgid=,stat=,command="]');
assert.ok(
[
process.execPath,
environment.REA_EVM_PRLIMIT_COMMAND ?? "/usr/bin/prlimit",
].includes(match[1]) || ownershipInspection,
`Unexpected host execution: ${line}`,
);
observedExecutables.add(match[1]);
} else if (/^[a-z]+\(/.test(line)) {
assert.ok(
permitsStdioSocketObservation(line, localSocketDescriptors),
`Unexpected attempted network operation: ${line}`,
);
}
}
}
assert.ok(observedExecutables.has(process.execPath));
assert.ok(
observedExecutables.has(
environment.REA_EVM_PRLIMIT_COMMAND ?? "/usr/bin/prlimit",
),
);
cases += 2;
} catch (cause) {
failures.push(cause);
} finally {
for (const close of [
() => client.close(),
() => transport.close(),
() => root.close(),
]) {
try {
await close();
} catch (cause) {
failures.push(cause);
}
}
}
const verifier = await completeVerifierRun(run);
if (
verifier.process_lineage.status !== "verified" ||
verifier.process_lineage.descendants.length !== 0
)
failures.push(
new Error(
"Verifier process lineage was not fully released: " +
JSON.stringify(verifier),
),
);
console.log(
JSON.stringify(
{
status: failures.length === 0 ? "passed" : "failed",
public_cases: cases,
compiler: solc.version(),
engine: "evmole@0.9.3",
...(failures.length === 0
? {
target_execution: "exec-syscalls-verified-absent",
network_requests: "socket-syscalls-verified-absent",
}
: {}),
verifier,
},
null,
2,
),
);
if (failures.length !== 0)
throw new AggregateError(
failures,
"Offline EVM interface verification failed; cleanup failures retained.",
);
function permitsStdioSocketObservation(line, localDescriptors) {
if (line.startsWith("socketpair(AF_UNIX,")) {
const pair = /\[(\d+), (\d+)\]\)\s+= 0$/.exec(line);
if (pair !== null) {
localDescriptors.add(pair[1]);
localDescriptors.add(pair[2]);
}
return true;
}
const name = /^getsockname\((\d+), \{sa_family=AF_UNIX\}/.exec(line);
if (name !== null) {
localDescriptors.add(name[1]);
return true;
}
// A failed descriptor query performs no request and identifies no socket.
if (line.startsWith("getsockname(") && / = -1 ENOTSOCK/.test(line))
return true;
const metadata =
/^(?:getsockopt\((\d+), SOL_SOCKET, SO_TYPE,|setsockopt\((\d+), SOL_SOCKET, SO_(?:RCVBUF|SNDBUF),|shutdown\((\d+), SHUT_WR\))/.exec(
line,
);
const descriptor = metadata?.[1] ?? metadata?.[2] ?? metadata?.[3];
// Allow descriptor queries/buffer configuration only after observing a local
// stdio socket. Other socket creation, connection and data operations fail.
return descriptor !== undefined && localDescriptors.has(descriptor);
}

View File

@ -2,9 +2,9 @@
name: reverse-engineer-anything
description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
metadata:
version: "31"
tool_count: 137
catalog_digest: "ce23d159fbfc07b338a5e2d51569a71597842f0cf3a3c09986c799512752ab02"
version: "32"
tool_count: 138
catalog_digest: "8ed2bd4b71684e3e391f909841edf32b68da20eede1f46f0dd6081e235644260"
---
# REA
@ -99,6 +99,11 @@ explicit path or endpoint and do not need it.
[historical captures](https://github.com/morluto/rea/blob/main/docs/web-network-captures.md) for the exact upstream
profile and credential exclusions.
- User-owned Electron runtime already open: `list_electron_targets`.
- Explicit local EVM bytecode carrier: `inspect_evm_interface` with caller-selected
`raw` or `hex` encoding. Preserve carrier/decoded digests and treat selectors,
argument strings and mutability as inferred candidates; this performs no chain
lookup or target execution. See the
[EVM bytecode guide](https://github.com/morluto/rea/blob/main/docs/evm-bytecode.md).
- Explicit Linux ELF file for offline layout, symbols, relocations or static
mitigation evidence: `inspect_binary_layout`. This target-free operation uses
caller-supplied pwntools without opening a disassembler database. Preserve its

View File

@ -31,6 +31,7 @@ type ToolAvailabilityReason = "available" | ToolUnavailabilityReason;
type ProviderDescriptor = ProviderCapability;
export type AvailabilityPolicy = {
readonly evmInterfaceEnabled?: boolean;
readonly processCaptureEnabled: boolean;
readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures;
readonly binaryLayoutEnabled?: boolean;
@ -289,6 +290,14 @@ const workflowAvailabilityFor = ({
remediation:
"On Linux x64, set absolute REA_PWNTOOLS_PYTHON to caller-supplied Python with pwntools 4.15.0. No active binary target is required.",
};
if (name === "inspect_evm_interface")
return policy.evmInterfaceEnabled === true
? { reason: "available", remediation: null }
: {
reason: "provider_missing",
remediation:
"The bundled offline EVM interface profile is currently real-verified on Linux x64. No active binary target or chain endpoint is required.",
};
if (kind === "firmware-provider") {
const enabled =
name === "inspect_firmware_regions"

View File

@ -0,0 +1,15 @@
import type {
AnalysisExecution,
ExecutionOptions,
} from "../AnalysisProvider.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type { InspectEvmInterfaceInput } from "../../domain/evm/evmInterface.js";
import type { Result } from "../../domain/result.js";
/** Replaceable offline interface-recovery engine, independent of wallets and chain clients. */
export interface EvmInterfacePort {
inspect(
input: InspectEvmInterfaceInput,
options?: ExecutionOptions,
): Promise<Result<AnalysisExecution, AnalysisError>>;
}

View File

@ -0,0 +1,124 @@
import { expect, it } from "vitest";
import { EvmInterfaceService } from "./EvmInterfaceService.js";
import {
evmInterfaceExecution,
evmInterfaceFixture,
} from "../../../tests/fixtures/evm/interface.js";
import { err, ok } from "../../domain/result.js";
import { parseEvidence } from "../../domain/evidence.js";
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
it("returns full inline inferred Evidence and unmodified upstream representation", async () => {
const value = evmInterfaceFixture();
const service = new EvmInterfaceService({
inspect: () => Promise.resolve(ok(evmInterfaceExecution(value))),
});
const result = await service.inspect({
path: value.artifact.path,
encoding: "hex",
});
if (!result.ok) throw result.error;
const evidence = parseEvidence(result.value);
expect(evidence.normalized_result).toEqual(value);
expect(evidence.raw_result).toEqual({ functions: [] });
expect(evidence.confidence).toBe("inferred");
expect(evidence.subject?.digest.sha256).toBe(value.artifact.sha256);
});
it.each([
{ path: "relative.hex", encoding: "hex" },
{ path: "/selected", encoding: "auto" },
{ path: "/selected" },
{ path: "/selected", encoding: "raw", approval: true },
])("rejects invalid selection before effects: %j", async (input) => {
const service = new EvmInterfaceService({
inspect: () => {
throw new Error("must not start");
},
});
expect(await service.inspect(input)).toMatchObject({
ok: false,
error: { _tag: "AnalysisInputError" },
});
});
it.each(["carrier", "encoding", "digest", "no-subject"])(
"rejects mismatched producer binding: %s",
async (problem) => {
const value = evmInterfaceFixture();
if (problem === "carrier") value.artifact.path = "/other";
if (problem === "encoding") value.artifact.encoding = "raw";
const observed = evmInterfaceExecution(value);
const execution = {
...observed,
subject:
problem === "no-subject"
? null
: {
path: "/artifacts/source-owned.hex",
sha256:
problem === "digest" ? "b".repeat(64) : value.artifact.sha256,
format: "file" as const,
},
};
const service = new EvmInterfaceService({
inspect: () => Promise.resolve(ok(execution)),
});
expect(
await service.inspect({
path: "/artifacts/source-owned.hex",
encoding: "hex",
}),
).toMatchObject({
ok: false,
error: { _tag: "AnalysisOutputError", capturedOutput: value.diagnostics },
});
},
);
it("preserves meaningful provider failure and cancellation before/after execution", async () => {
const failure = new AnalysisCapabilityUnavailableError(
"evm",
"inspect_evm_interface",
"Selected limiter missing",
);
const service = new EvmInterfaceService({
inspect: () => Promise.resolve(err(failure)),
});
expect(await service.inspect({ path: "/code", encoding: "raw" })).toEqual(
err(failure),
);
const controller = new AbortController();
const observed = evmInterfaceFixture();
observed.diagnostics = {
stdout: "selected\u0000",
stderr: "warning",
truncated: true,
};
const cancelled = new EvmInterfaceService({
inspect: () => {
controller.abort();
return Promise.resolve(ok(evmInterfaceExecution(observed)));
},
});
expect(
await cancelled.inspect(
{ path: "/artifacts/source-owned.hex", encoding: "hex" },
{ signal: controller.signal },
),
).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCancelledError",
capturedOutput: observed.diagnostics,
},
});
const untouched = new EvmInterfaceService({
inspect: () => {
throw new Error("must not start");
},
});
expect(
await untouched.inspect(
{ path: "/code", encoding: "raw" },
{ signal: controller.signal },
),
).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } });
});

View File

@ -0,0 +1,92 @@
import { isAbsolute } from "node:path";
import { z } from "zod";
import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { EvmInterfacePort } from "./EvmInterfacePort.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import {
inspectEvmInterfaceInputSchema,
evmInterfaceSchema,
} from "../../domain/evm/evmInterface.js";
import { projectInputIssues } from "../../domain/inputIssueProjection.js";
import { err, ok, type Result } from "../../domain/result.js";
const OPERATION = "inspect_evm_interface";
const diagnosticsSchema = z.object({
diagnostics: evmInterfaceSchema.shape.diagnostics,
});
/** Shared carrier validation and typed inline Evidence for CLI/MCP. */
export class EvmInterfaceService {
constructor(readonly provider: EvmInterfacePort) {}
/** Preserve selected byte identity while clearly labeling recovered ABI candidates as inferences. */
async inspect(
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION));
const input = inspectEvmInterfaceInputSchema.safeParse(rawInput);
if (!input.success)
return err(
new AnalysisInputError(
OPERATION,
{ cause: input.error },
projectInputIssues(input.error.issues, rawInput),
),
);
if (!isAbsolute(input.data.path))
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["path"],
reason: "invalid_format",
message: "Expected an absolute filesystem path on this host.",
},
]),
);
const inspected = await this.provider.inspect(input.data, options);
if (!inspected.ok) return inspected;
const diagnostics = diagnosticsSchema.safeParse(inspected.value.result);
const outputOptions = diagnostics.success
? { capturedOutput: diagnostics.data.diagnostics }
: undefined;
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION, outputOptions));
const execution = inspected.value;
const report = evmInterfaceSchema.safeParse(execution.result);
if (
!report.success ||
report.data.artifact.path !== input.data.path ||
report.data.artifact.encoding !== input.data.encoding ||
execution.subject?.path !== input.data.path ||
execution.subject.sha256 !== report.data.artifact.sha256
)
return err(
new AnalysisOutputError(
OPERATION,
"Interface adapter returned malformed data or changed the selected carrier identity/encoding.",
outputOptions,
),
);
return ok(
createEvidence(execution.subject, execution.provider, {
operation: OPERATION,
parameters: input.data,
result: report.data,
...(execution.rawResult === null
? {}
: { rawResult: execution.rawResult }),
confidence: "inferred",
locations: execution.locations,
limitations: execution.limitations,
}),
);
}
}

View File

@ -1,3 +1,4 @@
import { registerEvmCommands } from "./cli/evmCommands.js";
import { registerBinaryDiagnosticsCommands } from "./cli/binaryDiagnosticsCommands.js";
import { Cli } from "incur";
@ -56,6 +57,7 @@ export const createCli = (
registerSetupCommands(cli, logger);
registerCoreAnalysisCommands(cli, logger, resultOutput);
registerUtilityCommands(cli, logger, environment);
registerEvmCommands(cli, logger, environment);
registerArtifactCommands(cli, logger);
registerManagedCommands(cli, logger);
registerAndroidCommands(cli, logger, environment);

37
src/cli/evmCommands.ts Normal file
View File

@ -0,0 +1,37 @@
import { z } from "incur";
import { resolve } from "node:path";
import { createEvmInterfaceService } from "../composition/evm.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { Logger } from "../logger.js";
import type { CliInstance } from "./types.js";
import { withCommandCancellation } from "./commandCancellation.js";
/** Resolve operator paths and preserve explicit encoding through the shared workflow. */
export const registerEvmCommands = (
cli: CliInstance,
logger: Logger,
environment: Readonly<NodeJS.ProcessEnv>,
): void => {
const service = createEvmInterfaceService(environment);
cli.command(CLI_COMMANDS.inspectEvmInterface, {
description: "Infer interface candidates from explicit local EVM bytecode",
args: z.object({
path: z.string().describe("Local bytecode carrier"),
encoding: z
.enum(["raw", "hex"])
.describe("Selected carrier representation"),
}),
run: ({ args }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.inspectEvmInterface, async () => {
const result = await service.inspect(
{ path: resolve(args.path), encoding: args.encoding },
{ signal },
);
return result.ok ? result.value : projectAnalysisError(result.error);
}),
),
});
};

View File

@ -1,5 +1,6 @@
/** Canonical names used by every one-shot CLI command registration. */
export const CLI_COMMANDS = Object.freeze({
inspectEvmInterface: "inspect-evm-interface",
inspectWebNetworkCapture: "inspect-web-network-capture",
observeWebExecution: "observe-web-execution",
inspectWebEventListeners: "inspect-web-event-listeners",

8
src/composition/evm.ts Normal file
View File

@ -0,0 +1,8 @@
import { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js";
import { EvmoleInterfaceProvider } from "../evm/EvmoleInterfaceProvider.js";
/** Compose offline interface analysis without import-time I/O or dependency acquisition. */
export const createEvmInterfaceService = (
environment: Readonly<NodeJS.ProcessEnv> = process.env,
): EvmInterfaceService =>
new EvmInterfaceService(new EvmoleInterfaceProvider(environment));

View File

@ -1,3 +1,4 @@
import { EVM_TOOL_CONTRACTS } from "./evm/evmToolContracts.js";
import { BINARY_DIAGNOSTICS_TOOL_CONTRACTS } from "./native/binaryDiagnosticsToolContracts.js";
import { describe, expect, it } from "vitest";
import { z } from "zod";
@ -72,6 +73,7 @@ describe("tool contract surface", () => {
...NATIVE_TOOL_CONTRACTS,
...BINARY_DIAGNOSTICS_TOOL_CONTRACTS,
...ARTIFACT_TOOL_CONTRACTS,
...EVM_TOOL_CONTRACTS,
...ANDROID_TOOL_CONTRACTS,
...FIRMWARE_TOOL_CONTRACTS,
...MANAGED_TOOL_CONTRACTS,

View File

@ -5,6 +5,7 @@ import { jsonValueSchema } from "../domain/jsonValue.js";
const categorySchema = z.enum([
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
@ -37,6 +38,7 @@ export const analysisErrorProjectionSchema = z.discriminatedUnion("code", [
generic("artifact_changed"),
generic("unreadable_output"),
generic("capability_unavailable"),
generic("unsupported_target"),
generic("provider_unavailable"),
generic("provider_timeout"),
generic("resource_constraint"),

View File

@ -0,0 +1,26 @@
import {
inspectEvmInterfaceInputSchema,
evmInterfaceSchema,
} from "../../domain/evm/evmInterface.js";
import type { ToolContract } from "../toolContractTypes.js";
import { toolContractMetadata } from "../toolEffects.js";
import { evidenceResultOf } from "../toolOutputSchemas.js";
/** Target-free EVM capabilities are independent of archive/Apple artifact providers. */
export const EVM_TOOL_CONTRACTS = [
{
name: "inspect_evm_interface",
...toolContractMetadata("inspect_evm_interface"),
kind: "artifact-provider",
description:
"Inspect an explicit local flat EVM bytecode carrier and infer dispatch selectors, argument strings and mutability without executing the target or accessing a chain/RPC endpoint. Choose raw or hex encoding explicitly. Returns original carrier and decoded-byte SHA-256, full bytecode and interface candidates inline as inferred Evidence; bytecode kind, hardfork, deployed authenticity and discovery completeness remain unknown. Initial real-verified Linux x64 profile uses unchanged bundled EVMole 0.9.3/WASM in a bounded owned worker; EOF-style EF00 containers are unsupported. No signature lookup, wallet action or transaction broadcast.",
inputSchema: inspectEvmInterfaceInputSchema,
outputSchema: evidenceResultOf(evmInterfaceSchema),
examples: [
{
title: "Inspect a local runtime bytecode hex carrier",
input: { path: "/artifacts/runtime.hex", encoding: "hex" },
},
],
},
] as const satisfies readonly ToolContract[];

View File

@ -1,3 +1,4 @@
import { EVM_TOOL_CONTRACTS } from "./evm/evmToolContracts.js";
import { BINARY_DIAGNOSTICS_TOOL_CONTRACTS } from "./native/binaryDiagnosticsToolContracts.js";
import { describe, expect, it } from "vitest";
@ -26,6 +27,7 @@ const GROUPS = {
native: NATIVE_TOOL_CONTRACTS,
binary_diagnostics: BINARY_DIAGNOSTICS_TOOL_CONTRACTS,
artifact: ARTIFACT_TOOL_CONTRACTS,
evm: EVM_TOOL_CONTRACTS,
managed: MANAGED_TOOL_CONTRACTS,
android: ANDROID_TOOL_CONTRACTS,
firmware: FIRMWARE_TOOL_CONTRACTS,

View File

@ -1,3 +1,4 @@
import { EVM_TOOL_CONTRACTS } from "./evm/evmToolContracts.js";
import { BINARY_DIAGNOSTICS_TOOL_CONTRACTS } from "./native/binaryDiagnosticsToolContracts.js";
import { NATIVE_TOOL_CONTRACTS } from "./native/nativeToolContracts.js";
import { ARTIFACT_TOOL_CONTRACTS } from "./artifactToolContracts.js";
@ -26,6 +27,7 @@ export const TOOL_CONTRACTS = [
...NATIVE_TOOL_CONTRACTS,
...BINARY_DIAGNOSTICS_TOOL_CONTRACTS,
...ARTIFACT_TOOL_CONTRACTS,
...EVM_TOOL_CONTRACTS,
...MANAGED_TOOL_CONTRACTS,
...ANDROID_TOOL_CONTRACTS,
...FIRMWARE_TOOL_CONTRACTS,

View File

@ -41,6 +41,11 @@ const sessionEvidence = effects({ mutatesSession: true });
/** Explicit effect audit for every public tool. */
export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
inspect_evm_interface: effects({
mutatesSession: true,
writesFilesystem: true,
launchesProcess: true,
}),
inspect_web_network_capture: effects({
mutatesSession: true,
writesFilesystem: true,

View File

@ -6,6 +6,7 @@ const ANALYSIS_ERROR_TAGS = [
"AnalysisArtifactChangedError",
"AnalysisOutputError",
"AnalysisCapabilityUnavailableError",
"AnalysisUnsupportedTargetError",
"AnalysisCancelledError",
"AnalysisTimeoutError",
"AnalysisResourceConstraintError",

View File

@ -4,6 +4,19 @@ import {
type AnalysisErrorOptions,
} from "./analysisErrorBase.js";
/** A readable supplied artifact lies outside this operation's supported target formats. */
export class AnalysisUnsupportedTargetError extends AnalysisError {
readonly _tag = "AnalysisUnsupportedTargetError";
constructor(
readonly operation: string,
readonly path: string,
readonly reason: string,
options?: AnalysisErrorOptions,
) {
super(`Unsupported target for ${operation} at ${path}: ${reason}`, options);
}
}
/** Provider reported a resource failure, distinct from malformed input or unsupported coverage. */
export class AnalysisResourceConstraintError extends AnalysisError {
readonly _tag = "AnalysisResourceConstraintError";

View File

@ -2,6 +2,7 @@ import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisInputError,
AnalysisUnsupportedTargetError,
AnalysisResourceConstraintError,
} from "./analysisErrorCore.js";
import { ArtifactOperationError } from "./artifactOperationError.js";
@ -33,6 +34,8 @@ import { type AnalysisErrorProjection } from "./analysisErrorProjection.js";
export const analysisErrorRemediationAction = (
error: AnalysisError,
): string => {
if (error instanceof AnalysisUnsupportedTargetError)
return "Select a target supported by this operation or choose an operation supporting the reported target format.";
if (error instanceof AnalysisResourceConstraintError)
return error.resource === "cpu"
? "Review the reported worker CPU limits and observed signal. Retry with sufficient CPU time or a smaller artifact; REA retains tighter inherited limits."
@ -125,6 +128,7 @@ const STATIC_ERROR_CATEGORIES: Readonly<
AnalysisAccessDeniedError: "unavailable",
AnalysisArtifactChangedError: "integrity_mismatch",
AnalysisCapabilityUnavailableError: "unsupported_provider",
AnalysisUnsupportedTargetError: "unsupported_target",
ProviderSelectionError: "unsupported_provider",
EvidenceIntegrityError: "integrity_mismatch",
AnalysisCancelledError: "cancelled",
@ -137,6 +141,7 @@ const STATIC_ERROR_CATEGORIES: Readonly<
};
export const analysisErrorUserMessage = (error: AnalysisError): string => {
if (error instanceof AnalysisUnsupportedTargetError) return error.message;
if (error instanceof AnalysisResourceConstraintError) return error.reason;
if (error instanceof AnalysisAccessDeniedError)
return "Host filesystem permissions denied read access to the selected path.";
@ -270,6 +275,7 @@ const KNOWN_ERROR_TAGS = {
AnalysisArtifactChangedError: true,
AnalysisOutputError: true,
AnalysisCapabilityUnavailableError: true,
AnalysisUnsupportedTargetError: true,
AnalysisCancelledError: true,
AnalysisTimeoutError: true,
AnalysisResourceConstraintError: true,

View File

@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
AnalysisCapabilityUnavailableError,
AnalysisUnsupportedTargetError,
AnalysisInputError,
AnalysisOutputError,
AnalysisCancelledError,
@ -28,6 +29,32 @@ const retainedOutput = {
truncated: true,
};
it("distinguishes unsupported target formats from unavailable provider capabilities", () => {
const projected = projectAnalysisError(
new AnalysisUnsupportedTargetError(
"inspect_evm_interface",
"/selected/runtime.hex",
"EOF-style EF00 container is unsupported.",
{ capturedOutput: retainedOutput },
),
);
expect(projected).toMatchObject({
code: "unsupported_target",
category: "unsupported_target",
retryable: false,
details: {
operation: "inspect_evm_interface",
path: "/selected/runtime.hex",
reason: "EOF-style EF00 container is unsupported.",
captured_output: retainedOutput,
},
});
expect(projected.message).toContain("EF00");
expect(projected.remediation.action).toContain("target format");
expect(projected.remediation.action).not.toContain("doctor");
expect(analysisErrorProjectionSchema.safeParse(projected).success).toBe(true);
});
it.each([
["memory", null],
["memory", { address_space_bytes: 67108864 }],

View File

@ -11,6 +11,7 @@ import {
AnalysisArtifactChangedError,
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisUnsupportedTargetError,
AnalysisInputError,
AnalysisOutputError,
AnalysisTimeoutError,
@ -140,6 +141,7 @@ const STATIC_ERROR_CODES = {
AnalysisAccessDeniedError: "access_denied",
AnalysisArtifactChangedError: "artifact_changed",
AnalysisCapabilityUnavailableError: "capability_unavailable",
AnalysisUnsupportedTargetError: "unsupported_target",
AnalysisCancelledError: "cancelled",
HopperCancelledError: "cancelled",
AnalysisTimeoutError: "provider_timeout",
@ -187,6 +189,12 @@ const errorDetails = (
const requestErrorDetails = (
error: AnalysisError,
): Readonly<Record<string, JsonValue>> | undefined => {
if (error instanceof AnalysisUnsupportedTargetError)
return {
operation: error.operation,
path: error.path,
reason: error.reason,
};
if (error instanceof AnalysisResourceConstraintError)
return {
operation: error.operation,
@ -420,6 +428,7 @@ export interface AnalysisErrorProjection extends Readonly<
| "artifact_changed"
| "unreadable_output"
| "capability_unavailable"
| "unsupported_target"
| "provider_unavailable"
| "provider_timeout"
| "resource_constraint"
@ -437,6 +446,7 @@ export interface AnalysisErrorProjection extends Readonly<
readonly category:
| "invalid_input"
| "unsupported_provider"
| "unsupported_target"
| "integrity_mismatch"
| "truncated"
| "cancelled"

View File

@ -0,0 +1,36 @@
import { expect, it } from "vitest";
import { evmInterfaceSchema } from "./evmInterface.js";
import { evmInterfaceFixture } from "../../../tests/fixtures/evm/interface.js";
it("preserves original carrier and decoded byte identity as distinct observations", () => {
const fixture = evmInterfaceFixture();
expect(evmInterfaceSchema.parse(fixture)).toEqual(fixture);
expect(fixture.artifact.sha256).not.toBe(fixture.bytecode.sha256);
});
it.each(["byte-count", "body-offset", "selector", "authenticity"])(
"rejects false interface evidence: %s",
(problem) => {
const value = evmInterfaceFixture();
const raw = {
...value,
...(problem === "authenticity"
? {
bytecode: {
...value.bytecode,
deployment_authenticity: "verified",
},
}
: {}),
};
if (problem === "byte-count") raw.bytecode.bytes++;
if (problem === "body-offset" || problem === "selector")
raw.functions.push({
selector: problem === "selector" ? "bad" : "0xaabbccdd",
bytecode_offset: problem === "body-offset" ? 5 : 0,
dispatch: "abi",
inferred_arguments: null,
inferred_state_mutability: null,
});
expect(evmInterfaceSchema.safeParse(raw).success).toBe(false);
},
);

View File

@ -0,0 +1,76 @@
import { z } from "zod";
const byteOffset = z.number().int().nonnegative();
const sha256 = z.string().regex(/^[a-f0-9]{64}$/);
/** Select the carrier representation explicitly; no RPC or deployed-contract lookup is implied. */
export const inspectEvmInterfaceInputSchema = z.strictObject({
path: z
.string()
.min(1)
.describe("Absolute path to caller-selected local bytecode carrier"),
encoding: z
.enum(["raw", "hex"])
.describe(
"Raw bytes or UTF-8 hex with optional 0x prefix and outer ASCII whitespace",
),
});
/** Recovered interface candidates are inferences, independently of observed byte identity. */
export const evmInterfaceSchema = z
.strictObject({
artifact: z.strictObject({
path: z.string().min(1),
sha256,
bytes: byteOffset,
encoding: z.enum(["raw", "hex"]),
}),
bytecode: z.strictObject({
sha256,
bytes: byteOffset,
hex: z.string().regex(/^(?:[0-9a-f]{2})*$/),
digest_algorithm: z.literal("sha256"),
kind: z.literal("unknown"),
deployment_authenticity: z.literal("unknown"),
hardfork: z.literal("unknown"),
}),
evidence_kind: z.literal("inferred"),
functions: z.array(
z.strictObject({
selector: z.string().regex(/^0x[0-9a-f]{8}$/),
bytecode_offset: byteOffset,
dispatch: z.enum(["abi", "fallback"]),
inferred_arguments: z.string().nullable(),
inferred_state_mutability: z.string().nullable(),
}),
),
discovery_completeness: z.literal("unknown"),
runtime_execution: z.literal("not-performed"),
diagnostics: z.strictObject({
stdout: z.string(),
stderr: z.string(),
truncated: z.boolean(),
}),
limitations: z.array(z.string()),
})
.superRefine((value, context) => {
if (value.bytecode.hex.length !== value.bytecode.bytes * 2)
context.addIssue({
code: "custom",
path: ["bytecode"],
message:
"Selected byte count does not match the bytecode hex representation.",
});
for (const [index, fn] of value.functions.entries())
if (fn.bytecode_offset >= value.bytecode.bytes)
context.addIssue({
code: "custom",
path: ["functions", index, "bytecode_offset"],
message: "Inferred body offset lies outside the selected bytecode.",
});
});
export type InspectEvmInterfaceInput = z.infer<
typeof inspectEvmInterfaceInputSchema
>;
export type EvmInterface = z.infer<typeof evmInterfaceSchema>;

View File

@ -0,0 +1,34 @@
import { expect, it } from "vitest";
import {
decodeEvmBytecodeCarrier,
EvmCarrierFailure,
} from "./EvmBytecodeCarrier.js";
it.each(["6000FF", "0x6000ff", " \t0X6000FF\r\n"])(
"decodes explicit hex without changing selected bytes: %j",
(text) => {
expect(
Buffer.from(decodeEvmBytecodeCarrier(Buffer.from(text), "hex")).toString(
"hex",
),
).toBe("6000ff");
},
);
it.each(["0x1", "60 00", "0xzz", "\uFEFF6000", "\u00a06000"])(
"rejects malformed carrier rather than guessing bytecode: %j",
(text) => {
expect(() => decodeEvmBytecodeCarrier(Buffer.from(text), "hex")).toThrow(
EvmCarrierFailure,
);
},
);
it("distinguishes raw bytes, empty code and malformed UTF-8", () => {
const bytes = Uint8Array.from([0xff, 0x00]);
const decoded = decodeEvmBytecodeCarrier(bytes, "raw");
expect(decoded).toEqual(bytes);
expect(decoded).not.toBe(bytes);
expect(() => decodeEvmBytecodeCarrier(bytes, "hex")).toThrow("valid UTF-8");
expect(decodeEvmBytecodeCarrier(Buffer.from("0x"), "hex")).toEqual(
new Uint8Array(),
);
});

View File

@ -0,0 +1,26 @@
/** Expected carrier encoding failure, independent of the analysis engine. */
export class EvmCarrierFailure extends Error {}
/** Decode only the explicitly selected representation; source carrier bytes are retained separately. */
export const decodeEvmBytecodeCarrier = (
bytes: Uint8Array,
encoding: "raw" | "hex",
): Uint8Array => {
if (encoding === "raw") return Uint8Array.from(bytes);
let text: string;
try {
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
bytes,
);
} catch (cause: unknown) {
throw new EvmCarrierFailure("Hex carrier must be valid UTF-8.", { cause });
}
const code = text
.replace(/^[\x09-\x0d\x20]+|[\x09-\x0d\x20]+$/g, "")
.replace(/^0x/i, "");
if (!/^(?:[a-fA-F0-9]{2})*$/.test(code))
throw new EvmCarrierFailure(
"Hex carrier requires complete hexadecimal byte pairs, an optional 0x prefix and outer ASCII whitespace; interior separators are invalid.",
);
return Uint8Array.from(Buffer.from(code, "hex"));
};

View File

@ -0,0 +1,55 @@
import { expect, it } from "vitest";
import { selectEvmWorkerLimits } from "./EvmWorkerLimits.js";
const limits = (
soft = "unlimited",
hard = "unlimited",
) => `Limit Soft Limit Hard Limit Units
Max address space ${soft} ${hard} bytes
Max cpu time unlimited unlimited seconds
Max file size unlimited unlimited bytes
`;
it("uses configured maxima without equating virtual space and RSS", () => {
expect(selectEvmWorkerLimits(limits())).toEqual({
ok: true,
value: {
addressSpaceBytes: 3221225472,
cpuSeconds: 30,
fileSizeBytes: 16777216,
},
});
});
it.each([
["1073741824", "2147483648", 1073741824],
["2147483648", "2147483648", 2147483648],
["0", "0", 0],
["9223372036854775807", "9223372036854775807", 3221225472],
])(
"preserves tighter soft/hard boundaries without integer loss: %s/%s",
(soft, hard, expected) => {
expect(selectEvmWorkerLimits(limits(soft, hard))).toMatchObject({
ok: true,
value: { addressSpaceBytes: expected },
});
},
);
it("independently preserves CPU and file output restrictions", () => {
expect(
selectEvmWorkerLimits(
limits()
.replace("time unlimited unlimited", "time 5 10")
.replace("size unlimited unlimited", "size 4096 8192"),
),
).toMatchObject({ ok: true, value: { cpuSeconds: 5, fileSizeBytes: 4096 } });
});
it.each([
["not-text", null],
["soft-unlimited-hard-finite", limits("unlimited", "2147483648")],
["soft-exceeds-hard", limits("3", "2")],
["missing-row", limits().replace("Max file size", "Max other size")],
["duplicate-row", limits() + "Max cpu time 1 1 seconds\n"],
["wrong-unit", limits().replace("bytes", "pages")],
["oversized", "x".repeat(65537)],
])("rejects malformed/ambiguous resource observations: %s", (_label, input) => {
expect(selectEvmWorkerLimits(input).ok).toBe(false);
});

View File

@ -0,0 +1,58 @@
import { err, ok, type Result } from "../domain/result.js";
import { EVM_INTERFACE_LIMITS } from "./EvmoleRelease.js";
/** Selected worker soft limits to configure; inherited hard limits are left unchanged. */
export interface EvmWorkerLimits {
readonly addressSpaceBytes: number;
readonly cpuSeconds: number;
readonly fileSizeBytes: number;
}
/** Read only the three selected Linux limits, retaining tighter soft/hard boundaries. */
export const selectEvmWorkerLimits = (
raw: unknown,
): Result<EvmWorkerLimits, string> => {
if (typeof raw !== "string" || Buffer.byteLength(raw) > 64 * 1024)
return err(
"Linux resource limits must be text within the 64 KiB parsing budget.",
);
const selected: number[] = [];
for (const [name, units, maximum] of [
["Max address space", "bytes", EVM_INTERFACE_LIMITS.addressSpaceBytes],
["Max cpu time", "seconds", 30],
["Max file size", "bytes", EVM_INTERFACE_LIMITS.outputBytes],
] as const) {
const entries = raw
.split("\n")
.filter((line) => line.startsWith(name + " "));
if (entries.length !== 1)
return err(`Linux resource limits must report exactly one ${name} row.`);
const match = new RegExp(
`^${name}\\s+(unlimited|[0-9]+)\\s+(unlimited|[0-9]+)\\s+${units}\\s*$`,
).exec(entries[0] ?? "");
if (match === null)
return err(`Malformed inherited Linux resource limit: ${name}.`);
let effective = BigInt(maximum);
const soft = match[1];
const hard = match[2];
if (soft === undefined || hard === undefined)
return err(`Missing ${name} values.`);
if (
hard !== "unlimited" &&
(soft === "unlimited" || BigInt(soft) > BigInt(hard))
)
return err(`Inherited ${name} soft limit exceeds its hard limit.`);
for (const value of [soft, hard])
if (value !== "unlimited" && BigInt(value) < effective)
effective = BigInt(value);
selected.push(Number(effective));
}
const [addressSpaceBytes, cpuSeconds, fileSizeBytes] = selected;
if (
addressSpaceBytes === undefined ||
cpuSeconds === undefined ||
fileSizeBytes === undefined
)
return err("Required Linux resource limits were not selected.");
return ok({ addressSpaceBytes, cpuSeconds, fileSizeBytes });
};

205
src/evm/EvmoleFailures.ts Normal file
View File

@ -0,0 +1,205 @@
import type { EvmWorkerLimits } from "./EvmWorkerLimits.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import {
AnalysisError,
type AnalysisCapturedOutput,
} from "../domain/analysisErrorBase.js";
import type { ProviderProcessSnapshot } from "../process/ProviderProcess.js";
import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisCapabilityUnavailableError,
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
AnalysisResourceConstraintError,
AnalysisTimeoutError,
} from "../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { OwnedCommandFailure } from "../process/OwnedCommand.js";
import {
EVMOLE_PROVIDER_IDENTITY,
EVM_INTERFACE_LIMITS,
EVM_FILE_SIZE_FAILURE_EXIT,
} from "./EvmoleRelease.js";
/** Correlate a reserved exit with the owned worker's actual failure branch. */
export interface EvmFileSizeFailureEvidence {
readonly verified: boolean;
readonly failure: string | null;
}
/** Keep carrier read, engine and owned lifecycle failures distinct. */
export const evmInterfaceFailure = (
cause: unknown,
phase: "configuration" | "artifact-read" | "worker",
path: string,
workerLimits?: EvmWorkerLimits,
limiter = "/usr/bin/prlimit",
fileFailure?: EvmFileSizeFailureEvidence,
): AnalysisError => {
if (cause instanceof AnalysisError) return cause;
if (cause instanceof OwnedCommandFailure) {
const outputOptions =
cause.snapshot === null
? undefined
: { capturedOutput: capturedEvmOutput(cause.snapshot) };
if (cause.cleanupFailure !== null)
return new ProviderCleanupError(
EVMOLE_PROVIDER_IDENTITY.id,
cause.resources,
{
reason: cause.cleanupFailure,
...(cause.snapshot === null
? {}
: { captured_output: { ...capturedEvmOutput(cause.snapshot) } }),
previous_error: {
failure_kind: cause.reason,
message: cause.message,
exit_code: cause.snapshot?.exitCode ?? null,
signal: cause.snapshot?.signal ?? null,
stdout: cause.snapshot?.stdout.text ?? null,
stderr: cause.snapshot?.stderr.text ?? null,
},
},
{ operation: "inspect_evm_interface", cause },
);
if (cause.reason === "cancelled")
return new AnalysisCancelledError("inspect_evm_interface", outputOptions);
if (cause.reason === "timeout")
return new AnalysisTimeoutError(
"inspect_evm_interface",
EVM_INTERFACE_LIMITS.timeoutMs,
outputOptions,
);
if (cause.reason === "output-limit")
return new AnalysisOutputError(
"inspect_evm_interface",
cause.message,
outputOptions,
);
if (
cause.reason === "process" &&
(cause.snapshot?.signal === "SIGXCPU" ||
cause.snapshot?.signal === "SIGXFSZ" ||
(fileFailure?.verified === true &&
cause.snapshot?.exitCode === EVM_FILE_SIZE_FAILURE_EXIT &&
cause.snapshot.signal === null))
)
return new AnalysisResourceConstraintError(
"inspect_evm_interface",
cause.snapshot.signal === "SIGXCPU" ? "cpu" : "file-size",
(cause.snapshot.signal === null
? "Owned EVM worker reported EFBIG while writing its reply; the exact write failure cause is unknown."
: `Owned EVM command terminated with ${cause.snapshot.signal}; the exact signal cause is unknown.`) +
" Effective worker limits are unknown. Configured soft limits are retained separately.",
workerLimits === undefined
? null
: {
configured_soft_limits: {
address_space_bytes: workerLimits.addressSpaceBytes,
cpu_seconds: workerLimits.cpuSeconds,
file_size_bytes: workerLimits.fileSizeBytes,
},
effective_soft_limits: null,
},
outputOptions,
);
}
if (
cause instanceof Error &&
"code" in cause &&
"syscall" in cause &&
typeof cause.syscall === "string" &&
cause.syscall.startsWith("spawn ")
) {
const reason = `Configured util-linux prlimit could not launch (${String(cause.code)}): ${limiter}. Check its executable/interpreter and host execute access.`;
return new AnalysisCapabilityUnavailableError(
EVMOLE_PROVIDER_IDENTITY.id,
"inspect_evm_interface",
reason,
{ cause, userMessage: reason },
);
}
if (cause instanceof ArtifactReaderFailure) {
if (cause.reason === "cancelled")
return new AnalysisCancelledError("inspect_evm_interface");
if (cause.reason === "integrity")
return new AnalysisArtifactChangedError(
"inspect_evm_interface",
path,
cause.message,
{ cause },
);
if (
phase === "artifact-read" &&
["limit", "path", "format"].includes(cause.reason)
)
return new AnalysisInputError("inspect_evm_interface", { cause }, [
{
path: ["path"],
reason: cause.reason === "limit" ? "out_of_range" : "invalid_format",
message: cause.message,
},
]);
}
if (phase === "artifact-read" && cause instanceof Error && "code" in cause) {
if (cause.code === "EACCES" || cause.code === "EPERM")
return new AnalysisAccessDeniedError(
"inspect_evm_interface",
path,
cause.code,
{ cause },
);
if (cause.code === "ENOENT" || cause.code === "ENOTDIR")
return new AnalysisInputError("inspect_evm_interface", { cause }, [
{
path: ["path"],
reason: "invalid_value",
message: `Selected bytecode carrier could not be read (${String(cause.code)}): ${path}.`,
},
]);
}
return new ProviderAdapterError(
EVMOLE_PROVIDER_IDENTITY.id,
"inspect_evm_interface",
{
cause,
diagnostics: {
phase,
path,
...(workerLimits === undefined
? {}
: { configured_resource_limits: { ...workerLimits } }),
reason: cause instanceof Error ? cause.message : String(cause),
...(fileFailure === undefined
? {}
: { file_size_failure_marker: { ...fileFailure } }),
...(cause instanceof OwnedCommandFailure
? {
failure_kind: cause.reason,
exit_code: cause.snapshot?.exitCode ?? null,
signal: cause.snapshot?.signal ?? null,
stdout: cause.snapshot?.stdout.text ?? null,
stderr: cause.snapshot?.stderr.text ?? null,
...(cause.snapshot === null
? {}
: {
captured_output: { ...capturedEvmOutput(cause.snapshot) },
}),
}
: {}),
},
},
);
};
/** Preserve retained worker diagnostics with their observed truncation status. */
export const capturedEvmOutput = (
snapshot: ProviderProcessSnapshot,
): AnalysisCapturedOutput => ({
stdout: snapshot.stdout.text,
stderr: snapshot.stderr.text,
truncated: snapshot.diagnosticTruncated === true,
});

View File

@ -0,0 +1,66 @@
import { expect, it } from "vitest";
import { projectEvmoleInterface } from "./EvmoleInterfaceAdapter.js";
it("preserves the actual producer keys, duplicates and unknown inference absence", () => {
const raw = {
functions: [
{ selector: "aabbccdd", bytecodeOffset: 1, dispatch: "abi" },
{
selector: "aabbccdd",
bytecodeOffset: 2,
dispatch: "fallback",
arguments: "uint256",
stateMutability: "view",
},
],
};
const result = projectEvmoleInterface(raw);
expect(result.raw).toEqual(raw);
expect(result.functions).toEqual([
{
selector: "0xaabbccdd",
bytecode_offset: 1,
dispatch: "abi",
inferred_arguments: null,
inferred_state_mutability: null,
},
{
selector: "0xaabbccdd",
bytecode_offset: 2,
dispatch: "fallback",
inferred_arguments: "uint256",
inferred_state_mutability: "view",
},
]);
expect(projectEvmoleInterface({}).functions).toEqual([]);
const metadata = {
bytecodeOffset: 1,
cborLength: 4,
entries: [
{ key: "reported", value: { type: "integer", value: 9007199254740992 } },
],
};
expect(
projectEvmoleInterface({ functions: [], metadata }).raw.metadata,
).toEqual(metadata);
expect(
projectEvmoleInterface({
functions: [],
storage: undefined,
transientStorage: undefined,
disassembled: undefined,
basicBlocks: undefined,
controlFlowGraph: undefined,
metadata: undefined,
}).functions,
).toEqual([]);
});
it.each([
{ functions: [{ selector: "bad", bytecodeOffset: 0, dispatch: "abi" }] },
{
functions: [{ selector: "aabbccdd", bytecodeOffset: -1, dispatch: "abi" }],
},
{ metadata: {} },
])("rejects producer representations outside the selected profile", (raw) => {
expect(() => projectEvmoleInterface(raw)).toThrow();
});

View File

@ -0,0 +1,59 @@
import { z } from "zod";
const offset = z.number().int().nonnegative();
const reportedMetadata = z.strictObject({
bytecodeOffset: offset,
cborLength: offset,
entries: z.array(
z.strictObject({
key: z.string(),
value: z.discriminatedUnion("type", [
z.strictObject({ type: z.literal("string"), value: z.string() }),
z.strictObject({ type: z.literal("integer"), value: z.number() }),
z.strictObject({ type: z.literal("bytes"), value: z.string() }),
z.strictObject({ type: z.literal("bool"), value: z.boolean() }),
z.strictObject({ type: z.literal("undecoded"), value: z.string() }),
]),
}),
),
});
/** Parse the exact producer representation before assigning portable field names. */
export const evmoleInterfaceReplySchema = z.strictObject({
// The unchanged JS binding exposes disabled facets as own undefined keys.
// Their JSON transport representation is absence; actual facet values are
// outside this interface-only profile and must still fail validation.
storage: z.undefined().optional(),
transientStorage: z.undefined().optional(),
disassembled: z.undefined().optional(),
basicBlocks: z.undefined().optional(),
controlFlowGraph: z.undefined().optional(),
// Upstream also extracts metadata when selectors are requested, regardless
// of its metadata flag. Retain its reported values without precision claims.
metadata: reportedMetadata.optional(),
functions: z
.array(
z.strictObject({
selector: z.string().regex(/^[a-f0-9]{8}$/),
bytecodeOffset: offset,
dispatch: z.enum(["abi", "fallback"]),
arguments: z.string().optional(),
stateMutability: z.string().optional(),
}),
)
.optional(),
});
/** Preserve inference strings and unknown absence; selectors use the conventional 0x display prefix. */
export const projectEvmoleInterface = (raw: unknown) => {
const producer = evmoleInterfaceReplySchema.parse(raw);
return {
raw: producer,
functions: (producer.functions ?? []).map((fn) => ({
selector: `0x${fn.selector}`,
bytecode_offset: fn.bytecodeOffset,
dispatch: fn.dispatch,
inferred_arguments: fn.arguments ?? null,
inferred_state_mutability: fn.stateMutability ?? null,
})),
};
};

View File

@ -0,0 +1,401 @@
import { createHash, randomUUID } from "node:crypto";
import { access, readFile, stat, writeFile } from "node:fs/promises";
import { constants } from "node:fs";
import { isAbsolute, join } from "node:path";
import { fileURLToPath } from "node:url";
import { z } from "zod";
import {
createAnalysisExecution,
type AnalysisExecution,
type ExecutionOptions,
} from "../application/AnalysisProvider.js";
import type { EvmInterfacePort } from "../application/evm/EvmInterfacePort.js";
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
import { OwnedCommandFailure } from "../process/OwnedCommand.js";
import type {
AnalysisError,
AnalysisCapturedOutput,
} from "../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisUnsupportedTargetError,
AnalysisInputError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { InspectEvmInterfaceInput } from "../domain/evm/evmInterface.js";
import { evmInterfaceSchema } from "../domain/evm/evmInterface.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { err, ok, type Result } from "../domain/result.js";
import { runOwnedCommand } from "../process/OwnedCommand.js";
import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
import {
evmInterfaceFailure,
capturedEvmOutput,
type EvmFileSizeFailureEvidence,
} from "./EvmoleFailures.js";
import {
evmoleInterfaceReplySchema,
projectEvmoleInterface,
} from "./EvmoleInterfaceAdapter.js";
import {
EVMOLE_PROVIDER_IDENTITY,
EVM_INTERFACE_LIMITS,
EVM_FILE_SIZE_FAILURE_EXIT,
} from "./EvmoleRelease.js";
import { decodeEvmBytecodeCarrier } from "./EvmBytecodeCarrier.js";
import {
selectEvmWorkerLimits,
type EvmWorkerLimits,
} from "./EvmWorkerLimits.js";
const OPERATION = "inspect_evm_interface";
const replySchema = z.discriminatedUnion("ok", [
z.strictObject({
ok: z.literal(true),
version: z.literal("0.9.3"),
bytecode: z.strictObject({
sha256: z.string().regex(/^[a-f0-9]{64}$/),
bytes: z.number().int().nonnegative(),
hex: z.string().regex(/^(?:[a-f0-9]{2})*$/),
}),
raw: evmoleInterfaceReplySchema,
}),
z.strictObject({
ok: z.literal(false),
reason: z.enum(["format", "decoder", "output-limit", "unsupported"]),
message: z.string(),
}),
]);
type Launcher = NonNullable<Parameters<typeof runOwnedCommand>[2]>["launcher"];
/** Run unchanged EVMole/WASM in an owned worker; no target, chain or network execution. */
export class EvmoleInterfaceProvider implements EvmInterfacePort {
constructor(
readonly environment: Readonly<NodeJS.ProcessEnv> = process.env,
readonly launcher?: Launcher,
readonly createRuntime: () => Promise<
Pick<PrivateRuntimeRoot, "path" | "close">
> = () => PrivateRuntimeRoot.create({ prefix: "rea-evm-interface-" }),
) {}
/** Bind recovered interface candidates to both original carrier and decoded byte identity. */
async inspect(
input: InspectEvmInterfaceInput,
options?: ExecutionOptions,
): Promise<Result<AnalysisExecution, AnalysisError>> {
let root: Pick<PrivateRuntimeRoot, "path" | "close"> | undefined;
let retainedOutput: AnalysisCapturedOutput | undefined;
let phase: "configuration" | "artifact-read" | "worker" = "configuration";
let workerLimits: EvmWorkerLimits | undefined;
const limiter =
this.environment.REA_EVM_PRLIMIT_COMMAND ?? "/usr/bin/prlimit";
let result: Result<AnalysisExecution, AnalysisError>;
try {
if (options?.signal?.aborted) throw new AnalysisCancelledError(OPERATION);
if (process.platform !== "linux" || process.arch !== "x64")
throw new AnalysisCapabilityUnavailableError(
EVMOLE_PROVIDER_IDENTITY.id,
OPERATION,
"Initial real-verified offline EVM interface profile supports Linux x64 only.",
);
try {
if (!isAbsolute(limiter))
throw new Error(
"REA_EVM_PRLIMIT_COMMAND must be an absolute executable path.",
);
await access(limiter, constants.X_OK);
if (!(await stat(limiter)).isFile())
throw new Error(
"Configured prlimit must be a regular executable file, not a directory.",
);
} catch (cause: unknown) {
const reason = `Offline EVM interface inspection requires caller-supplied util-linux prlimit: ${limiter}; ${cause instanceof Error ? cause.message : String(cause)}`;
throw new AnalysisCapabilityUnavailableError(
EVMOLE_PROVIDER_IDENTITY.id,
OPERATION,
reason,
{ cause, userMessage: reason },
);
}
let inheritedLimits: string;
try {
inheritedLimits = await readFile("/proc/self/limits", "utf8");
} catch (cause: unknown) {
const reason = `Could not read inherited Linux resource limits at /proc/self/limits: ${cause instanceof Error ? cause.message : String(cause)}`;
throw new AnalysisCapabilityUnavailableError(
EVMOLE_PROVIDER_IDENTITY.id,
OPERATION,
reason,
{ cause, userMessage: reason },
);
}
const limits = selectEvmWorkerLimits(inheritedLimits);
if (!limits.ok)
throw new AnalysisCapabilityUnavailableError(
EVMOLE_PROVIDER_IDENTITY.id,
OPERATION,
limits.error,
{ userMessage: limits.error },
);
workerLimits = limits.value;
phase = "artifact-read";
const carrier = await readStableArtifact(
input.path,
EVM_INTERFACE_LIMITS.inputBytes,
options?.signal,
);
phase = "worker";
root = await this.createRuntime();
const snapshotPath = join(root.path, "carrier.snapshot");
const requestPath = join(root.path, "request.json");
const replyPath = join(root.path, "reply.json");
await writeFile(snapshotPath, carrier.bytes, {
flag: "wx",
mode: 0o600,
...(options?.signal === undefined ? {} : { signal: options.signal }),
});
await writeFile(
requestPath,
JSON.stringify({
snapshot_path: snapshotPath,
reply_path: replyPath,
failure_marker_path: join(root.path, "resource.failure"),
encoding: input.encoding,
}),
{ flag: "wx", mode: 0o600 },
);
const { NODE_OPTIONS: _ambientNodeOptions, ...environment } =
this.environment;
const execution = await runOwnedCommand(
{
command: limiter,
arguments: [
`--as=${String(workerLimits.addressSpaceBytes)}:`,
`--fsize=${String(workerLimits.fileSizeBytes)}:`,
"--core=0:",
`--cpu=${String(workerLimits.cpuSeconds)}:`,
"--",
process.execPath,
"--max-old-space-size=256",
"--v8-pool-size=1",
"--disable-wasm-trap-handler",
fileURLToPath(
new URL("./EvmoleInterfaceWorker.js", import.meta.url),
),
requestPath,
],
cwd: root.path,
expectedCommand: null,
runId: `rea-evm-interface-${randomUUID()}`,
hostEnvironment: environment,
},
{
timeoutMs: EVM_INTERFACE_LIMITS.timeoutMs,
diagnosticBytes: EVM_INTERFACE_LIMITS.diagnosticBytes,
},
{
...(options?.signal === undefined ? {} : { signal: options.signal }),
...(this.launcher === undefined ? {} : { launcher: this.launcher }),
},
);
const capturedOutput = capturedEvmOutput(execution);
retainedOutput = capturedOutput;
let reply: z.output<typeof replySchema>;
try {
const file = await readStableArtifact(
replyPath,
EVM_INTERFACE_LIMITS.outputBytes,
options?.signal,
);
reply = replySchema.parse(JSON.parse(file.bytes.toString("utf8")));
} catch (cause: unknown) {
if (options?.signal?.aborted)
throw new AnalysisCancelledError(OPERATION, { capturedOutput });
throw new AnalysisOutputError(
OPERATION,
`Owned interface worker reply failed for ${input.path}: ${cause instanceof Error ? cause.message : String(cause)}`,
{ cause, capturedOutput },
);
}
if (!reply.ok) {
if (reply.reason === "unsupported")
throw new AnalysisUnsupportedTargetError(
OPERATION,
input.path,
reply.message,
{ capturedOutput },
);
if (reply.reason === "format")
throw new AnalysisInputError(OPERATION, { capturedOutput }, [
{
path: ["path"],
reason: "invalid_format",
message: reply.message,
},
]);
if (reply.reason === "output-limit")
throw new AnalysisOutputError(OPERATION, reply.message, {
capturedOutput,
});
throw new ProviderAdapterError(EVMOLE_PROVIDER_IDENTITY.id, OPERATION, {
diagnostics: {
phase,
failure_kind: reply.reason,
reason: reply.message,
stdout: execution.stdout.text,
stderr: execution.stderr.text,
captured_output: { ...capturedOutput },
},
});
}
const selectedBytes = decodeEvmBytecodeCarrier(
carrier.bytes,
input.encoding,
);
const selectedHex = Buffer.from(selectedBytes).toString("hex");
const decodedHash = createHash("sha256")
.update(selectedBytes)
.digest("hex");
if (
decodedHash !== reply.bytecode.sha256 ||
selectedHex !== reply.bytecode.hex ||
selectedBytes.length !== reply.bytecode.bytes
)
throw new AnalysisOutputError(
OPERATION,
"Decoded bytecode representation does not match the selected carrier, encoding or reported SHA-256.",
{ capturedOutput },
);
const limitations = [
`Configured worker resource soft limits: address_space_bytes=${String(workerLimits.addressSpaceBytes)}, cpu_seconds=${String(workerLimits.cpuSeconds)}, file_size_bytes=${String(workerLimits.fileSizeBytes)}. Selected from inherited tighter limits; original hard limits are retained. Effective worker limits are not independently probed.`,
"Upstream may also report terminal metadata while recovering selectors. Its reported values are retained in raw_result, but integer precision and non-text-key coverage are unknown in the JS binding; complete original bytecode remains available.",
"Recovered selectors, argument strings and mutability are upstream inferences, not verified ABI signatures or names. Empty results do not prove the absence of callable dispatch.",
"Bytecode kind, hardfork and deployed-contract authenticity are unknown. This inspection performs no target runtime execution, chain/RPC lookup or network request.",
"Original carrier and decoded bytecode SHA-256 are distinct identities; SHA-256 is not the Ethereum Keccak code hash.",
"The owned worker has a 256 MiB JavaScript heap, 3 GiB virtual address-space limit and 30-second deadline. Virtual address space is distinct from resident memory; limits fail without partial success.",
];
const validated = evmInterfaceSchema.safeParse({
artifact: {
path: input.path,
sha256: carrier.sha256,
bytes: carrier.bytes.length,
encoding: input.encoding,
},
bytecode: {
...reply.bytecode,
digest_algorithm: "sha256",
kind: "unknown",
deployment_authenticity: "unknown",
hardfork: "unknown",
},
evidence_kind: "inferred",
functions: projectEvmoleInterface(reply.raw).functions,
discovery_completeness: "unknown",
runtime_execution: "not-performed",
diagnostics: {
stdout: execution.stdout.text,
stderr: execution.stderr.text,
truncated: capturedOutput.truncated,
},
limitations,
});
if (!validated.success)
throw new AnalysisOutputError(
OPERATION,
`Interface worker returned invalid bytecode-relative evidence: ${validated.error.issues[0]?.message ?? "schema mismatch"}`,
{ capturedOutput },
);
const report = validated.data;
result = ok(
createAnalysisExecution(report, EVMOLE_PROVIDER_IDENTITY, {
rawResult: reply.raw,
subject: { path: input.path, format: "file", sha256: carrier.sha256 },
locations: [{ kind: "artifact-path", path: input.path }],
limitations,
}),
);
} catch (cause: unknown) {
let fileFailure: EvmFileSizeFailureEvidence | undefined;
if (
cause instanceof OwnedCommandFailure &&
cause.snapshot?.exitCode === EVM_FILE_SIZE_FAILURE_EXIT &&
cause.snapshot.signal === null &&
root !== undefined
) {
try {
const marker = await readStableArtifact(
join(root.path, "resource.failure"),
1,
);
if (!marker.bytes.equals(Buffer.from("F")))
throw new Error(
"Private worker failure marker does not match the observed exit status.",
);
fileFailure = { verified: true, failure: null };
} catch (markerFailure: unknown) {
fileFailure = {
verified: false,
failure:
markerFailure instanceof Error
? markerFailure.message
: String(markerFailure),
};
}
}
result = err(
options?.signal?.aborted &&
(cause === options.signal.reason ||
(cause instanceof Error && cause.name === "AbortError"))
? new AnalysisCancelledError(
OPERATION,
retainedOutput === undefined
? undefined
: { capturedOutput: retainedOutput },
)
: evmInterfaceFailure(
cause,
phase,
input.path,
workerLimits,
limiter,
fileFailure,
),
);
}
if (root !== undefined) {
try {
await root.close();
} catch (cause: unknown) {
return err(
new ProviderCleanupError(
EVMOLE_PROVIDER_IDENTITY.id,
[root.path],
{
reason: cause instanceof Error ? cause.message : String(cause),
previous_error: result.ok
? null
: projectAnalysisError(result.error),
...(retainedOutput === undefined
? {}
: { captured_output: { ...retainedOutput } }),
},
{ operation: OPERATION },
),
);
}
}
return result.ok && options?.signal?.aborted
? err(
new AnalysisCancelledError(
OPERATION,
retainedOutput === undefined
? undefined
: { capturedOutput: retainedOutput },
),
)
: result;
}
}

View File

@ -0,0 +1,108 @@
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { z } from "zod";
import {
decodeEvmBytecodeCarrier,
EvmCarrierFailure,
} from "./EvmBytecodeCarrier.js";
import { projectEvmoleInterface } from "./EvmoleInterfaceAdapter.js";
import {
EVM_INTERFACE_LIMITS,
EVM_FILE_SIZE_FAILURE_EXIT,
} from "./EvmoleRelease.js";
const requestSchema = z.strictObject({
snapshot_path: z.string(),
reply_path: z.string(),
failure_marker_path: z.string(),
encoding: z.enum(["raw", "hex"]),
});
class UnsupportedContainer extends Error {}
const main = async (): Promise<void> => {
const requestPath = process.argv[2];
if (requestPath === undefined)
throw new Error("Owned worker request path missing.");
const request = requestSchema.parse(
JSON.parse(await readFile(requestPath, "utf8")),
);
let reply: unknown;
try {
const bytes = decodeEvmBytecodeCarrier(
await readFile(request.snapshot_path),
request.encoding,
);
if (bytes[0] === 0xef && bytes[1] === 0x00)
throw new UnsupportedContainer(
"Container marker EF00 is outside the verified flat-bytecode interface profile.",
);
const modulePath = import.meta.resolve("evmole/no_tla");
const metadata = z
.object({ version: z.literal("0.9.3") })
.parse(
JSON.parse(
await readFile(new URL("../package.json", modulePath), "utf8"),
),
);
const engine = await import("evmole/no_tla");
engine.initSync({
module: await readFile(
new URL(import.meta.resolve("evmole/evmole_bg.wasm")),
),
});
const inferred = projectEvmoleInterface(
engine.contractInfo(Buffer.from(bytes).toString("hex"), {
selectors: true,
arguments: true,
stateMutability: true,
}),
);
reply = {
ok: true,
version: metadata.version,
bytecode: {
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
hex: Buffer.from(bytes).toString("hex"),
},
raw: inferred.raw,
};
} catch (cause: unknown) {
reply = {
ok: false,
reason:
cause instanceof EvmCarrierFailure
? "format"
: cause instanceof UnsupportedContainer
? "unsupported"
: "decoder",
message: cause instanceof Error ? cause.message : String(cause),
};
}
let encoded = JSON.stringify(reply);
if (Buffer.byteLength(encoded) > EVM_INTERFACE_LIMITS.outputBytes)
encoded = JSON.stringify({
ok: false,
reason: "output-limit",
message: "Complete interface evidence exceeds the 16 MiB reply budget.",
});
try {
await writeFile(request.reply_path, encoded, { flag: "wx", mode: 0o600 });
} catch (cause: unknown) {
if (cause instanceof Error && "code" in cause && cause.code === "EFBIG") {
try {
await writeFile(request.failure_marker_path, "F", {
flag: "wx",
mode: 0o600,
});
} catch (markerFailure: unknown) {
process.stderr.write(
`File-size failure marker unavailable: ${markerFailure instanceof Error ? markerFailure.message : String(markerFailure)}\n`,
);
}
process.exitCode = EVM_FILE_SIZE_FAILURE_EXIT;
} else throw cause;
}
};
await main();

16
src/evm/EvmoleRelease.ts Normal file
View File

@ -0,0 +1,16 @@
/** Unchanged locked npm/WASM profile; provenance is independent of chain deployment authenticity. */
export const EVMOLE_PROVIDER_IDENTITY = {
id: "evmole-interface",
name: "REA EVM interface adapter",
version: "evmole@0.9.3",
} as const;
/** Worker-reserved status for an observed EFBIG write without a complete reply. */
export const EVM_FILE_SIZE_FAILURE_EXIT = 76;
/** Complete offline inspection budgets; worker JavaScript heap is bounded separately. */
export const EVM_INTERFACE_LIMITS = {
inputBytes: 4 * 1024 * 1024,
outputBytes: 16 * 1024 * 1024,
timeoutMs: 30_000,
diagnosticBytes: 1024 * 1024,
addressSpaceBytes: 3 * 1024 * 1024 * 1024,
} as const;

View File

@ -5,5 +5,5 @@ export const PACKAGE_METADATA = {
serverSdkVersion: "2.3.1",
clientSdkVersion: "2.3.1",
coreSdkVersion: "2.3.1",
skillVersion: "31",
skillVersion: "32",
} as const;

View File

@ -1,3 +1,6 @@
import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js";
import { createEvmInterfaceService } from "../composition/evm.js";
import { registerEvmTools } from "./registerEvmTools.js";
import { registerBinaryDiagnosticsTools } from "./registerBinaryDiagnosticsTools.js";
import { createBinaryLayoutService } from "../composition/binaryDiagnostics.js";
import type { BinaryLayoutService } from "../application/binaryDiagnostics/BinaryLayoutService.js";
@ -62,6 +65,7 @@ const ACTIVE_TARGET_INSTRUCTIONS =
"REA analyzes the active reverse-engineering target. Use the analysis tool that answers the question directly. Search or list symbols when discovery is needed; analyze_function provides a function dossier, and focused procedure tools return individual facets.";
export interface CreateServerOptions {
readonly evmInterface?: EvmInterfaceService;
readonly logger?: Logger;
readonly binaryLayout?: BinaryLayoutService;
readonly firmwareAnalysis?: FirmwareAnalysisPort;
@ -89,6 +93,9 @@ const installSessionToolAvailability = (
const policy = sessionAvailabilityPolicy(options.availabilityPolicy, {
optionalProviderLoadFailures: options.optionalProviderLoadFailures,
optionalFeatures: {
evmInterfaceEnabled:
options.evmInterface !== undefined ||
(process.platform === "linux" && process.arch === "x64"),
webModuleResolutionEnabled:
options.webModuleTrace !== undefined ||
isAbsolute(process.env.REA_BROWSER_EXECUTABLE ?? ""),
@ -205,6 +212,12 @@ export const createServer = (
toolLogger,
recordEvidence,
);
registerEvmTools(
server,
options.evmInterface ?? createEvmInterfaceService(),
toolLogger,
recordEvidence,
);
registerFirmwareTools(
server,
new FirmwareAnalysisService(

View File

@ -0,0 +1,32 @@
import type { McpServer } from "@modelcontextprotocol/server";
import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
/** Bind the named EVM contract to shared validation and session Evidence recording. */
export const registerEvmTools = (
server: McpServer,
service: EvmInterfaceService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
): void => {
const contract = toolContract("inspect_evm_interface");
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input, context) => {
const result = await logToolExecution(logger, contract.name, () =>
service.inspect(input, { signal: context.mcpReq.signal }),
);
if (!result.ok) return toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, contract)
: toCallToolResult(result, contract);
},
);
};

View File

@ -10,6 +10,7 @@ export interface SessionAvailabilityDefaults {
| undefined;
readonly optionalFeatures?: Pick<
SessionAvailability,
| "evmInterfaceEnabled"
| "browserObservationEnabled"
| "browserScenarioEnabled"
| "electronObservationEnabled"
@ -32,6 +33,8 @@ export const sessionAvailabilityPolicy = (
const policy =
configured ??
(() => ({
evmInterfaceEnabled:
defaults.optionalFeatures?.evmInterfaceEnabled ?? false,
processCaptureEnabled: platform !== "win32",
binaryLayoutEnabled:
defaults.optionalFeatures?.binaryLayoutEnabled ?? false,

View File

@ -1,3 +1,4 @@
import { EVMOLE_PROVIDER_IDENTITY } from "../../../src/evm/EvmoleRelease.js";
import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsRelease.js";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
@ -117,6 +118,7 @@ describe("canonical product catalog", () => {
IDA_PROVIDER_IDENTITY,
NATIVE_MACOS_PROVIDER_IDENTITY,
PWNTOOLS_PROVIDER_IDENTITY,
EVMOLE_PROVIDER_IDENTITY,
ARTIFACT_GRAPH_PROVIDER,
ANDROID_APPLICATION_PROVIDER,
APPLE_APPLICATION_PROVIDER,

View File

@ -0,0 +1,597 @@
import { createHash } from "node:crypto";
import { access, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it, onTestFinished } from "vitest";
import { z } from "zod";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { evmInterfaceSchema } from "../../../src/domain/evm/evmInterface.js";
import { EvmoleInterfaceProvider } from "../../../src/evm/EvmoleInterfaceProvider.js";
import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js";
import { waitForProviderProcessReady } from "../../fixtures/providerProcess.js";
import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js";
import {
createTestWorkspace,
removeTestWorkspace,
} from "../../support/workspace/workspaceFixture.js";
const unsupportedHost = process.platform !== "linux" || process.arch !== "x64";
const requestSchema = z.strictObject({
snapshot_path: z.string(),
reply_path: z.string(),
failure_marker_path: z.string(),
encoding: z.enum(["raw", "hex"]),
});
const fixture = async () => {
const workspace = await createTestWorkspace("rea-evm-interface-boundary-");
onTestFinished(() => removeTestWorkspace(workspace.root));
const path = join(workspace.root, "selected.hex");
await writeFile(path, "0x60006000f3\n");
return { path, root: workspace.root };
};
it.runIf(!unsupportedHost).each(["SIGXCPU", "SIGXFSZ", "EFBIG"])(
"preserves %s with configured limits distinct from unknown effective limits",
async (failure) => {
const { path } = await fixture();
let ownedPath = "";
const provider = new EvmoleInterfaceProvider({}, async (spawn) => {
ownedPath = spawn.cwd ?? "";
if (failure === "EFBIG")
await writeFile(join(ownedPath, "resource.failure"), "F");
return spawnOwnedProviderProcess({
...spawn,
command: process.execPath,
arguments: [
"-e",
failure === "EFBIG"
? 'process.stdout.write("resource diagnostic", () => { process.exitCode = 76; })'
: failure === "SIGXFSZ"
? 'process.on("SIGXFSZ", () => { process.removeAllListeners("SIGXFSZ"); process.kill(process.pid, "SIGXFSZ"); }); process.stdout.write("resource diagnostic", () => process.kill(process.pid, "SIGXFSZ")); setTimeout(() => {}, 1000);'
: `process.stdout.write("resource diagnostic", () => process.kill(process.pid, "${failure}"))`,
],
});
});
const result = await provider.inspect({ path, encoding: "hex" });
if (result.ok) throw new Error("Expected observed resource failure");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "resource_constraint",
details: {
resource: failure === "SIGXCPU" ? "cpu" : "file-size",
reported_limits: {
configured_soft_limits: { cpu_seconds: expect.any(Number) },
effective_soft_limits: null,
},
captured_output: { stdout: "resource diagnostic", truncated: false },
},
});
expect(result.error.message).toContain(
failure === "EFBIG" ? "exact write failure cause" : "exact signal cause",
);
await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" });
},
);
it.runIf(!unsupportedHost).each(["missing", "wrong"])(
"does not classify a launcher exit 76 with %s worker failure evidence",
async (scenario) => {
const { path } = await fixture();
let ownedPath = "";
const provider = new EvmoleInterfaceProvider({}, async (spawn) => {
ownedPath = spawn.cwd ?? "";
if (scenario === "wrong")
await writeFile(
join(ownedPath, "resource.failure"),
Buffer.from([0xc6]),
);
return spawnOwnedProviderProcess({
...spawn,
command: process.execPath,
arguments: [
"-e",
'process.stdout.write("launcher failure", () => { process.exitCode = 76; });',
],
});
});
const result = await provider.inspect({ path, encoding: "hex" });
if (result.ok) throw new Error("Expected launcher failure");
expect(projectAnalysisError(result.error).code).toBe("execution_failure");
expect(result.error).toMatchObject({
diagnostics: {
file_size_failure_marker: {
verified: false,
failure: expect.any(String),
},
captured_output: { stdout: "launcher failure", truncated: false },
},
});
await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" });
},
);
it.runIf(!unsupportedHost)(
"retains actual diagnostic truncation when overflow and process cleanup both fail",
async () => {
const { path } = await fixture();
let ownedPath = "";
const provider = new EvmoleInterfaceProvider({}, async (spawn) => {
ownedPath = spawn.cwd ?? "";
const launched = await spawnOwnedProviderProcess({
...spawn,
command: process.execPath,
arguments: ["-e", 'process.stdout.write("x".repeat(1048577))'],
});
return {
...launched,
cleanup: async () => {
await launched.cleanup?.();
throw new Error("injected post-cleanup reporting failure");
},
};
});
const result = await provider.inspect({ path, encoding: "hex" });
if (result.ok) throw new Error("Expected compounded lifecycle failure");
expect(result.error).toMatchObject({
cleanupIncomplete: true,
diagnostics: {
reason: "injected post-cleanup reporting failure",
previous_error: { failure_kind: "output-limit" },
captured_output: { truncated: true, stderr: "" },
},
});
await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" });
},
);
it.runIf(!unsupportedHost).each([1048576, 1048577])(
"keeps complete diagnostics or rejects a valid worker reply with %s diagnostic bytes",
async (size) => {
const { path } = await fixture();
let ownedPath = "";
const provider = new EvmoleInterfaceProvider({}, async (spawn) => {
ownedPath = spawn.cwd ?? "";
const requestPath = spawn.arguments.at(-1);
if (requestPath === undefined) throw new Error("Request missing");
const request = requestSchema.parse(
JSON.parse(await readFile(requestPath, "utf8")),
);
const bytes = Buffer.from("60006000f3", "hex");
await writeFile(
request.reply_path,
JSON.stringify({
ok: true,
version: "0.9.3",
bytecode: {
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
hex: bytes.toString("hex"),
},
raw: { functions: [] },
}),
);
return spawnOwnedProviderProcess({
...spawn,
command: process.execPath,
arguments: ["-e", `process.stderr.write("x".repeat(${String(size)}))`],
});
});
const result = await provider.inspect({ path, encoding: "hex" });
if (size === 1048576) {
if (!result.ok) throw result.error;
const report = evmInterfaceSchema.parse(result.value.result);
expect(report.diagnostics.stderr).toHaveLength(size);
expect(report.diagnostics.truncated).toBe(false);
} else {
if (result.ok)
throw new Error("Expected explicit diagnostic budget failure");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "unreadable_output",
details: { captured_output: { truncated: true } },
});
}
await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" });
},
);
it
.runIf(!unsupportedHost)
.each([
"success",
"format",
"unsupported",
"wrong-version",
"wrong-decoded-carrier",
"broken-reply",
"out-of-range-body",
])(
"binds source carrier/worker representation and releases owned files: %s",
async (scenario) => {
const { path } = await fixture();
let ownedRoot = "";
const provider = new EvmoleInterfaceProvider(
{ NODE_OPTIONS: "--inspect=0" },
async (spawn) => {
ownedRoot = spawn.cwd ?? "";
expect(spawn.hostEnvironment?.NODE_OPTIONS).toBeUndefined();
expect(spawn.arguments).toContain("--as=3221225472:");
expect(spawn.arguments).toContain("--disable-wasm-trap-handler");
const requestPath = spawn.arguments.at(-1);
if (requestPath === undefined) throw new Error("request missing");
const request = requestSchema.parse(
JSON.parse(await readFile(requestPath, "utf8")),
);
expect(request.encoding).toBe("hex");
expect(await readFile(request.snapshot_path, "utf8")).toBe(
"0x60006000f3\n",
);
const hex = scenario === "wrong-decoded-carrier" ? "00" : "60006000f3";
const bytes = Buffer.from(hex, "hex");
await writeFile(
request.reply_path,
scenario === "broken-reply"
? "broken"
: JSON.stringify(
["format", "unsupported"].includes(scenario)
? {
ok: false,
reason: scenario,
message: "Malformed hex carrier.",
}
: {
ok: true,
version: scenario === "wrong-version" ? "0.0.0" : "0.9.3",
bytecode: {
sha256: createHash("sha256")
.update(bytes)
.digest("hex"),
bytes: bytes.length,
hex,
},
raw: {
functions:
scenario === "out-of-range-body"
? [
{
selector: "aabbccdd",
bytecodeOffset: 5,
dispatch: "abi",
},
]
: [],
},
},
),
);
return spawnOwnedProviderProcess({
...spawn,
command: process.execPath,
arguments: ["-e", 'process.stderr.write("upstream warning")'],
});
},
);
const result = await provider.inspect({ path, encoding: "hex" });
if (scenario === "success") {
if (!result.ok) throw result.error;
expect(result.value.result).toMatchObject({
artifact: { path, encoding: "hex", bytes: 13 },
bytecode: { hex: "60006000f3", bytes: 5 },
diagnostics: { stderr: "upstream warning" },
});
expect(result.value.rawResult).toEqual({ functions: [] });
} else
expect(result).toMatchObject({
ok: false,
error: {
_tag:
scenario === "format"
? "AnalysisInputError"
: scenario === "unsupported"
? "AnalysisUnsupportedTargetError"
: "AnalysisOutputError",
},
});
if (!result.ok)
expect(projectAnalysisError(result.error)).toMatchObject({
details: {
captured_output: {
stdout: "",
stderr: "upstream warning",
truncated: false,
},
},
});
expect(await readFile(path, "utf8")).toBe("0x60006000f3\n");
await expect(access(ownedRoot)).rejects.toMatchObject({ code: "ENOENT" });
},
);
it.runIf(!unsupportedHost).each(["cancelled", "output-limit", "process"])(
"preserves lifecycle failure and independently releases an acquired worker: %s",
async (scenario) => {
const { path } = await fixture();
const controller = new AbortController();
let pid: number | undefined;
let ownedRoot = "";
const provider = new EvmoleInterfaceProvider({}, async (spawn) => {
ownedRoot = spawn.cwd ?? "";
const launched = await spawnOwnedProviderProcess({
...spawn,
command: process.execPath,
arguments: [
"-e",
scenario === "cancelled"
? 'process.stdout.write("ready\\n");setInterval(()=>undefined,1000)'
: scenario === "output-limit"
? 'process.stdout.write("x".repeat(1048577))'
: 'process.stderr.write("worker failed");process.exit(7)',
],
});
pid = launched.process.pid;
if (scenario === "cancelled") {
await waitForProviderProcessReady(launched.process);
setImmediate(() => controller.abort());
}
return launched;
});
expect(
await provider.inspect(
{ path, encoding: "hex" },
{ signal: controller.signal },
),
).toMatchObject({
ok: false,
error: {
_tag:
scenario === "cancelled"
? "AnalysisCancelledError"
: scenario === "output-limit"
? "AnalysisOutputError"
: "ProviderAdapterError",
},
});
if (pid === undefined) throw new Error("worker not acquired");
const acquiredPid = pid;
expect(() => process.kill(acquiredPid, 0)).toThrow();
await expect(access(ownedRoot)).rejects.toMatchObject({ code: "ENOENT" });
},
);
it.runIf(!unsupportedHost)(
"distinguishes unavailable limiter, absent carrier and prelaunch cancellation",
async () => {
const { path } = await fixture();
expect(
await new EvmoleInterfaceProvider({
REA_EVM_PRLIMIT_COMMAND: path + ".absent",
}).inspect({ path, encoding: "hex" }),
).toMatchObject({
ok: false,
error: { _tag: "AnalysisCapabilityUnavailableError" },
});
expect(
await new EvmoleInterfaceProvider({}).inspect({
path: path + ".absent",
encoding: "raw",
}),
).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
const controller = new AbortController();
controller.abort();
expect(
await new EvmoleInterfaceProvider({}).inspect(
{ path, encoding: "raw" },
{ signal: controller.signal },
),
).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } });
},
);
it.runIf(unsupportedHost)(
"reports unverified host coverage before acquiring a provider",
async () => {
expect(
await new EvmoleInterfaceProvider({}).inspect({
path: "/selected.hex",
encoding: "hex",
}),
).toMatchObject({
ok: false,
error: { _tag: "AnalysisCapabilityUnavailableError" },
});
},
);
it.runIf(!unsupportedHost)(
"preserves actual snapshot-write cancellation and root cleanup",
async () => {
const { path } = await fixture();
const controller = new AbortController();
let ownedRoot = "";
const provider = new EvmoleInterfaceProvider(
{},
() => {
throw new Error("must not launch worker");
},
async () => {
const root = await PrivateRuntimeRoot.create({
prefix: "rea-evm-cancel-",
});
ownedRoot = root.path;
controller.abort();
return root;
},
);
expect(
await provider.inspect(
{ path, encoding: "hex" },
{ signal: controller.signal },
),
).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } });
await expect(access(ownedRoot)).rejects.toMatchObject({ code: "ENOENT" });
},
);
it.runIf(!unsupportedHost)(
"reports private storage denial without claiming selected carrier read denial",
async () => {
const { path } = await fixture();
const provider = new EvmoleInterfaceProvider({}, undefined, () =>
Promise.reject(
Object.assign(new Error("private root denied"), { code: "EACCES" }),
),
);
expect(await provider.inspect({ path, encoding: "hex" })).toMatchObject({
ok: false,
error: {
_tag: "ProviderAdapterError",
diagnostics: { phase: "worker", reason: "private root denied" },
},
});
},
);
it.runIf(!unsupportedHost)(
"retains original format failure when owned-root cleanup fails",
async () => {
const { path, root } = await fixture();
const provider = new EvmoleInterfaceProvider(
{},
async (spawn) => {
const requestPath = spawn.arguments.at(-1);
if (requestPath === undefined) throw new Error("request missing");
const request = requestSchema.parse(
JSON.parse(await readFile(requestPath, "utf8")),
);
await writeFile(
request.reply_path,
JSON.stringify({
ok: false,
reason: "format",
message: "Odd selected hex.",
}),
);
return spawnOwnedProviderProcess({
...spawn,
command: process.execPath,
arguments: ["-e", "process.exit(0)"],
});
},
() =>
Promise.resolve({
path: root,
close: () => Promise.reject(new Error("root cleanup failed")),
}),
);
expect(await provider.inspect({ path, encoding: "hex" })).toMatchObject({
ok: false,
error: {
cleanupIncomplete: true,
cleanupResources: [root],
diagnostics: {
reason: "root cleanup failed",
previous_error: { category: "invalid_input" },
},
},
});
},
);
it.runIf(!unsupportedHost).each(["late-cancellation", "cleanup-failure"])(
"retains captured warnings after a successful worker: %s",
async (scenario) => {
const { path } = await fixture();
const controller = new AbortController();
let ownedRoot = "";
const provider = new EvmoleInterfaceProvider(
{},
async (spawn) => {
const requestPath = spawn.arguments.at(-1);
if (requestPath === undefined) throw new Error("request missing");
const request = requestSchema.parse(
JSON.parse(await readFile(requestPath, "utf8")),
);
const bytes = Buffer.from("60006000f3", "hex");
await writeFile(
request.reply_path,
JSON.stringify({
ok: true,
version: "0.9.3",
bytecode: {
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
hex: bytes.toString("hex"),
},
raw: { functions: [] },
}),
);
return spawnOwnedProviderProcess({
...spawn,
command: process.execPath,
arguments: ["-e", 'process.stderr.write("upstream warning")'],
});
},
async () => {
const root = await PrivateRuntimeRoot.create({
prefix: "rea-evm-late-",
});
ownedRoot = root.path;
return {
path: root.path,
close: async () => {
await root.close();
if (scenario === "late-cancellation") controller.abort();
else throw new Error("reported cleanup failure");
},
};
},
);
const result = await provider.inspect(
{ path, encoding: "hex" },
{ signal: controller.signal },
);
if (result.ok) throw new Error("Expected lifecycle failure");
const output = { stdout: "", stderr: "upstream warning", truncated: false };
expect(projectAnalysisError(result.error)).toMatchObject({
category:
scenario === "late-cancellation" ? "cancelled" : "execution_failure",
details:
scenario === "late-cancellation"
? { captured_output: output }
: { diagnostics: { captured_output: output } },
});
await expect(access(ownedRoot)).rejects.toMatchObject({ code: "ENOENT" });
},
);
it.runIf(!unsupportedHost).each(["directory", "missing-interpreter"])(
"identifies configured limiter constraints without blaming the carrier: %s",
async (scenario) => {
const { path, root } = await fixture();
const limiter =
scenario === "directory" ? root : join(root, "broken-limiter");
if (scenario === "missing-interpreter")
await writeFile(limiter, "#!/rea-missing-limiter-interpreter\n", {
mode: 0o700,
});
let ownedRoot: string | undefined;
const provider = new EvmoleInterfaceProvider(
{ REA_EVM_PRLIMIT_COMMAND: limiter },
undefined,
async () => {
const runtime = await PrivateRuntimeRoot.create({
prefix: "rea-evm-limiter-",
});
ownedRoot = runtime.path;
return runtime;
},
);
const result = await provider.inspect({ path, encoding: "hex" });
if (result.ok) throw new Error("Expected unavailable limiter");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "capability_unavailable",
category: "unsupported_provider",
details: { provider_id: "evmole-interface" },
});
expect(result.error.message).toContain(limiter);
if (ownedRoot !== undefined)
await expect(access(ownedRoot)).rejects.toMatchObject({ code: "ENOENT" });
expect(await readFile(path, "utf8")).toBe("0x60006000f3\n");
},
);

View File

@ -0,0 +1,78 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { Ajv2020 } from "ajv/dist/2020.js";
import { expect, it, onTestFinished } from "vitest";
import { EvmInterfaceService } from "../../../src/application/evm/EvmInterfaceService.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";
import {
evmInterfaceFixture,
evmInterfaceExecution,
} from "../../fixtures/evm/interface.js";
it("advertises exact valid schemas and records inline EVM interface evidence without a binary target", async () => {
const value = evmInterfaceFixture();
const service = new EvmInterfaceService({
inspect: () => Promise.resolve(ok(evmInterfaceExecution(value))),
});
const session = createTestBinarySession(() => {
throw new Error("deep provider must not start");
});
const server = createServer(session, session, { evmInterface: service });
const client = new Client({ name: "evm-interface-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_evm_interface",
);
if (advertised?.outputSchema === undefined)
throw new Error("EVM interface inspection must publish both schemas");
const ajv = new Ajv2020({ strict: false, validateFormats: false });
const inputSchema: Record<string, unknown> = advertised.inputSchema;
const outputSchema: Record<string, unknown> = advertised.outputSchema;
expect(ajv.validateSchema(inputSchema)).toBe(true);
expect(ajv.validateSchema(outputSchema)).toBe(true);
expect(
ajv.validate(inputSchema, { path: value.artifact.path, encoding: "hex" }),
).toBe(true);
expect(
ajv.validate(inputSchema, {
path: value.artifact.path,
encoding: "hex",
approval: true,
}),
).toBe(false);
const response = await client.callTool({
name: "inspect_evm_interface",
arguments: { path: value.artifact.path, encoding: "hex" },
});
expect(response.isError).not.toBe(true);
expect(
ajv.validate(outputSchema, response.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const parsed = toolContract("inspect_evm_interface").outputSchema.parse(
response.structuredContent,
);
const evidence = parseEvidence(parsed.evidence);
expect(parsed.result).toEqual(value);
expect(parsed.result).toEqual(evidence.normalized_result);
expect(session.evidenceById(evidence.evidence_id)).toEqual(evidence);
expect(
(
await client.callTool({
name: "inspect_evm_interface",
arguments: { path: "relative.hex", encoding: "hex" },
})
).isError,
).toBe(true);
});

44
tests/fixtures/evm/interface.ts vendored Normal file
View File

@ -0,0 +1,44 @@
import { createHash } from "node:crypto";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import type { EvmInterface } from "../../../src/domain/evm/evmInterface.js";
/** Source-owned transport seam, independent of real engine verification. */
export const evmInterfaceFixture = (
path = "/artifacts/source-owned.hex",
): EvmInterface => ({
artifact: { path, sha256: "a".repeat(64), bytes: 10, encoding: "hex" },
bytecode: {
sha256: createHash("sha256")
.update(Buffer.from("60006000f3", "hex"))
.digest("hex"),
bytes: 5,
hex: "60006000f3",
digest_algorithm: "sha256",
kind: "unknown",
deployment_authenticity: "unknown",
hardfork: "unknown",
},
evidence_kind: "inferred",
functions: [],
discovery_completeness: "unknown",
runtime_execution: "not-performed",
diagnostics: { stdout: "", stderr: "", truncated: false },
limitations: ["Interface completeness remains unknown."],
});
/** Bind a fixture to its explicit producing seam and carrier identity. */
export const evmInterfaceExecution = (value = evmInterfaceFixture()) =>
createAnalysisExecution(
value,
{ id: "fixture-evm", name: "Source-owned EVM seam", version: "1" },
{
rawResult: { functions: [] },
subject: {
path: value.artifact.path,
format: "file",
sha256: value.artifact.sha256,
},
limitations: value.limitations,
locations: [{ kind: "artifact-path", path: value.artifact.path }],
},
);

21
third_party/evmole/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Maxim Andreev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

22
third_party/evmole/README.md vendored Normal file
View File

@ -0,0 +1,22 @@
# EVM interface upstream profile
REA integrates unchanged [EVMole](https://github.com/cdump/evmole) as the exact
npm dependency `evmole@0.9.3`, with lockfile integrity verification. Tag `0.9.3`
resolves to commit `0409707c2efef11a9043f9b4505cad5719054a7e`.
The JavaScript binding and WASM engine remain in the original upstream package;
REA contains only carrier validation, a worker adapter and portable contracts.
No submodule or vendored engine fork is required. No signature database,
LLM decompiler, blockchain node or RPC dependency is acquired.
Upstream is MIT, copyright (c) 2023 Maxim Andreev. Its npm tarball omits the
license file, so REA packages the unchanged tag's [LICENSE](LICENSE) here.
The engine is initialized through the upstream `no_tla`/`initSync` interface
only inside an owned worker; importing REA services performs no WASM/file I/O.
Initial interface analysis uses `contractInfo` with selectors, arguments and
stateMutability enabled. Selector recovery also extracts/reports terminal metadata;
its reported representation is retained with explicit unknown precision/coverage.
Other upstream outputs are separate future contracts;
in particular, CFG destination IDs are not byte addresses and terminal CBOR
metadata cannot establish a lossless integer claim through the current binding.

View File

@ -37,7 +37,7 @@ const TEST_PROJECTS = [
name: "adapters",
include: [
"src/*.test.ts",
"src/{artifacts,browser,dotnet,firmware,ghidra,hopper,ida,inspector,javascript,native,process,reference,server,windows}/**/*.test.ts",
"src/{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,