Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28b1e5cadc | ||
|
|
6543405252 | ||
|
|
456aa4f6de | ||
|
|
cca7bf0022 | ||
|
|
73c8b36615 | ||
|
|
bb742110a4 | ||
|
|
a97bd5ed29 | ||
|
|
bddddccdee | ||
|
|
96fd98cce0 | ||
|
|
749e58772a | ||
|
|
ded59689ce | ||
|
|
0363e5a8e1 | ||
|
|
905e4866c4 | ||
|
|
f6009d0077 | ||
|
|
4d8d76b253 | ||
|
|
162f9bf7ff | ||
|
|
fef6f73064 | ||
|
|
efd81f9776 | ||
|
|
0751974d49 | ||
|
|
5156ba44c5 | ||
|
|
c19e15a2b3 | ||
|
|
649661dc8b | ||
|
|
e4cc0f09de | ||
|
|
a53689c55d | ||
|
|
2d426c924c | ||
|
|
a929c9f714 | ||
|
|
8c47a9ce4b | ||
|
|
de619bd334 | ||
|
|
24d9337649 | ||
|
|
2ec93ba166 | ||
|
|
afdaf98117 | ||
|
|
944981d8d5 | ||
|
|
b32a11e1a7 | ||
|
|
0e19db9fce | ||
|
|
66a9ad58fc | ||
|
|
4b985e5021 | ||
|
|
c8fd2f152b | ||
|
|
02ef2b1c41 | ||
|
|
961471205c | ||
|
|
a6d6e595f3 | ||
|
|
9ffc6210a4 | ||
|
|
b973b1b1ec | ||
|
|
47947eb01c | ||
|
|
2d99e1e3b2 | ||
|
|
5d6eeb22fb | ||
|
|
e964a06b83 | ||
|
|
5aa30b8b86 | ||
|
|
8c7569aca7 | ||
|
|
539a0f95b5 | ||
|
|
c73cf0ab76 | ||
|
|
aa7008f8a5 | ||
|
|
1887e1120e | ||
|
|
ca7ef69c0c | ||
|
|
b9c439831d | ||
|
|
bace2c5a4f | ||
|
|
3cd80cbb88 | ||
|
|
6cd5ea0fb2 | ||
|
|
706bba6e13 | ||
|
|
9964af6b79 | ||
|
|
3059d8a40f | ||
|
|
86b223b33e | ||
|
|
92741187ec | ||
|
|
5fdb0636b4 | ||
|
|
72b3209f3a | ||
|
|
f9ce32b474 | ||
|
|
46c9cba03a | ||
|
|
b4db35f092 | ||
|
|
7a56fe6260 | ||
|
|
b25922e4e8 | ||
|
|
b6ad1f2c3d | ||
|
|
2b6f14f528 | ||
|
|
f51503ef1c | ||
|
|
2bf812772d | ||
|
|
a936303723 | ||
|
|
93ca276921 | ||
|
|
911323c5dd | ||
|
|
f63f12d8ff | ||
|
|
983b070d4d | ||
|
|
9e22da845b | ||
|
|
f970b886aa | ||
|
|
a0e5e9db2b | ||
|
|
1f692c83e2 | ||
|
|
782e0147cd | ||
|
|
9defed9725 | ||
|
|
5f6bb7a5ba | ||
|
|
2596f8da14 | ||
|
|
b12d0c6361 | ||
|
|
cbcba2902d | ||
|
|
07d416505c | ||
|
|
3ea0233342 | ||
|
|
cf6957e796 | ||
|
|
ed200a2302 | ||
|
|
7df2050606 | ||
|
|
130ffdf6bc | ||
|
|
b09d02d93c | ||
|
|
1f0775684b | ||
|
|
7296f2b5bf | ||
|
|
3c7b176a3d | ||
|
|
aa23ec883f | ||
|
|
934ffd28d1 | ||
|
|
79f6756239 | ||
|
|
78e1308f49 | ||
|
|
4e3d335439 | ||
|
|
ea92328775 | ||
|
|
28d3cb545c | ||
|
|
696fb834b8 | ||
|
|
3ae5ac3359 | ||
|
|
a47517c67c |
+9
-2
@@ -2,7 +2,7 @@
|
||||
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: "35"
|
||||
version: "36"
|
||||
---
|
||||
|
||||
# REA
|
||||
@@ -31,7 +31,7 @@ When tools are absent or registration is stale:
|
||||
`npx -y rea-agents@latest doctor --client codex --json`. Substitute the current
|
||||
supported client: `claude_code`, `claude_desktop`, `codex`, `cursor`,
|
||||
`gemini_cli`, `windsurf`, `devin`, `opencode`, `antigravity`, `copilot_cli`,
|
||||
`commandcode`, `vscode`, `grok_build`, `omp`, or `grok_bot`. `grok_bot` has no local
|
||||
`commandcode`, `qwen_code`, `vscode`, `grok_build`, `omp`, `pi`, `hermes`, or `grok_bot`. `grok_bot` has no local
|
||||
file registration: doctor reports the Grok Bot chat step, and setup does not
|
||||
write `mcp.json`. If the client is unknown, use `doctor --json` and
|
||||
inspect its registration results before choosing a setup scope.
|
||||
@@ -121,6 +121,9 @@ explicit path or endpoint and do not need it.
|
||||
Historical PIDs do not select live processes. Request `include_debugger_context`
|
||||
only when core-only mapping candidates help; see the
|
||||
[recorded crash guide](https://github.com/morluto/rea/blob/main/docs/recorded-crashes.md).
|
||||
- PE resource type/name/language trees and static icon group references:
|
||||
`inspect_pe_resources` with an explicit path. This portable operation requires
|
||||
no active target or engine; opaque payloads retain file ranges and SHA-256.
|
||||
- Native executable, library, or analysis database: `open_binary`, then
|
||||
use focused analysis tools directly; call `binary_overview` when metadata or
|
||||
inventory context is useful and available from the selected provider.
|
||||
@@ -150,6 +153,10 @@ list stays complete; availability depends on the operation, target, and host.
|
||||
## Work summary-first
|
||||
|
||||
Start with the default result and use its inline Evidence and graph context.
|
||||
For a JavaScript application or ELF layout whose size is unknown or large,
|
||||
call `analyze_javascript_application` or `inspect_binary_layout` with
|
||||
`"detail": "summary"`, then inspect the retained `parent_evidence_id` with
|
||||
`inspect_analysis_view`.
|
||||
Do not repeat an identical tool call. Make a focused follow-up only when the
|
||||
returned result leaves a specific question unanswered.
|
||||
|
||||
+15
-2
@@ -37,8 +37,9 @@ unknown rather than false.
|
||||
For a completed `analyze_function` dossier, use `inspect_analysis_view` with
|
||||
`view: {kind: "native", facet: "value_flow_operations", offset: 0, limit: 32}`
|
||||
and the exact retained Evidence ID to read selected facts without reanalysis.
|
||||
Native facets also include procedure, pseudocode, assembly, callers/callees,
|
||||
references, and value-flow summaries. Pseudocode offsets and limits count UTF-16
|
||||
Native facets also include procedure, pseudocode, assembly, basic blocks,
|
||||
comments, callers/callees, references, unresolved calls, referenced strings and
|
||||
names, the native API record, and value-flow summaries. Pseudocode offsets and limits count UTF-16
|
||||
code units; use the returned `next_offset` for continuation. Provider limitations
|
||||
and unavailable facts remain explicit; the complete parent Evidence is retained.
|
||||
|
||||
@@ -152,3 +153,15 @@ context. PSP/stack/device state remains unmodeled. See [DOS guide](https://githu
|
||||
### Function annotations in Ghidra
|
||||
|
||||
Use `annotate_native_function` for one function name and/or entry comments, with at least one explicit change. Review its annotation readback and refreshed dossier inline. Changes are atomic and session-scoped; empty comments clear them, omitted fields preserve them. Later MCP calls observe edits until close. CLI `annotate-native-function` returns the updated analysis before discarding the session. Original executable bytes are unchanged; edits invalidate immutable snapshots. Windows P0 does not admit database mutations.
|
||||
|
||||
## Offline PE resource inventory
|
||||
|
||||
Use `inspect_pe_resources` with an absolute `path`, or
|
||||
`rea inspect-pe-resources ./selected.exe --json`, for portable PE32/PE32+
|
||||
resource inspection without opening a native target. Preserve returned Evidence:
|
||||
type/name/language IDs and UTF-16 names, original directory/data/payload ranges,
|
||||
code pages, payload hashes and static icon-group references. Same-language icon
|
||||
resolution is explicit; missing references and size mismatch remain visible.
|
||||
Other payloads are opaque, and this operation does not decode images or simulate
|
||||
Windows resource loading. Consult the budgets and supported tree profile in
|
||||
[offline binary diagnostics](https://github.com/morluto/rea/blob/main/docs/binary-diagnostics.md).
|
||||
@@ -28,7 +28,7 @@ intentionally not included. -->
|
||||
- Provider, bridge, target-format, or platform compatibility:
|
||||
- Evidence, artifact, provenance, or reconstruction contract:
|
||||
- Process execution, authorization, cleanup, or containment impact:
|
||||
- Generated metadata (`docs/public/product-catalog.json`), package, or installation impact (CI generates and validates ignored catalogs and `skills/`; edit authored instructions in `skill-src/` and never commit build outputs):
|
||||
- Generated metadata (`docs/public/product-catalog.json`), package, or installation impact (CI generates and validates ignored catalogs and `skills/`; edit authored instructions in `.agents/skills/reverse-engineer-anything/` and never commit build outputs):
|
||||
|
||||
## Evidence and regression coverage
|
||||
|
||||
|
||||
@@ -56,9 +56,9 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/rea-wakaru"
|
||||
curl --fail --location --retry 3 https://github.com/pionxzh/wakaru/releases/download/v1.13.0/wakaru-linux-x64.tar.gz -o "$RUNNER_TEMP/rea-wakaru/release.tar.gz"
|
||||
curl --fail --location --retry 3 https://github.com/pionxzh/wakaru/releases/download/v1.14.0/wakaru-linux-x64.tar.gz -o "$RUNNER_TEMP/rea-wakaru/release.tar.gz"
|
||||
tar -xzf "$RUNNER_TEMP/rea-wakaru/release.tar.gz" -C "$RUNNER_TEMP/rea-wakaru" wakaru
|
||||
printf '%s %s\n' e8c7ca052974604197389f6ace7464e4dd24dce64e2730971521bd6606ec9c9e "$RUNNER_TEMP/rea-wakaru/wakaru" | sha256sum --check
|
||||
printf '%s %s\n' 664fa06782a61eda7ed15885134b52525fd565dd77607e642062ceb6cd927c38 "$RUNNER_TEMP/rea-wakaru/wakaru" | sha256sum --check
|
||||
- name: Install isolated fixture compilers
|
||||
run: npm install --prefix "$RUNNER_TEMP/rea-fixture-compilers" --ignore-scripts --no-audit --no-fund esbuild@0.25.10 webpack@5.101.3
|
||||
- run: npm run build:cached
|
||||
|
||||
@@ -35,9 +35,5 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y libcairo2 fonts-dejavu-core
|
||||
python3 -m pip install -r website/requirements.txt
|
||||
- name: Prepare website assets
|
||||
run: python3 scripts/prepare-website.py
|
||||
- name: Check website links, metadata and publishing configuration
|
||||
run: python3 scripts/verify-website.py
|
||||
- name: Check sitemap discovery and metadata regressions
|
||||
run: python3 scripts/test-website.py
|
||||
- name: Check shared HTML, links, assets and contributor workflows
|
||||
run: python3 scripts/website.py check
|
||||
|
||||
@@ -59,10 +59,8 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y libcairo2 fonts-dejavu-core
|
||||
python3 -m pip install -r website/requirements.txt
|
||||
- name: Prepare website assets
|
||||
run: python3 scripts/prepare-website.py
|
||||
- name: Check website links, metadata and publishing configuration
|
||||
run: python3 scripts/verify-website.py
|
||||
- name: Check shared HTML, links, assets and contributor workflows
|
||||
run: python3 scripts/website.py check
|
||||
- name: Record the published commit
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
|
||||
@@ -35,7 +35,7 @@ Use [.nvmrc](.nvmrc) and `package.json#packageManager` for the pinned toolchain.
|
||||
|
||||
Use relevant checks for the change. The complete local gate, `npm run check:pr`, is optional for broad changes or CI diagnosis. See [CONTRIBUTING.md](CONTRIBUTING.md) for development, generated-file ownership, and release conventions.
|
||||
|
||||
`docs/public/product-catalog.json`, `docs/verification/managed-conformance-*.json`, and `skills/` are ignored outputs. Edit source contracts and `skill-src/`, then regenerate as needed. `.cache/mcp-tool-catalog.json` is test metadata; runtime builds and source checking do not consume it. Never commit binaries, provider project documents, credentials, `dist/`, `node_modules/`, or local planning artifacts.
|
||||
`docs/public/product-catalog.json`, `docs/verification/managed-conformance-*.json`, and `skills/` are ignored outputs. Edit source contracts and `.agents/skills/reverse-engineer-anything/`, then regenerate as needed. `.cache/mcp-tool-catalog.json` is test metadata; runtime builds and source checking do not consume it. Never commit binaries, provider project documents, credentials, `dist/`, `node_modules/`, or local planning artifacts.
|
||||
|
||||
## Tool and Test Changes
|
||||
|
||||
|
||||
+10
-5
@@ -95,8 +95,12 @@ Pre-commit formats and lints staged files; pre-push runs `check:fast`.
|
||||
`evidence:generate` prepares only the managed commitment and its runtime/skill
|
||||
dependencies. The ignored outputs are `docs/public/product-catalog.json`,
|
||||
`docs/verification/managed-conformance-*.json`, and `skills/`. Edit authored
|
||||
skill instructions in `skill-src/`. CI validates generated outputs and retains
|
||||
them as artifacts; do not commit them. Reviewed source metadata, including
|
||||
skill instructions in `.agents/skills/reverse-engineer-anything/`, where Skills
|
||||
can discover the public bundle directly from a clean checkout. `verify:package`
|
||||
installs that bundle with Skills 1.7.2 using default repository-root discovery
|
||||
and checks every installed file, alongside the packaged setup checks.
|
||||
CI validates generated outputs and retains them as artifacts; do not commit
|
||||
them. Reviewed source metadata, including
|
||||
`src/generatedPackageMetadata.ts` and `docs/error-contract.schema.json`, remains
|
||||
tracked and checked for freshness.
|
||||
|
||||
@@ -120,9 +124,10 @@ for exact-path, awaited cleanup, including failures and timeouts.
|
||||
rejects remaining REA-owned paths. Never glob-clean shared `/tmp/rea-*` content.
|
||||
|
||||
`REA_LOG_LEVEL` selects `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or
|
||||
`silent`. MCP defaults to `info` on stderr; one-shot CLI logging is opt-in on
|
||||
stdout. Request arguments, bridge tokens, and environment data are redacted
|
||||
from diagnostic logs.
|
||||
`silent`. Logs go to stderr: MCP defaults to `info`, and one-shot CLI logging
|
||||
is opt-in, so stdout carries only protocol messages and command results.
|
||||
Request arguments, bridge tokens, and environment data are redacted from
|
||||
diagnostic logs.
|
||||
|
||||
## Real Hopper changes
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
**English** · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
**English** · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
|
||||
|
||||
# REA: Reverse Engineer Anything
|
||||
|
||||
@@ -151,13 +151,16 @@ 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://rea.tools/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) |
|
||||
| Recorded Linux crashes | Raw notes, every recorded thread's registers/signals and optional mapping candidates | Caller-supplied pwntools; optional GDB/pwndbg; [recorded crashes](docs/recorded-crashes.md) |
|
||||
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier on Linux x64; [offline EVM guide](docs/evm-bytecode.md) |
|
||||
| Recorded Linux crashes | Raw notes, every recorded thread's registers/signals and optional mapping candidates | Caller-supplied pwntools on Linux x64; optional GDB/pwndbg; [recorded crashes](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Modules, imports, source maps, routes, IPC and native add-on relationships | Node.js and npm; [application analysis](https://rea.tools/guides/javascript/) |
|
||||
| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](https://rea.tools/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) |
|
||||
| .NET assemblies | Metadata, CIL instructions, declared native dependencies and build comparisons | None (static only); [managed-code guide](docs/managed-code-analysis.md) |
|
||||
| Android APKs | Manifest declarations, classes, decompiled methods and references | Headless JADX and a full JDK on Linux/macOS/Windows x64; [Android guide](docs/android-analysis.md) |
|
||||
| Android devices | Devices, packages, processes, logs, dumps, screen captures, and APK/file transfers | Caller-supplied adb (emulator or hardware); [ADB guide](docs/adb-device-analysis.md) |
|
||||
| JEB projects | Engine-backed project units, artifact digests and type/method pseudo-code | A caller-started JEB client serving MCP, selected with REA_JEB_MCP_URL; [JEB guide](docs/jeb-analysis.md) |
|
||||
| Android resources | Decoded manifest, string tables, locales and version facts | Caller-supplied Apktool launcher; [resource guide](docs/apktool-resource-analysis.md) |
|
||||
| Firmware | Regions, extraction results and native-analysis handoffs | Binwalk / Unblob on Linux; [firmware guide](docs/firmware-analysis.md) |
|
||||
| Packages and resources | File inventories, digests, plists, Apple bundle anatomy and extracted resources | [Artifact and JavaScript guide](docs/javascript-artifact-reconstruction.md), [Apple applications](docs/apple-application-analysis.md) |
|
||||
| Process behavior | Terminal output, interactions, exit and filesystem observations, and run comparisons | Linux/macOS with a native PTY; [process capture](docs/process-capture.md) |
|
||||
@@ -307,7 +310,7 @@ Report vulnerabilities through [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Star history
|
||||
|
||||
🎉 **50,000 GitHub stars — thank you!**
|
||||
🎉 **60,000 GitHub stars — thank you!**
|
||||
|
||||
Thanks to everyone using REA, reporting bugs, requesting features, testing builds, and contributing fixes.
|
||||
|
||||
|
||||
+7
-4
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · **العربية** · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · **العربية** · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
|
||||
|
||||
# REA: هندسة عكسية لأي شيء
|
||||
|
||||
@@ -135,13 +135,16 @@ npx rea-agents@latest setup
|
||||
| ------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| الملفات الثنائية للشيفرة الأصلية | شيفرة شبه برمجية، وتعليمات تجميع، وسلاسل نصية، ورموز، واستدعاءات، ومراجع | Hopper أو Ghidra أو IDA؛ [تحليل الشيفرة الأصلية](https://rea.tools/guides/native/) |
|
||||
| بنية ELF دون تشغيل | الأقسام والمقاطع والرموز ومعلومات إعادة التموضع الأصلية، وآليات الحماية المحتملة المستدلّ عليها بالفحص الثابت | pwntools يوفّره المستدعي على Linux x64؛ [تشخيص الملفات الثنائية](docs/binary-diagnostics.md) |
|
||||
| شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا؛ [دليل EVM دون تشغيل](docs/evm-bytecode.md) |
|
||||
| أعطال Linux المسجّلة | سجلات note الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](docs/recorded-crashes.md) |
|
||||
| شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا على Linux x64؛ [دليل EVM دون تشغيل](docs/evm-bytecode.md) |
|
||||
| أعطال Linux المسجّلة | سجلات note الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي على Linux x64؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](https://rea.tools/guides/javascript/) |
|
||||
| المواقع | بنية الصفحة، والسكربتات، وملاحظات الشبكة، ولقطات الشاشة المطلوبة | متصفح من عائلة Chrome؛ [تحليل المتصفح](https://rea.tools/guides/browser/) |
|
||||
| تسجيلات الشبكة المحفوظة | الطلبات، والاستجابات، ومحتويات الحمولة المتاحة، ومواضعها في المصدر | HAR؛ وmitmdump على Linux لتسجيلات mitmproxy بصيغتها الأصلية؛ [دليل تسجيلات الشبكة](docs/web-network-captures.md) |
|
||||
| تجميعات .NET | البيانات الوصفية، وتعليمات CIL، واعتماديات الشيفرة الأصلية المعلنة، ومقارنات البناء | فحص ثابت؛ [دليل الشيفرة المُدارة](docs/managed-code-analysis.md) |
|
||||
| حزم Android APK | تصريحات ملف manifest، والأصناف، والدوال الناتجة عن فك الترجمة، والمراجع | JADX دون واجهة رسومية وJDK كامل على Linux/macOS؛ [دليل Android](docs/android-analysis.md) |
|
||||
| حزم Android APK | تصريحات ملف manifest، والأصناف، والدوال الناتجة عن فك الترجمة، والمراجع | JADX دون واجهة رسومية وJDK كامل على Linux/macOS/Windows x64؛ [دليل Android](docs/android-analysis.md) |
|
||||
| أجهزة Android | الأجهزة والحزم والعمليات والسجلات والدامبات ولقطات الشاشة ونقل APK/الملفات | adb يوفّره المُستدعي (محاكي أو جهاز)؛ [دليل ADB](docs/adb-device-analysis.md) |
|
||||
| مشاريع JEB | وحدات المشروع من المحرك وبصمات المصنّفات والكود الزائف للأنواع/الدوال | عميل JEB يبدأه المُستدعي ويقدّم MCP، ويُختار عبر REA_JEB_MCP_URL؛ [دليل JEB](docs/jeb-analysis.md) |
|
||||
| موارد Android | البيان المفكوك الترميز وجداول السلاسل واللغات وحقائق الإصدار | مُشغّل Apktool يوفّره المُستدعي؛ [دليل الموارد](docs/apktool-resource-analysis.md) |
|
||||
| البرامج الثابتة | المناطق، ونتائج الاستخراج، وما يُمرّر إلى تحليل الشيفرة الأصلية | Binwalk / Unblob على Linux؛ [دليل البرامج الثابتة](docs/firmware-analysis.md) |
|
||||
| الحزم والموارد | قوائم الملفات، والبصمات، وملفات plist، وبنية حزم Apple، والموارد المستخرجة | [دليل تحليل الملفات وJavaScript](docs/javascript-artifact-reconstruction.md)، [تطبيقات Apple](docs/apple-application-analysis.md) |
|
||||
| سلوك العمليات | مخرجات الطرفية، والتفاعلات، وملاحظات الخروج ونظام الملفات، ومقارنات التشغيل | Linux/macOS مع PTY أصلي؛ [تسجيل سلوك العمليات](docs/process-capture.md) |
|
||||
|
||||
+7
-4
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · **Deutsch** · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · **Deutsch** · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
|
||||
|
||||
# REA: Alles reverse-engineeren
|
||||
|
||||
@@ -135,13 +135,16 @@ REA benötigt Node.js 22.x (>=22.19), 24.x (>=24.11) oder 26+ sowie npm. Zusätz
|
||||
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Native Binärdateien | Pseudocode, Assemblercode, Zeichenketten, Symbole, Aufrufe und Verweise | Hopper, Ghidra oder IDA; [native Analyse](https://rea.tools/guides/native/) |
|
||||
| Offline-ELF-Struktur | Sektionen, Segmente, ursprüngliche Symbole/Relokationen und mögliche Schutzmaßnahmen aus statischer Analyse | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; [Binärdiagnostik](docs/binary-diagnostics.md) |
|
||||
| EVM-Bytecode | Dispatch-Selektoren, Byte-Offsets, abgeleitete Argumente und Veränderbarkeit des Zustands | Lokale Eingabe mit Rohbytes oder Hexdarstellung; [Offline-EVM-Leitfaden](docs/evm-bytecode.md) |
|
||||
| Aufgezeichnete Linux-Abstürze | Unverarbeitete Note-Einträge, Register/Signale aller aufgezeichneten Threads und optionale Zuordnungskandidaten | Vom Aufrufer bereitgestelltes pwntools; optional GDB/pwndbg; [aufgezeichnete Abstürze](docs/recorded-crashes.md) |
|
||||
| EVM-Bytecode | Dispatch-Selektoren, Byte-Offsets, abgeleitete Argumente und Veränderbarkeit des Zustands | Lokale Eingabe mit Rohbytes oder Hexdarstellung unter Linux x64; [Offline-EVM-Leitfaden](docs/evm-bytecode.md) |
|
||||
| Aufgezeichnete Linux-Abstürze | Unverarbeitete Note-Einträge, Register/Signale aller aufgezeichneten Threads und optionale Zuordnungskandidaten | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; optional GDB/pwndbg; [aufgezeichnete Abstürze](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Module, Imports, Source Maps, Routen, IPC und Beziehungen zu nativen Add-ons | Node.js und npm; [Anwendungsanalyse](https://rea.tools/guides/javascript/) |
|
||||
| Websites | Seitenstruktur, Skripte, Netzwerkbeobachtungen und angeforderte Screenshots | Ein Browser aus der Chrome-Familie; [Browseranalyse](https://rea.tools/guides/browser/) |
|
||||
| Gespeicherte Netzwerkaufzeichnungen | Anfragen, Antworten, zugängliche Nutzdaten und Quellpositionen | HAR; mitmdump unter Linux für Aufzeichnungen im nativen mitmproxy-Format; [Leitfaden zu Netzwerkaufzeichnungen](docs/web-network-captures.md) |
|
||||
| .NET-Assemblies | Metadaten, CIL-Anweisungen, deklarierte native Abhängigkeiten und Build-Vergleiche | Statische Untersuchung; [Leitfaden für verwalteten Code](docs/managed-code-analysis.md) |
|
||||
| Android-APKs | Manifest-Deklarationen, Klassen, dekompilierte Methoden und Verweise | JADX ohne grafische Oberfläche und ein vollständiges JDK unter Linux/macOS; [Android-Leitfaden](docs/android-analysis.md) |
|
||||
| Android-APKs | Manifest-Deklarationen, Klassen, dekompilierte Methoden und Verweise | JADX ohne grafische Oberfläche und ein vollständiges JDK unter Linux/macOS/Windows x64; [Android-Leitfaden](docs/android-analysis.md) |
|
||||
| Android-Geräte | Geräte, Pakete, Prozesse, Logs, Dumps, Bildschirmaufnahmen sowie APK- und Dateiübertragungen | Vom Aufrufer bereitgestelltes adb (Emulator oder Gerät); [ADB-Leitfaden](docs/adb-device-analysis.md) |
|
||||
| JEB-Projekte | Engine-gestützte Projekteinheiten, Artefakt-Prüfsummen und Pseudokode von Typen/Methoden | Ein vom Aufrufer gestarteter JEB-Client mit MCP, ausgewählt über REA_JEB_MCP_URL; [JEB-Leitfaden](docs/jeb-analysis.md) |
|
||||
| Android-Ressourcen | Dekodiertes Manifest, String-Tabellen, Sprachen und Versionsfakten | Vom Aufrufer bereitgestellter Apktool-Starter; [Ressourcen-Leitfaden](docs/apktool-resource-analysis.md) |
|
||||
| Firmware | Bereiche, Extraktionsergebnisse und Übergaben an die native Analyse | Binwalk / Unblob unter Linux; [Firmware-Leitfaden](docs/firmware-analysis.md) |
|
||||
| Pakete und Ressourcen | Dateiinventare, Hashwerte, plists, Aufbau von Apple-Bundles und extrahierte Ressourcen | [Artefakt- und JavaScript-Leitfaden](docs/javascript-artifact-reconstruction.md), [Apple-Anwendungen](docs/apple-application-analysis.md) |
|
||||
| Prozessverhalten | Terminalausgabe, Interaktionen, Beobachtungen zu Prozessende und Dateisystem sowie Laufvergleiche | Linux/macOS mit nativer PTY; [Prozessaufzeichnung](docs/process-capture.md) |
|
||||
|
||||
+7
-4
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · **Español** · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · **Español** · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
|
||||
|
||||
# REA: Ingeniería inversa de cualquier cosa
|
||||
|
||||
@@ -135,13 +135,16 @@ REA requiere Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, además de npm. Las h
|
||||
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Binarios nativos | Pseudocódigo, ensamblador, cadenas, símbolos, llamadas y referencias | Hopper, Ghidra o IDA; [análisis nativo](https://rea.tools/guides/native/) |
|
||||
| Estructura ELF sin ejecutar | Secciones, segmentos, símbolos/reubicaciones originales y posibles medidas de mitigación detectadas mediante análisis estático | pwntools proporcionado por quien llama, en Linux x64; [diagnóstico de binarios](docs/binary-diagnostics.md) |
|
||||
| Bytecode EVM | Selectores de despacho, desplazamientos en bytes, argumentos inferidos y mutabilidad | Entrada local con bytes sin procesar o representación hexadecimal; [guía de EVM sin ejecutar](docs/evm-bytecode.md) |
|
||||
| Fallos de Linux registrados | Registros note sin procesar, registros/señales de cada hilo registrado y posibles correspondencias de memoria opcionales | pwntools proporcionado por quien llama; GDB/pwndbg opcionales; [fallos registrados](docs/recorded-crashes.md) |
|
||||
| Bytecode EVM | Selectores de despacho, desplazamientos en bytes, argumentos inferidos y mutabilidad | Entrada local con bytes sin procesar o representación hexadecimal, en Linux x64; [guía de EVM sin ejecutar](docs/evm-bytecode.md) |
|
||||
| Fallos de Linux registrados | Registros note sin procesar, registros/señales de cada hilo registrado y posibles correspondencias de memoria opcionales | pwntools proporcionado por quien llama, en Linux x64; GDB/pwndbg opcionales; [fallos registrados](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Módulos, importaciones, mapas de código fuente, rutas, IPC y relaciones con complementos nativos | Node.js y npm; [análisis de aplicaciones](https://rea.tools/guides/javascript/) |
|
||||
| Sitios web | Estructura de la página, scripts, observaciones de red y capturas de pantalla solicitadas | Un navegador de la familia Chrome; [análisis del navegador](https://rea.tools/guides/browser/) |
|
||||
| Capturas de red guardadas | Solicitudes, respuestas, contenido accesible de las cargas útiles y ubicaciones de origen | HAR; mitmdump en Linux para capturas en formato nativo de mitmproxy; [guía de capturas](docs/web-network-captures.md) |
|
||||
| Ensamblados .NET | Metadatos, instrucciones CIL, dependencias nativas declaradas y comparaciones de compilaciones | Inspección estática; [guía de código administrado](docs/managed-code-analysis.md) |
|
||||
| APK de Android | Declaraciones del manifiesto, clases, métodos descompilados y referencias | JADX sin interfaz gráfica y un JDK completo en Linux/macOS; [guía de Android](docs/android-analysis.md) |
|
||||
| APK de Android | Declaraciones del manifiesto, clases, métodos descompilados y referencias | JADX sin interfaz gráfica y un JDK completo en Linux/macOS/Windows x64; [guía de Android](docs/android-analysis.md) |
|
||||
| Dispositivos Android | Dispositivos, paquetes, procesos, registros, volcados, capturas de pantalla y transferencias de APK/archivos | adb suministrado por el autor de la llamada (emulador o dispositivo); [guía de ADB](docs/adb-device-analysis.md) |
|
||||
| Proyectos JEB | Unidades de proyecto del motor, resúmenes de artefactos y pseudocódigo de tipos/métodos | Un cliente JEB iniciado por el autor de la llamada que sirve MCP, seleccionado con REA_JEB_MCP_URL; [guía de JEB](docs/jeb-analysis.md) |
|
||||
| Recursos de Android | Manifiesto decodificado, tablas de cadenas, idiomas y datos de versión | Lanzador de Apktool suministrado por el autor de la llamada; [guía de recursos](docs/apktool-resource-analysis.md) |
|
||||
| Firmware | Regiones, resultados de extracción y derivaciones al análisis nativo | Binwalk / Unblob en Linux; [guía de firmware](docs/firmware-analysis.md) |
|
||||
| Paquetes y recursos | Inventarios de archivos, resúmenes criptográficos, plists, estructura de paquetes de Apple y recursos extraídos | [Guía de artefactos y JavaScript](docs/javascript-artifact-reconstruction.md), [aplicaciones de Apple](docs/apple-application-analysis.md) |
|
||||
| Comportamiento de procesos | Salida de la terminal, interacciones, observaciones de salida y del sistema de archivos, y comparaciones entre ejecuciones | Linux/macOS con una PTY nativa; [captura de procesos](docs/process-capture.md) |
|
||||
|
||||
+18
-15
@@ -1,6 +1,6 @@
|
||||
<div align="center" dir="rtl">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · **فارسی**
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · **فارسی** · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
|
||||
|
||||
# REA: مهندسی معکوس هر چیزی
|
||||
|
||||
@@ -123,20 +123,23 @@ npx rea-agents@latest setup
|
||||
|
||||
REA به Node.js 22.x (>=22.19)، 24.x (>=24.11) یا 26+، بههمراه npm نیاز دارد. ابزارهای اضافی و پشتیبانی سیستم میزبان به نوع هدف بستگی دارند:
|
||||
|
||||
| هدف | خروجی REA | پیشنیازها و راهنما |
|
||||
| --------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| باینریهای بومی | شبهکد، اسمبلی، رشتهها، نمادها، فراخوانیها و ارجاعات | Hopper، Ghidra یا IDA؛ [تحلیل بومی](https://rea.tools/guides/native/) |
|
||||
| ساختار ELF آفلاین | بخشها، سگمنتها، نمادها و relocationهای اصلی و گزینههای احتمالی کاهش آسیبپذیری ایستا | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ [تشخیص باینری](docs/binary-diagnostics.md) |
|
||||
| بایتکد EVM | selectorهای dispatch، offsetهای بایتی، آرگومانهای استنباطشده و قابلیت تغییر وضعیت | دادهٔ خام/hex محلی؛ [راهنمای EVM آفلاین](docs/evm-bytecode.md) |
|
||||
| کرشهای ثبتشدهٔ لینوکس | یادداشتهای خام، رجیسترها/سیگنالهای تمام threadهای ثبتشده و گزینههای احتمالی نگاشت | pwntools تأمینشده توسط فراخواننده؛ GDB/pwndbg اختیاری؛ [کرشهای ثبتشده](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | ماژولها، importها، source mapها، مسیرها، IPC و ارتباط افزونههای بومی | Node.js و npm؛ [تحلیل برنامه](https://rea.tools/guides/javascript/) |
|
||||
| وبسایتها | ساختار صفحه، اسکریپتها، مشاهدات شبکه و اسکرینشاتهای درخواستی | مرورگر مبتنی بر Chrome؛ [تحلیل مرورگر](https://rea.tools/guides/browser/) |
|
||||
| دادههای ضبطشدهٔ شبکه | درخواستها، پاسخها، payloadهای آشکارشده و مکان آنها در منبع | HAR؛ ابزار mitmdump در لینوکس برای ضبطهای بومی mitmproxy؛ [راهنمای ضبط شبکه](docs/web-network-captures.md) |
|
||||
| اسمبلیهای .NET | فراداده، دستورالعملهای CIL، وابستگیهای بومی اعلامشده و مقایسهٔ buildها | بررسی ایستا؛ [راهنمای کد مدیریتشده](docs/managed-code-analysis.md) |
|
||||
| فایلهای APK اندروید | تعاریف Manifest، کلاسها، متدهای دیکامپایلشده و ارجاعات | JADX بدون رابط گرافیکی و JDK کامل در Linux/macOS؛ [راهنمای اندروید](docs/android-analysis.md) |
|
||||
| Firmware | ناحیهها، نتایج استخراج و تحویل به ابزارهای تحلیل بومی | Binwalk / Unblob در لینوکس؛ [راهنمای Firmware](docs/firmware-analysis.md) |
|
||||
| بستهها و منابع | فهرست فایلها، digestها، فایلهای plist، ساختار bundle اپل و منابع استخراجشده | [راهنمای آرتیفکت و JavaScript](docs/javascript-artifact-reconstruction.md)، [برنامههای اپل](docs/apple-application-analysis.md) |
|
||||
| رفتار فرایند | خروجی ترمینال، تعاملات، وضعیت خروج، مشاهدات سیستم فایل و مقایسهٔ اجراها | Linux/macOS با PTY بومی؛ [ضبط فرایند](docs/process-capture.md) |
|
||||
| هدف | خروجی REA | پیشنیازها و راهنما |
|
||||
| --------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| باینریهای بومی | شبهکد، اسمبلی، رشتهها، نمادها، فراخوانیها و ارجاعات | Hopper، Ghidra یا IDA؛ [تحلیل بومی](https://rea.tools/guides/native/) |
|
||||
| ساختار ELF آفلاین | بخشها، سگمنتها، نمادها و relocationهای اصلی و گزینههای احتمالی کاهش آسیبپذیری ایستا | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ [تشخیص باینری](docs/binary-diagnostics.md) |
|
||||
| بایتکد EVM | selectorهای dispatch، offsetهای بایتی، آرگومانهای استنباطشده و قابلیت تغییر وضعیت | دادهٔ خام/hex محلی روی Linux x64؛ [راهنمای EVM آفلاین](docs/evm-bytecode.md) |
|
||||
| کرشهای ثبتشدهٔ لینوکس | یادداشتهای خام، رجیسترها/سیگنالهای تمام threadهای ثبتشده و گزینههای احتمالی نگاشت | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ GDB/pwndbg اختیاری؛ [کرشهای ثبتشده](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | ماژولها، importها، source mapها، مسیرها، IPC و ارتباط افزونههای بومی | Node.js و npm؛ [تحلیل برنامه](https://rea.tools/guides/javascript/) |
|
||||
| وبسایتها | ساختار صفحه، اسکریپتها، مشاهدات شبکه و اسکرینشاتهای درخواستی | مرورگر مبتنی بر Chrome؛ [تحلیل مرورگر](https://rea.tools/guides/browser/) |
|
||||
| دادههای ضبطشدهٔ شبکه | درخواستها، پاسخها، payloadهای آشکارشده و مکان آنها در منبع | HAR؛ ابزار mitmdump در لینوکس برای ضبطهای بومی mitmproxy؛ [راهنمای ضبط شبکه](docs/web-network-captures.md) |
|
||||
| اسمبلیهای .NET | فراداده، دستورالعملهای CIL، وابستگیهای بومی اعلامشده و مقایسهٔ buildها | بررسی ایستا؛ [راهنمای کد مدیریتشده](docs/managed-code-analysis.md) |
|
||||
| فایلهای APK اندروید | تعاریف Manifest، کلاسها، متدهای دیکامپایلشده و ارجاعات | JADX بدون رابط گرافیکی و JDK کامل در Linux/macOS/Windows x64؛ [راهنمای اندروید](docs/android-analysis.md) |
|
||||
| دستگاههای اندروید | دستگاهها، بستهها، فرایندها، لاگها، دامپها، اسکرینشاتها و انتقال APK/فایل | adb تأمینشده توسط فراخوانکننده (شبیهساز یا دستگاه)؛ [راهنمای ADB](docs/adb-device-analysis.md) |
|
||||
| پروژههای JEB | واحدهای پروژه از موتور، هشهای مصنوع و شبهکدِ نوع/متد | یک کلاینت JEB که توسط فراخوانکننده اجرا شده و MCP ارائه میدهد و با REA_JEB_MCP_URL انتخاب میشود؛ [راهنمای JEB](docs/jeb-analysis.md) |
|
||||
| منابع اندروید | مانیفست رمزگشاییشده، جداول رشتهها، زبانها و اطلاعات نسخه | راهانداز Apktool تأمینشده توسط فراخوانکننده؛ [راهنمای منابع](docs/apktool-resource-analysis.md) |
|
||||
| Firmware | ناحیهها، نتایج استخراج و تحویل به ابزارهای تحلیل بومی | Binwalk / Unblob در لینوکس؛ [راهنمای Firmware](docs/firmware-analysis.md) |
|
||||
| بستهها و منابع | فهرست فایلها، digestها، فایلهای plist، ساختار bundle اپل و منابع استخراجشده | [راهنمای آرتیفکت و JavaScript](docs/javascript-artifact-reconstruction.md)، [برنامههای اپل](docs/apple-application-analysis.md) |
|
||||
| رفتار فرایند | خروجی ترمینال، تعاملات، وضعیت خروج، مشاهدات سیستم فایل و مقایسهٔ اجراها | Linux/macOS با PTY بومی؛ [ضبط فرایند](docs/process-capture.md) |
|
||||
|
||||
بررسی ایستای JavaScript و .NET فایلهای ارائهشده را بدون اجرای برنامه میخواند. ضبط زمان اجرا، هدف انتخابشده را با مجوزهای کاربری شما اجرا میکند یا با آن تعامل دارد؛ آثار این عملیات در راهنمای مربوط به هر نوع ضبط توضیح داده شدهاند.
|
||||
|
||||
|
||||
+7
-4
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · **Français** · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · **Français** · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
|
||||
|
||||
# REA : rétro-ingénierie de tout
|
||||
|
||||
@@ -136,13 +136,16 @@ REA nécessite Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, ainsi que npm. Les
|
||||
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Binaires natifs | Pseudo-code, assembleur, chaînes, symboles, appels et références | Hopper, Ghidra ou IDA ; [analyse native](https://rea.tools/guides/native/) |
|
||||
| Structure ELF hors ligne | Sections, segments, symboles et relocations d'origine, et protections statiques candidates | pwntools fourni par l'utilisateur sous Linux x64 ; [diagnostics binaires](docs/binary-diagnostics.md) |
|
||||
| Bytecode EVM | Sélecteurs de dispatch, offsets en octets, arguments inférés et mutabilité | Fichier local brut ou hexadécimal ; [guide EVM hors ligne](docs/evm-bytecode.md) |
|
||||
| Crashs Linux enregistrés | Notes brutes, registres et signaux de chaque thread enregistré, et correspondances de mappings facultatives | pwntools fourni par l'utilisateur ; GDB/pwndbg en option ; [crashs enregistrés](docs/recorded-crashes.md) |
|
||||
| Bytecode EVM | Sélecteurs de dispatch, offsets en octets, arguments inférés et mutabilité | Fichier local brut ou hexadécimal sous Linux x64 ; [guide EVM hors ligne](docs/evm-bytecode.md) |
|
||||
| Crashs Linux enregistrés | Notes brutes, registres et signaux de chaque thread enregistré, et correspondances de mappings facultatives | pwntools fourni par l'utilisateur sous Linux x64 ; GDB/pwndbg en option ; [crashs enregistrés](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Modules, imports, source maps, routes, IPC et relations avec les modules natifs | Node.js et npm ; [analyse d'applications](https://rea.tools/guides/javascript/) |
|
||||
| Sites web | Structure des pages, scripts, observations réseau et captures d'écran demandées | Un navigateur de la famille Chrome ; [analyse du navigateur](https://rea.tools/guides/browser/) |
|
||||
| Captures réseau enregistrées | Requêtes, réponses, payloads exposés et emplacements dans le code source | HAR ; mitmdump sous Linux pour les captures mitmproxy natives ; [guide des captures](docs/web-network-captures.md) |
|
||||
| Assemblys .NET | Métadonnées, instructions CIL, dépendances natives déclarées et comparaisons de builds | Inspection statique ; [guide du code managé](docs/managed-code-analysis.md) |
|
||||
| APK Android | Déclarations du manifeste, classes, méthodes décompilées et références | JADX headless et un JDK complet sous Linux/macOS ; [guide Android](docs/android-analysis.md) |
|
||||
| APK Android | Déclarations du manifeste, classes, méthodes décompilées et références | JADX headless et un JDK complet sous Linux/macOS/Windows x64 ; [guide Android](docs/android-analysis.md) |
|
||||
| Appareils Android | Appareils, paquets, processus, journaux, dumps, captures d'écran et transferts d'APK/fichiers | adb fourni par l'appelant (émulateur ou appareil) ; [guide ADB](docs/adb-device-analysis.md) |
|
||||
| Projets JEB | Unités de projet fournies par le moteur, condensés d'artefacts et pseudo-code de types/méthodes | Un client JEB démarré par l'appelant servant MCP, sélectionné via REA_JEB_MCP_URL ; [guide JEB](docs/jeb-analysis.md) |
|
||||
| Ressources Android | Manifeste décodé, tables de chaînes, langues et faits de version | Lanceur Apktool fourni par l'appelant ; [guide des ressources](docs/apktool-resource-analysis.md) |
|
||||
| Firmware | Régions, résultats d'extraction et passage de relais vers l'analyse native | Binwalk / Unblob sous Linux ; [guide firmware](docs/firmware-analysis.md) |
|
||||
| Paquets et ressources | Inventaires de fichiers, empreintes, plists, anatomie des bundles Apple et ressources extraites | [Guide artefacts et JavaScript](docs/javascript-artifact-reconstruction.md), [applications Apple](docs/apple-application-analysis.md) |
|
||||
| Comportement des processus | Sortie du terminal, interactions, code de sortie, observations du système de fichiers et comparaisons | Linux/macOS avec un PTY natif ; [capture de processus](docs/process-capture.md) |
|
||||
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · **Bahasa Indonesia** · [Italiano](README_it.md)
|
||||
|
||||
# REA: Rekayasa Balik Apa Pun
|
||||
|
||||
### Satu MCP untuk rekayasa balik pada berbagai biner, aplikasi, dan perilaku saat runtime.
|
||||
|
||||
**Melihat fitur yang Anda sukai. Pahami cara kerjanya hingga ke tingkat biner.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[Situs Web](https://rea.tools/) · [Panduan](https://rea.tools/guides/) · [Contoh Penggunaan](https://rea.tools/showcase/)**
|
||||
|
||||
[Mulai Cepat](#mulai-cepat) · [Cara Kerja REA](#cara-kerja-rea) · [Hal yang Dapat Anda Analisis](#hal-yang-dapat-anda-analisis) · [Contoh Penggunaan](#contoh-penggunaan) · [FAQ](#faq) · [Dokumentasi](#dokumentasi)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA menjalankan jembatan analisisnya di dalam Hopper saat memeriksa biner native" width="1200" />
|
||||
|
||||
<br /><br />
|
||||
|
||||
<table aria-label="Komunitas REA">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>Bergabunglah dengan Komunitas Rekayasa Balik</strong>
|
||||
</a><br />
|
||||
<sub>Discord · Tanya Jawab · Berbagi Hasil</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Melihat fitur dalam suatu aplikasi yang ingin Anda hadirkan di produk sendiri? Minta agen Anda menyelidikinya menggunakan REA. REA dapat memeriksa aplikasi tanpa kode sumbernya, menjelaskan cara kerja fitur tersebut, menunjukkan bukti, dan membangun versi untuk proyek Anda.
|
||||
|
||||
REA menghubungkan agen Anda dengan berbagai alat untuk memeriksa biner native, aplikasi JavaScript dan Electron, assembly .NET, serta situs web. Anda juga dapat menggunakan alat yang sama dari terminal. Analisis dijalankan secara lokal, dan hasilnya mencakup bukti serta keterbatasan yang mendasari setiap kesimpulan.
|
||||
|
||||
Proses penyiapan mendaftarkan REA ke agen Anda dan memasang instruksi alur kerja yang sesuai. Analisis native dapat menggunakan instalasi Hopper, Ghidra, atau IDA yang sudah ada. Proses penyiapan juga dapat memasang Hopper secara opsional setelah mendapatkan persetujuan. Analisis JavaScript statis tidak memerlukan mesin analisis native.
|
||||
|
||||
> **[Kunjungi situs web REA](https://rea.tools/)** untuk mendapatkan petunjuk penyiapan, panduan bergambar, dan studi kasus nyata.
|
||||
|
||||
## Mulai Cepat
|
||||
|
||||
### Siapkan agen Anda
|
||||
|
||||
Dengan Node.js dan npm yang telah terpasang, jalankan:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
Pilih agen yang ingin digunakan, tinjau perubahan yang diusulkan, lalu setujui perubahan tersebut. Proses penyiapan menambahkan server MCP REA dan instruksi alur kerja yang sesuai, serta membuat cadangan konfigurasi yang sudah ada. Setelah itu, mulai ulang agen Anda.
|
||||
|
||||
Penyiapan mendukung Claude Code, Codex, Cursor, Gemini CLI, Grok Build, dan [agen lainnya](docs/installation.md#supported-agents). Lihat [instalasi dan penyiapan](docs/installation.md) untuk konfigurasi penyedia dan pendaftaran MCP secara manual.
|
||||
|
||||
### Minta agen Anda
|
||||
|
||||
```text
|
||||
Pahami cara kerja pencarian di aplikasi Notes, tunjukkan buktinya, dan buat fitur serupa untuk proyek saya.
|
||||
```
|
||||
|
||||
Ganti Notes dengan aplikasi target dan fitur yang ingin Anda pahami.
|
||||
|
||||
### Gunakan terminal
|
||||
|
||||
Periksa direktori aplikasi JavaScript/Electron yang telah diekstrak atau berkas ASAR:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
Hasilnya mencakup modul, impor, batas antarkomponen Electron, dan bukti yang mendasarinya. Ganti jalur tersebut dengan lokasi target Anda, misalnya `"D:/apps/example"` di Windows.
|
||||
|
||||
Untuk memasang perintah `rea` agar dapat digunakan secara rutin:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
```
|
||||
|
||||
Untuk analisis native, konfigurasikan penyedia terlebih dahulu. Lihat [Panduan CLI dan Bukti](docs/cli.md) untuk perintah native, pemilihan penyedia, snapshot, dan pembuatan skrip.
|
||||
|
||||
### Perbarui REA
|
||||
|
||||
REA berkembang dengan cepat, dan rilis baru sering menyertakan perbaikan bug. Pastikan instalasi Anda selalu diperbarui.
|
||||
|
||||
Untuk CLI yang dipasang melalui npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Untuk memperbarui pendaftaran agen dan skill Anda, jalankan perintah penyiapan yang ditampilkan oleh proses pembaruan.
|
||||
|
||||
Jika Anda menggunakan `npx`, perbarui penyiapan agen dengan:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Tinjau perubahan penyiapan dan mulai ulang agen Anda. Untuk menjalankan perintah CLI sekali pakai, gunakan `npx rea-agents@latest` diikuti dengan perintah yang diinginkan.
|
||||
|
||||
## Cara Kerja REA
|
||||
|
||||
Agen Anda memanggil REA melalui MCP untuk memeriksa target dan menelusuri kode yang relevan. REA mengembalikan temuan beserta bukti pendukungnya. Agen kemudian menggunakan temuan tersebut untuk mengajukan pertanyaan lanjutan, menjelaskan perilaku, atau menulis dan menguji implementasi.
|
||||
|
||||
Perintah CLI menggunakan alur kerja yang sama.
|
||||
|
||||

|
||||
|
||||
[Buka ilustrasi dalam ukuran penuh](website/public/assets/figures/rea-investigation-flow.svg).
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## Hal yang Dapat Anda Analisis
|
||||
|
||||
REA memerlukan Node.js 22.x (>=22.19), 24.x (>=24.11), atau 26+, beserta npm.
|
||||
|
||||
Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis:
|
||||
|
||||
| Target | Hasil yang diberikan REA | Persyaratan dan panduan |
|
||||
| -------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Biner native | Pseudocode, assembly, string, simbol, pemanggilan, dan referensi | Hopper, Ghidra, atau IDA; [analisis native](https://rea.tools/guides/native/) |
|
||||
| Tata letak ELF offline | Bagian, segmen, simbol/relokasi asli, dan kandidat mitigasi statis | pwntools yang disediakan pemanggil pada Linux x64; [diagnostik biner](docs/binary-diagnostics.md) |
|
||||
| Bytecode EVM | Selector dispatch, offset byte, argumen yang disimpulkan, dan sifat mutabilitas | Pembawa data raw/hex lokal; [panduan EVM offline](docs/evm-bytecode.md) |
|
||||
| Crash Linux yang direkam | Catatan mentah, register/sinyal setiap thread yang direkam, dan kandidat pemetaan opsional | pwntools yang disediakan pemanggil; GDB/pwndbg opsional; [crash yang direkam](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Modul, impor, source map, rute, IPC, dan hubungan add-on native | Node.js dan npm; [analisis aplikasi](https://rea.tools/guides/javascript/) |
|
||||
| Situs web | Struktur halaman, skrip, pengamatan jaringan, dan tangkapan layar yang diminta | Peramban berbasis Chrome; [analisis peramban](https://rea.tools/guides/browser/) |
|
||||
| Rekaman jaringan tersimpan | Permintaan, respons, payload yang terekspos, dan lokasi sumber | HAR; mitmdump di Linux untuk rekaman native mitmproxy; [panduan rekaman](docs/web-network-captures.md) |
|
||||
| Assembly .NET | Metadata, instruksi CIL, dependensi native yang dideklarasikan, dan perbandingan build | Tidak ada (hanya analisis statis); [panduan kode terkelola](docs/managed-code-analysis.md) |
|
||||
| APK Android | Deklarasi manifest, kelas, metode hasil dekompilasi, dan referensi | JADX mode headless dan JDK lengkap di Linux/macOS/Windows x64; [panduan Android](docs/android-analysis.md) |
|
||||
| Perangkat Android | Perangkat, paket, proses, log, dump, tangkapan layar, serta transfer APK/berkas | adb buatan pemanggil (emulator atau perangkat); [panduan ADB](docs/adb-device-analysis.md) |
|
||||
| Proyek JEB | Unit proyek bertenaga mesin, hash artefak, dan pseudokode tipe/metode | Klien JEB yang dijalankan pemanggil dan menyajikan MCP, dipilih dengan REA_JEB_MCP_URL; [panduan JEB](docs/jeb-analysis.md) |
|
||||
| Resource Android | Manifest terdekode, tabel string, lokal, dan fakta versi | Peluncur Apktool buatan pemanggil; [panduan resource](docs/apktool-resource-analysis.md) |
|
||||
| Firmware | Wilayah, hasil ekstraksi, dan pengalihan ke analisis native | Binwalk / Unblob di Linux; [panduan firmware](docs/firmware-analysis.md) |
|
||||
| Paket dan sumber daya | Inventaris berkas, digest, plist, struktur paket Apple, dan sumber daya yang diekstrak | [Panduan artefak dan JavaScript](docs/javascript-artifact-reconstruction.md), [aplikasi Apple](docs/apple-application-analysis.md) |
|
||||
| Perilaku proses | Keluaran terminal, interaksi, pengamatan proses keluar dan sistem berkas, serta perbandingan eksekusi | Linux/macOS dengan PTY native; [perekaman proses](docs/process-capture.md) |
|
||||
|
||||
Pemeriksaan JavaScript dan .NET statis membaca berkas yang disediakan tanpa menjalankan aplikasinya. Perekaman runtime menjalankan atau berinteraksi dengan target yang dipilih menggunakan izin pengguna Anda. Setiap panduan runtime menjelaskan dampaknya.
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
Format native dan dukungan sistem host berbeda-beda menurut penyedia. Lihat [penyiapan Hopper dan Ghidra](docs/installation.md#hopper), [panduan IDA](docs/ida-provider.md), dan [dukungan eksperimental Ghidra di Windows](docs/windows-ghidra-p0.md).
|
||||
|
||||
Ghidra juga mendukung [analisis DOS 16-bit](docs/ghidra-dos.md).
|
||||
|
||||
Untuk biner berukuran besar, tingkatkan batas waktu startup menggunakan `REA_GHIDRA_STARTUP_TIMEOUT_MS`.
|
||||
|
||||
Untuk memilih penyedia, lihat [panduan CLI](docs/cli.md#choose-a-provider).
|
||||
|
||||
Periksa [ketersediaan rilis](docs/installation.md#released-package-and-main) untuk mengetahui fitur yang ditambahkan setelah rilis npm terbaru.
|
||||
|
||||
## Contoh Penggunaan
|
||||
|
||||
[](https://rea.tools/showcase/)
|
||||
|
||||
### DX-Ball: merekonstruksi perhitungan sound-pan
|
||||
|
||||
Ikuti pemanggilan suara hingga ke fungsi pembantu yang mengubah posisi menjadi nilai pan, periksa instruksinya, lalu ubah pseudocode yang belum lengkap menjadi C. Hasil rekonstruksi tersebut lolos pada 3.205 kasus x86 asli dan mereproduksi seluruh 63 byte fungsi hasil kompilasi.
|
||||
|
||||
[Baca studi kasus](https://rea.tools/showcase/dx-ball/) · [Repositori rekonstruksi](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion: menelusuri jembatan clipboard Electron
|
||||
|
||||
Temukan API clipboard pada renderer, telusuri jalurnya melalui preload dan IPC hingga ke proses utama, lalu periksa format clipboard kaya.
|
||||
|
||||
[Baca studi kasus](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04: memulihkan perhitungan bullet-ring DOS
|
||||
|
||||
Periksa instruksi 16-bit dari gim PC-98 asli, pulihkan perhitungan sudut tetap dan sudut yang diarahkan, lalu bandingkan C++ hasil rekonstruksi dengan keluaran historis kompiler.
|
||||
|
||||
[Baca studi kasus](https://rea.tools/showcase/th04/) · [Repositori rekonstruksi](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
Jika Anda telah menggunakan REA untuk menganalisis sesuatu yang menarik, kami ingin melihat hasilnya. Bagikan kasus Anda melalui [issue](https://github.com/morluto/rea/issues) atau [pull request](https://github.com/morluto/rea/pulls), sertakan target, pertanyaan Anda, bagaimana REA membantu, dan apa yang Anda temukan.
|
||||
|
||||
## FAQ
|
||||
|
||||
<details>
|
||||
<summary><strong>Agen apa saja yang dapat menggunakan REA?</strong></summary>
|
||||
|
||||
Agen apa pun yang mendukung server MCP lokal dapat menggunakan REA. Proses penyiapan mengonfigurasi [agen yang didukung](docs/installation.md#supported-agents). Klien lain dapat menggunakan [pendaftaran MCP manual](docs/installation.md#mcp-registry).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Apakah saya memerlukan Hopper, Ghidra, atau IDA?</strong></summary>
|
||||
|
||||
Analisis native mendalam menggunakan salah satu dari ketiganya. Pemeriksaan JavaScript dan .NET statis dapat dilakukan tanpa mesin analisis native.
|
||||
|
||||
Proses penyiapan dapat memasang Hopper setelah memperoleh persetujuan. Ghidra dan IDA menggunakan instalasi yang sudah tersedia. Lihat [penyiapan penyedia](docs/installation.md#hopper).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Apakah saya harus menjalankan Hopper terlebih dahulu?</strong></summary>
|
||||
|
||||
REA akan menjalankan Hopper saat suatu operasi membutuhkannya. Di macOS, dialog saat pertama kali dijalankan mungkin meminta Anda memilih mode demo atau mengaktifkan lisensi.
|
||||
|
||||
Lihat [startup dan pemecahan masalah Hopper](docs/installation.md#launcher-paths-and-troubleshooting).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Apa fungsi memasang skill dari skills.sh?</strong></summary>
|
||||
|
||||
Skill tersebut menyediakan instruksi investigasi untuk agen Anda. Gunakan `npx rea-agents setup` untuk mendaftarkan server MCP REA dan memasang instruksi yang sesuai, lalu mulai ulang agen Anda.
|
||||
|
||||
Lihat [instalasi khusus skill](docs/installation.md#skill-only-installation).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Kode seperti apa yang dihasilkan REA?</strong></summary>
|
||||
|
||||
Analisis native menghasilkan pseudocode dan assembly. Analisis JavaScript/Electron memulihkan modul dan hubungan antarmodulnya. Agen Anda menggunakan temuan tersebut untuk menulis dan menguji implementasi. [Contoh penggunaan](#contoh-penggunaan) menyediakan contoh yang telah dikerjakan.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Apakah REA mengunggah aplikasi saya?</strong></summary>
|
||||
|
||||
REA menganalisis target secara lokal. Agen Anda menerima hasil alat, sedangkan penyedia model yang digunakannya memiliki kebijakan datanya sendiri.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Apa yang harus saya lakukan jika menemukan bug?</strong></summary>
|
||||
|
||||
Perbarui terlebih dahulu. Rilis terbaru mungkin sudah memperbaiki masalah tersebut.
|
||||
|
||||
Untuk CLI yang dipasang melalui npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Untuk penyiapan agen melalui `npx`:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Jika Anda menggunakan agen, selesaikan [pembaruan penyiapan](#perbarui-rea) dan mulai ulang agen tersebut.
|
||||
|
||||
Coba kembali tugas yang sama. Jika masalah masih terjadi, [buat issue](https://github.com/morluto/rea/issues) dengan menyertakan versi REA, jenis target, langkah-langkah untuk mereproduksi masalah, dan keluaran kesalahan.
|
||||
|
||||
</details>
|
||||
|
||||
## Dokumentasi
|
||||
|
||||
Mulailah dengan [panduan praktik di situs web](https://rea.tools/guides/).
|
||||
|
||||
Untuk mengetahui opsi, prasyarat, dan kontrak hasil secara tepat:
|
||||
|
||||
- [Instalasi dan penyiapan](docs/installation.md): pendaftaran agen, konfigurasi penyedia, pembaruan, dan penghapusan instalasi.
|
||||
- [Kesiapan dan pemecahan masalah](docs/installation.md#check-readiness-for-your-task): mendiagnosis satu agen atau mesin analisis.
|
||||
- [CLI dan Bukti](docs/cli.md): perintah, pemilihan penyedia, snapshot, impor/ekspor, dan status keluar.
|
||||
- [Kontrak MCP](docs/mcp-contracts.md) dan [prompt agen](docs/mcp-prompts.md): hasil alat, sesi, dan investigasi terpandu.
|
||||
- [Katalog alat](docs/mcp-contracts.md#generated-catalog): inventaris alat, penyedia, dan perintah CLI yang dibuat secara otomatis.
|
||||
- [Peta jalan](docs/roadmap.md): pekerjaan yang direncanakan dan pelacak kemampuan.
|
||||
|
||||
Laporkan kerentanan melalui [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Riwayat Bintang
|
||||
|
||||
🎉 **50.000 bintang GitHub, terima kasih!**
|
||||
|
||||
Terima kasih kepada semua orang yang menggunakan REA, melaporkan bug, meminta fitur, menguji build, dan berkontribusi dalam perbaikan.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
<img alt="Riwayat bintang GitHub REA" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Penafian
|
||||
|
||||
REA menyediakan alat untuk penelitian, analisis, dan rekonstruksi rekayasa balik yang sah. Anda bertanggung jawab untuk memperoleh otorisasi yang diperlukan dan mematuhi hukum yang berlaku. Proyek ini tidak mendukung penggunaan ilegal atau tanpa izin.
|
||||
|
||||
REA adalah proyek perangkat lunak sumber terbuka. Kami tidak menerbitkan atau mendukung mata uang kripto maupun token apa pun. Token yang menggunakan nama REA tidak berafiliasi dengan proyek ini.
|
||||
|
||||
## Kontribusi
|
||||
|
||||
Kami sangat menghargai bantuan Anda untuk REA! [Buat issue](https://github.com/morluto/rea/issues) untuk melaporkan bug atau menyarankan fitur, atau [kirim pull request](https://github.com/morluto/rea/pulls) untuk meningkatkan kode atau dokumentasi.
|
||||
|
||||
Lihat [CONTRIBUTING.md](CONTRIBUTING.md) untuk penyiapan pengembangan dan pemeriksaan, [pengujian](docs/testing.md) untuk tahapan verifikasi, serta [peta arsitektur](docs/architecture.mermaid) untuk struktur proyek.
|
||||
|
||||
## Lisensi
|
||||
|
||||
[MIT](LICENSE)
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · **Italiano**
|
||||
|
||||
# REA: Reverse Engineer Anything
|
||||
|
||||
### Un unico MCP per l'ingegneria inversa su binari, applicazioni e comportamento a runtime.
|
||||
|
||||
**Vedi una funzionalità che ti piace. Capisci come funziona, fino al livello binario.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#cosa-puoi-analizzare)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[Sito web](https://rea.tools/) · [Guide](https://rea.tools/guides/) · [Showcase](https://rea.tools/showcase/)**
|
||||
|
||||
[Quick start](#quick-start) · [Come funziona REA](#come-funziona-rea) · [Cosa puoi analizzare](#cosa-puoi-analizzare) · [Showcase](#showcase) · [FAQ](#faq) · [Documentazione](#documentazione)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA che avvia il suo bridge di analisi dentro Hopper mentre ispeziona un binario nativo" width="1200" />
|
||||
|
||||
<br /><br />
|
||||
|
||||
<table aria-label="Community REA">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>Unisciti alla community di ingegneria inversa</strong>
|
||||
</a><br />
|
||||
<sub>Discord · Domande e risposte · Show and Tell</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Hai visto una funzionalità in un'app che vorresti nel tuo prodotto? Chiedi al tuo agente di analizzarla con REA. Può ispezionare l'app senza il suo codice sorgente, spiegare come funziona la funzionalità, mostrare le prove e realizzare una versione per il tuo progetto.
|
||||
|
||||
REA collega il tuo agente a strumenti per ispezionare binari nativi, app JavaScript ed Electron, assembly .NET e siti web. Puoi usare gli stessi strumenti anche dal terminale. L'analisi viene eseguita localmente e i risultati includono le prove e i limiti dietro ogni conclusione.
|
||||
|
||||
La configurazione registra REA con il tuo agente e installa le istruzioni di workflow corrispondenti. L'analisi nativa può usare un'installazione esistente di Hopper, Ghidra o IDA; la configurazione può installare opzionalmente Hopper previa approvazione. L'analisi statica JavaScript non richiede alcun motore di analisi nativo.
|
||||
|
||||
> **[Visita il sito web di REA](https://rea.tools/)** per istruzioni di configurazione, guide illustrate e veri casi studio.
|
||||
|
||||
## Quick start
|
||||
|
||||
### Configura il tuo agente
|
||||
|
||||
Con Node.js e npm installati, esegui:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
Scegli i tuoi agenti, rivedi le modifiche proposte e approvale. La configurazione aggiunge il server MCP di REA e le istruzioni di workflow corrispondenti, con backup della configurazione esistente. Riavvia quindi il tuo agente.
|
||||
|
||||
La configurazione supporta Claude Code, Codex, Cursor, Gemini CLI, Grok Build e [altri agenti](docs/installation.md#supported-agents). Vedi [installazione e configurazione](docs/installation.md) per la configurazione dei provider e la registrazione manuale MCP.
|
||||
|
||||
### Chiedi al tuo agente
|
||||
|
||||
```text
|
||||
Spiega come funziona la ricerca nell'app Note, mostrami le prove e realizza
|
||||
una funzionalità simile per il mio progetto.
|
||||
```
|
||||
|
||||
Sostituisci Note con la tua app di destinazione e la funzionalità che vuoi capire.
|
||||
|
||||
### Usa il terminale
|
||||
|
||||
Ispeziona una directory di app JavaScript/Electron estratta o un ASAR:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /percorso/assoluto/verso/app --json
|
||||
```
|
||||
|
||||
Il risultato include moduli, import, confini Electron e le relative prove. Sostituisci il percorso con il tuo bersaglio, ad esempio `"D:/apps/example"` su Windows.
|
||||
|
||||
Per installare il comando `rea` per un uso regolare:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
```
|
||||
|
||||
Per l'analisi nativa, configura prima un provider. Vedi la [guida CLI ed Evidence](docs/cli.md) per i comandi nativi, la selezione del provider, gli snapshot e lo scripting.
|
||||
|
||||
### Aggiorna REA
|
||||
|
||||
REA cambia rapidamente e le nuove versioni includono correzioni di bug frequenti. Tieni aggiornata la tua installazione.
|
||||
|
||||
Per una CLI installata via npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Per aggiornare le registrazioni del tuo agente e lo skill, esegui il comando di configurazione mostrato dall'aggiornamento.
|
||||
|
||||
Se usi `npx`, aggiorna la configurazione del tuo agente con:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Rivedi le modifiche di configurazione e riavvia il tuo agente. Per comandi CLI singoli, usa `npx rea-agents@latest` seguito dal comando.
|
||||
|
||||
## Come funziona REA
|
||||
|
||||
Il tuo agente chiama REA tramite MCP per ispezionare il bersaglio e tracciare il codice rilevante. REA restituisce i risultati con le relative prove. L'agente li usa per fare domande di approfondimento, spiegare il comportamento o scrivere e testare un'implementazione. Anche i comandi CLI usano gli stessi workflow.
|
||||
|
||||

|
||||
|
||||
[Apri la figura a dimensione intera](website/public/assets/figures/rea-investigation-flow.svg).
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## Cosa puoi analizzare
|
||||
|
||||
REA richiede Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, oltre a npm. Strumenti aggiuntivi e supporto host dipendono dal bersaglio:
|
||||
|
||||
| Obiettivo | Cosa restituisce REA | Requisiti e guida |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Binari nativi | Pseudocodice, assembly, stringhe, simboli, chiamate e riferimenti | Hopper, Ghidra o IDA; [analisi nativa](https://rea.tools/guides/native/) |
|
||||
| Layout ELF offline | Sezioni, segmenti, simboli/relocazioni originali e candidati mitigazioni statiche | pwntools fornito dal chiamante su Linux x64; [diagnostica binaria](docs/binary-diagnostics.md) |
|
||||
| Bytecode EVM | Selettori di dispatch, offset dei byte, argomenti inferiti e mutabilità | Carrier raw/hex locale; [guida EVM offline](docs/evm-bytecode.md) |
|
||||
| Crash Linux registrati | Note grezze, registri/segnai di ogni thread registrato e candidati mapping opzionali | pwntools fornito dal chiamante; GDB/pwndbg opzionale; [crash registrati](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Moduli, import, source map, route, IPC e relazioni con add-on nativi | Node.js e npm; [analisi delle applicazioni](https://rea.tools/guides/javascript/) |
|
||||
| Siti web | Struttura della pagina, script, osservazioni di rete e screenshot richiesti | Un browser della famiglia Chrome; [analisi browser](https://rea.tools/guides/browser/) |
|
||||
| Acquisizioni di rete salvate | Richieste, risposte, payload esposti e posizioni sorgente | HAR; mitmdump su Linux per acquisizioni mitmproxy native; [guida alle acquisizioni](docs/web-network-captures.md) |
|
||||
| Assembly .NET | Metadati, istruzioni CIL, dipendenze native dichiarate e confronti di build | Nessuno (solo statico); [guida managed-code](docs/managed-code-analysis.md) |
|
||||
| APK Android | Dichiarazioni manifest, classi, metodi decompilati e riferimenti | JADX headless e un JDK completo su Linux/macOS/Windows x64; [guida Android](docs/android-analysis.md) |
|
||||
| Dispositivi Android | Dispositivi, pacchetti, processi, log, dump, screenshot e trasferimenti APK/file | adb fornito dal chiamante (emulatore o hardware); [guida ADB](docs/adb-device-analysis.md) |
|
||||
| Progetti JEB | Unità di progetto dal motore, digest degli artifact e pseudocodice di tipi/metodi | Client JEB avviato dal chiamante via MCP, selezionato con REA_JEB_MCP_URL; [guida JEB](docs/jeb-analysis.md) |
|
||||
| Risorse Android | Manifest decodificato, tabelle stringhe, lingue e dati di versione | Launcher Apktool fornito dal chiamante; [guida alle risorse](docs/apktool-resource-analysis.md) |
|
||||
| Firmware | Regioni, risultati di estrazione e passaggi all'analisi nativa | Binwalk / Unblob su Linux; [guida firmware](docs/firmware-analysis.md) |
|
||||
| Pacchetti e risorse | Inventari file, digest, plist, anatomia dei bundle Apple e risorse estratte | [Guida artifact e JavaScript](docs/javascript-artifact-reconstruction.md), [Applicazioni Apple](docs/apple-application-analysis.md) |
|
||||
| Comportamento dei processi | Output del terminale, interazioni, osservazioni su uscita e filesystem ed confronti di esecuzione | Linux/macOS con un PTY nativo; [cattura dei processi](docs/process-capture.md) |
|
||||
|
||||
L'ispezione statica di JavaScript e .NET legge i file forniti senza eseguire l'applicazione. La cattura a runtime esegue o interagisce con il bersaglio selezionato usando i tuoi permessi utente; ogni guida runtime descrive i suoi effetti.
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
I formati nativi e il supporto host variano in base al provider. Vedi la [configurazione di Hopper e Ghidra](docs/installation.md#hopper), la [guida IDA](docs/ida-provider.md) e il [supporto sperimentale Windows Ghidra](docs/windows-ghidra-p0.md). Ghidra supporta anche l'[analisi DOS a 16 bit](docs/ghidra-dos.md). Per binari di grandi dimensioni, aumenta il timeout di avvio con `REA_GHIDRA_STARTUP_TIMEOUT_MS`. Per la selezione del provider, vedi la [guida CLI](docs/cli.md#choose-a-provider). Controlla la [disponibilità delle release](docs/installation.md#released-package-and-main) per le funzionalità aggiunte dall'ultima release npm.
|
||||
|
||||
## Showcase
|
||||
|
||||
[](https://rea.tools/showcase/)
|
||||
|
||||
### DX-Ball: ricostruisci un calcolo sound-pan
|
||||
|
||||
Segui una chiamata sonora fino al suo helper posizione-to-pan, ispeziona le istruzioni e trasforma il pseudocodice incompleto in C. La ricostruzione supera 3.205 casi x86 originali e riproduce tutti i 63 byte di funzione compilati.
|
||||
|
||||
[Leggi il case study](https://rea.tools/showcase/dx-ball/) · [Repository di ricostruzione](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion: traccia il clipboard bridge di Electron
|
||||
|
||||
Trova l'API clipboard del renderer, seguila attraverso preload e IPC fino al processo principale e ispeziona il formato rich clipboard.
|
||||
|
||||
[Leggi il case study](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04: recupera un calcolo dell'anello di proiettili DOS
|
||||
|
||||
Ispeziona le istruzioni a 16 bit del gioco PC-98 originale, recupera i calcoli dell'angolo fisso e mirato e confronta il C++ ricostruito con l'output del compilatore storico.
|
||||
|
||||
[Leggi il case study](https://rea.tools/showcase/th04/) · [Repository di ricostruzione](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
Se hai usato REA su qualcosa di interessante, ci piacerebbe vederlo. Condividi il tuo caso in una [issue](https://github.com/morluto/rea/issues) o in una [pull request](https://github.com/morluto/rea/pulls), includendo il bersaglio, la tua domanda, come REA ti ha aiutato e cosa hai scoperto.
|
||||
|
||||
## FAQ
|
||||
|
||||
<details>
|
||||
<summary><strong>Quali agenti possono usare REA?</strong></summary>
|
||||
|
||||
Qualsiasi agente che supporti server MCP locali. La configurazione configura gli [agenti supportati](docs/installation.md#supported-agents); altri client possono usare la [registrazione MCP manuale](docs/installation.md#mcp-registry).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Ho bisogno di Hopper, Ghidra o IDA?</strong></summary>
|
||||
|
||||
L'analisi nativa profonda ne usa uno. L'ispezione statica di JavaScript e .NET funziona senza un motore di analisi nativo. La configurazione può installare Hopper previa approvazione; Ghidra e IDA usano le tue installazioni esistenti. Vedi [configurazione provider](docs/installation.md#hopper).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Devo avviare Hopper per primo?</strong></summary>
|
||||
|
||||
REA avvia Hopper quando un'operazione ne ha bisogno. Su macOS, una finestra di dialogo al primo avvio potrebbe chiederti di scegliere la modalità demo o di attivare la licenza. Vedi [avvio di Hopper e risoluzione dei problemi](docs/installation.md#launcher-paths-and-troubleshooting).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Cosa fa l'installazione dello skill da skills.sh?</strong></summary>
|
||||
|
||||
Lo skill fornisce istruzioni di indagine per il tuo agente. Usa `npx rea-agents setup` per registrare il server MCP di REA e installare le istruzioni corrispondenti, quindi riavvia il tuo agente. Vedi [installazione solo skill](docs/installation.md#skill-only-installation).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Quale codice restituisce REA?</strong></summary>
|
||||
|
||||
L'analisi nativa restituisce pseudocodice e assembly. L'analisi JavaScript/Electron recupera moduli e le loro relazioni. Il tuo agente usa questi risultati per scrivere e testare un'implementazione; gli [showcase](#showcase) offrono esempi concreti.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>REA carica la mia app da qualche parte?</strong></summary>
|
||||
|
||||
REA analizza i bersagli localmente. Il tuo agente riceve i risultati degli strumenti e il suo provider di modello ha la propria politica sui dati.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Cosa devo fare se trovo un bug?</strong></summary>
|
||||
|
||||
Aggiorna prima; una release recente potrebbe averlo già risolto.
|
||||
|
||||
Per una CLI installata via npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Per la configurazione dell'agente tramite `npx`:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Se stai usando un agente, completa l'[aggiornamento della configurazione](#aggiorna-rea) e riavvialo. Riprova lo stesso task. Se il problema persiste, [apri un issue](https://github.com/morluto/rea/issues) con la tua versione di REA, il tipo di bersaglio, i passaggi per riprodurlo e l'output di errore.
|
||||
|
||||
</details>
|
||||
|
||||
## Documentazione
|
||||
|
||||
Inizia dalle [guide pratiche](https://rea.tools/guides/) del sito web. Per opzioni esatte, prerequisiti e contratti dei risultati:
|
||||
|
||||
- [Installazione e configurazione](docs/installation.md): registrazione agente, configurazione provider, aggiornamenti e disinstallazione.
|
||||
- [Pronto all'uso e risoluzione dei problemi](docs/installation.md#check-readiness-for-your-task): diagnostica un agente o un motore di analisi.
|
||||
- [CLI ed Evidence](docs/cli.md): comandi, selezione provider, snapshot, import/export e stati di uscita.
|
||||
- [Contratti MCP](docs/mcp-contracts.md) e [prompt agente](docs/mcp-prompts.md): risultati degli strumenti, sessioni e indagini guidate.
|
||||
- [Catalogo strumenti](docs/mcp-contracts.md#generated-catalog): inventario generato di strumenti, provider e comandi CLI.
|
||||
- [Roadmap](docs/roadmap.md): lavoro pianificato e tracker delle capacità.
|
||||
|
||||
Segnala vulnerabilità tramite [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Cronologia delle stelle
|
||||
|
||||
🎉 **50.000 stelle su GitHub — grazie!**
|
||||
|
||||
Grazie a tutti coloro che usano REA, segnalano bug, propongono funzionalità, testano le build e contribuiscono con correzioni.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
<img alt="Cronologia stelle GitHub di REA" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Disclaimer
|
||||
|
||||
REA fornisce strumenti per la ricerca, l'analisi e la ricostruzione di ingegneria inversa lecita. Sei responsabile di ottenere le eventuali autorizzazioni richieste e di rispettare le leggi applicabili. Il progetto non approva l'uso illegale o non autorizzato.
|
||||
|
||||
REA è un progetto software open-source. Non abbiamo emesso né approvato alcuna criptovaluta o token. I token che usano il nome REA non sono affiliati al progetto.
|
||||
|
||||
## Contributing
|
||||
|
||||
Apprezzeremmo il tuo aiuto con REA! [Apri un issue](https://github.com/morluto/rea/issues) per segnalare un bug o suggerire una funzionalità, oppure [invia una pull request](https://github.com/morluto/rea/pulls) per migliorare il codice o la documentazione.
|
||||
|
||||
Vedi [CONTRIBUTING.md](CONTRIBUTING.md) per la configurazione di sviluppo e i controlli, [testing](docs/testing.md) per le lane di verifica e la [mappa dell'architettura](docs/architecture.mermaid) per la struttura del progetto.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
+18
-15
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · **日本語** · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · **日本語** · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
|
||||
|
||||
# REA:あらゆるものをリバースエンジニアリング
|
||||
|
||||
@@ -131,20 +131,23 @@ npx rea-agents@latest setup
|
||||
|
||||
REA には Node.js 22.x(>=22.19)、24.x(>=24.11)、または 26+ と npm が必要です。追加のツールや対応ホストは、対象によって異なります。
|
||||
|
||||
| 対象 | REA が返すもの | 要件とガイド |
|
||||
| -------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](https://rea.tools/guides/native/) |
|
||||
| オフライン ELF レイアウト | セクション、セグメント、元のシンボル/再配置情報、静的解析によるセキュリティ対策の候補 | Linux x64 で利用者が用意する pwntools;[バイナリ診断](docs/binary-diagnostics.md) |
|
||||
| EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | ローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](docs/evm-bytecode.md) |
|
||||
| 記録済みの Linux クラッシュ | 生の note レコード、記録された全スレッドのレジスター/シグナル、任意のマッピング候補 | 利用者が用意する pwntools;GDB/pwndbg は任意;[記録済みクラッシュ](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、IPC、ネイティブアドオンの関係 | Node.js と npm;[アプリケーション分析](https://rea.tools/guides/javascript/) |
|
||||
| ウェブサイト | ページ構造、スクリプト、ネットワークの観測結果、依頼されたスクリーンショット | Chrome 系ブラウザー;[ブラウザー分析](https://rea.tools/guides/browser/) |
|
||||
| 保存済みのネットワークキャプチャ | リクエスト、レスポンス、取得可能なペイロード、ソース内の位置 | HAR;mitmproxy ネイティブ形式のキャプチャには Linux 上の mitmdump;[キャプチャガイド](docs/web-network-captures.md) |
|
||||
| .NET アセンブリ | メタデータ、CIL 命令、宣言されたネイティブ依存関係、ビルドの比較 | 静的な検査;[マネージドコードのガイド](docs/managed-code-analysis.md) |
|
||||
| Android APK | マニフェストの宣言、クラス、逆コンパイルされたメソッド、参照 | Linux/macOS 上のヘッドレス JADX と完全な JDK;[Android ガイド](docs/android-analysis.md) |
|
||||
| ファームウェア | 領域、抽出結果、ネイティブ分析への引き継ぎ | Linux 上の Binwalk / Unblob;[ファームウェアガイド](docs/firmware-analysis.md) |
|
||||
| パッケージとリソース | ファイル一覧、ダイジェスト、plist、Apple バンドルの構造、抽出されたリソース | [アーティファクトと JavaScript のガイド](docs/javascript-artifact-reconstruction.md)、[Apple アプリケーション](docs/apple-application-analysis.md) |
|
||||
| プロセスの動作 | ターミナル出力、対話、終了とファイルシステムの観測結果、実行の比較 | ネイティブ PTY を備えた Linux/macOS;[プロセスキャプチャ](docs/process-capture.md) |
|
||||
| 対象 | REA が返すもの | 要件とガイド |
|
||||
| -------------------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](https://rea.tools/guides/native/) |
|
||||
| オフライン ELF レイアウト | セクション、セグメント、元のシンボル/再配置情報、静的解析によるセキュリティ対策の候補 | Linux x64 で利用者が用意する pwntools;[バイナリ診断](docs/binary-diagnostics.md) |
|
||||
| EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | Linux x64 上のローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](docs/evm-bytecode.md) |
|
||||
| 記録済みの Linux クラッシュ | 生の note レコード、記録された全スレッドのレジスター/シグナル、任意のマッピング候補 | Linux x64 で利用者が用意する pwntools;GDB/pwndbg は任意;[記録済みクラッシュ](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、IPC、ネイティブアドオンの関係 | Node.js と npm;[アプリケーション分析](https://rea.tools/guides/javascript/) |
|
||||
| ウェブサイト | ページ構造、スクリプト、ネットワークの観測結果、依頼されたスクリーンショット | Chrome 系ブラウザー;[ブラウザー分析](https://rea.tools/guides/browser/) |
|
||||
| 保存済みのネットワークキャプチャ | リクエスト、レスポンス、取得可能なペイロード、ソース内の位置 | HAR;mitmproxy ネイティブ形式のキャプチャには Linux 上の mitmdump;[キャプチャガイド](docs/web-network-captures.md) |
|
||||
| .NET アセンブリ | メタデータ、CIL 命令、宣言されたネイティブ依存関係、ビルドの比較 | 静的な検査;[マネージドコードのガイド](docs/managed-code-analysis.md) |
|
||||
| Android APK | マニフェストの宣言、クラス、逆コンパイルされたメソッド、参照 | Linux/macOS/Windows x64 上のヘッドレス JADX と完全な JDK;[Android ガイド](docs/android-analysis.md) |
|
||||
| Android デバイス | デバイス、パッケージ、プロセス、ログ、ダンプ、スクリーンキャプチャ、APK/ファイル転送 | 呼び出し側が用意した adb(エミュレーターまたは実機);[ADB ガイド](docs/adb-device-analysis.md) |
|
||||
| JEBプロジェクト | エンジンによるプロジェクトユニット、アーティファクトのダイジェスト、型/メソッドの擬似コード | 呼び出し側が起動し MCP を提供する JEB クライアント(REA_JEB_MCP_URL で選択);[JEB ガイド](docs/jeb-analysis.md) |
|
||||
| Android リソース | デコードされたマニフェスト、文字列テーブル、ロケール、バージョン情報 | 呼び出し側が用意した Apktool ランチャー;[リソースガイド](docs/apktool-resource-analysis.md) |
|
||||
| ファームウェア | 領域、抽出結果、ネイティブ分析への引き継ぎ | Linux 上の Binwalk / Unblob;[ファームウェアガイド](docs/firmware-analysis.md) |
|
||||
| パッケージとリソース | ファイル一覧、ダイジェスト、plist、Apple バンドルの構造、抽出されたリソース | [アーティファクトと JavaScript のガイド](docs/javascript-artifact-reconstruction.md)、[Apple アプリケーション](docs/apple-application-analysis.md) |
|
||||
| プロセスの動作 | ターミナル出力、対話、終了とファイルシステムの観測結果、実行の比較 | ネイティブ PTY を備えた Linux/macOS;[プロセスキャプチャ](docs/process-capture.md) |
|
||||
|
||||
静的な JavaScript と .NET の検査は、指定されたファイルを読み取り、アプリケーションを実行しません。実行時キャプチャは、あなたのユーザー権限で対象を実行したり、対象とやり取りしたりします。それぞれの実行時ガイドに、どのような操作が行われるかを記載しています。
|
||||
|
||||
|
||||
+7
-4
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · **한국어** · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · **한국어** · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
|
||||
|
||||
# REA: 무엇이든 리버스 엔지니어링
|
||||
|
||||
@@ -135,13 +135,16 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
|
||||
| --------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 네이티브 바이너리 | 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조 | Hopper, Ghidra 또는 IDA; [네이티브 분석](https://rea.tools/guides/native/) |
|
||||
| 오프라인 ELF 레이아웃 | 섹션, 세그먼트, 원본 심볼/재배치 정보, 정적 분석으로 얻은 보안 완화 기법 후보 | Linux x64에서 호출자가 제공하는 pwntools; [바이너리 진단](docs/binary-diagnostics.md) |
|
||||
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](docs/evm-bytecode.md) |
|
||||
| 기록된 Linux 크래시 | 원시 note 레코드, 기록된 모든 스레드의 레지스터/시그널, 선택적 매핑 후보 | 호출자가 제공하는 pwntools; GDB/pwndbg는 선택 사항; [기록된 크래시](docs/recorded-crashes.md) |
|
||||
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](docs/evm-bytecode.md) |
|
||||
| 기록된 Linux 크래시 | 원시 note 레코드, 기록된 모든 스레드의 레지스터/시그널, 선택적 매핑 후보 | Linux x64에서 호출자가 제공하는 pwntools; GDB/pwndbg는 선택 사항; [기록된 크래시](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | 모듈, 임포트, 소스 맵, 라우트, IPC, 네이티브 애드온 관계 | Node.js와 npm; [애플리케이션 분석](https://rea.tools/guides/javascript/) |
|
||||
| 웹사이트 | 페이지 구조, 스크립트, 네트워크 관찰 결과, 요청한 스크린샷 | Chrome 계열 브라우저; [브라우저 분석](https://rea.tools/guides/browser/) |
|
||||
| 저장된 네트워크 캡처 | 요청, 응답, 접근 가능한 페이로드, 소스 위치 | HAR; mitmproxy 네이티브 캡처에는 Linux의 mitmdump; [캡처 가이드](docs/web-network-captures.md) |
|
||||
| .NET 어셈블리 | 메타데이터, CIL 명령, 선언된 네이티브 의존성, 빌드 비교 | 정적 검사; [관리 코드 가이드](docs/managed-code-analysis.md) |
|
||||
| Android APK | 매니페스트 선언, 클래스, 디컴파일된 메서드, 참조 | Linux/macOS의 헤드리스 JADX와 전체 JDK; [Android 가이드](docs/android-analysis.md) |
|
||||
| Android APK | 매니페스트 선언, 클래스, 디컴파일된 메서드, 참조 | Linux/macOS/Windows x64의 헤드리스 JADX와 전체 JDK; [Android 가이드](docs/android-analysis.md) |
|
||||
| Android 기기 | 기기, 패키지, 프로세스, 로그, 덤프, 화면 캡처, APK/파일 전송 | 호출자 제공 adb(에뮬레이터 또는 실제 기기); [ADB 가이드](docs/adb-device-analysis.md) |
|
||||
| JEB 프로젝트 | 엔진 기반 프로젝트 유닛, 아티팩트 해시, 타입/메서드 의사코드 | 호출자가 시작하고 MCP를 제공하는 JEB 클라이언트(REA_JEB_MCP_URL로 선택); [JEB 가이드](docs/jeb-analysis.md) |
|
||||
| Android 리소스 | 디코딩된 매니페스트, 문자열 테이블, 로캘, 버전 정보 | 호출자 제공 Apktool 런처; [리소스 가이드](docs/apktool-resource-analysis.md) |
|
||||
| 펌웨어 | 영역, 추출 결과, 네이티브 분석으로 전달할 정보 | Linux의 Binwalk / Unblob; [펌웨어 가이드](docs/firmware-analysis.md) |
|
||||
| 패키지 및 리소스 | 파일 목록, 다이제스트, plist, Apple 번들 구조, 추출한 리소스 | [아티팩트 및 JavaScript 가이드](docs/javascript-artifact-reconstruction.md), [Apple 애플리케이션](docs/apple-application-analysis.md) |
|
||||
| 프로세스 동작 | 터미널 출력, 상호작용, 종료 및 파일 시스템 관찰 결과, 실행 비교 | 네이티브 PTY를 지원하는 Linux/macOS; [프로세스 캡처](docs/process-capture.md) |
|
||||
|
||||
+7
-4
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · **Polski** · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · **Polski** · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
|
||||
|
||||
# REA: Reverse Engineer Anything
|
||||
|
||||
@@ -135,13 +135,16 @@ REA wymaga Node.js 22.x (>=22.19), 24.x (>=24.11) lub 26+ oraz npm. Dodatkowe na
|
||||
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Natywne pliki binarne | Pseudokod, kod asemblera, ciągi znaków, symbole, wywołania i odwołania | Hopper, Ghidra lub IDA; [analiza kodu natywnego](https://rea.tools/guides/native/) |
|
||||
| Układ pliku ELF w trybie offline | Sekcje, segmenty, oryginalne symbole/relokacje i potencjalne mechanizmy zabezpieczeń wskazane przez analizę statyczną | pwntools dostarczone przez użytkownika w systemie Linux x64; [diagnostyka plików binarnych](docs/binary-diagnostics.md) |
|
||||
| Kod bajtowy EVM | Selektory wywołań, przesunięcia bajtowe, wywnioskowane argumenty i mutowalność | Lokalny plik z surowymi bajtami lub zapisem szesnastkowym; [przewodnik EVM offline](docs/evm-bytecode.md) |
|
||||
| Zarejestrowane awarie systemu Linux | Surowe rekordy note, rejestry/sygnały każdego zarejestrowanego wątku i opcjonalni kandydaci na mapowania | pwntools dostarczone przez użytkownika; opcjonalnie GDB/pwndbg; [zarejestrowane awarie](docs/recorded-crashes.md) |
|
||||
| Kod bajtowy EVM | Selektory wywołań, przesunięcia bajtowe, wywnioskowane argumenty i mutowalność | Lokalny plik z surowymi bajtami lub zapisem szesnastkowym w systemie Linux x64; [przewodnik EVM offline](docs/evm-bytecode.md) |
|
||||
| Zarejestrowane awarie systemu Linux | Surowe rekordy note, rejestry/sygnały każdego zarejestrowanego wątku i opcjonalni kandydaci na mapowania | pwntools dostarczone przez użytkownika w systemie Linux x64; opcjonalnie GDB/pwndbg; [zarejestrowane awarie](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Moduły, importy, mapy źródeł, trasy, IPC i powiązania z natywnymi dodatkami | Node.js i npm; [analiza aplikacji](https://rea.tools/guides/javascript/) |
|
||||
| Strony internetowe | Struktura strony, skrypty, obserwacje sieciowe i zamówione zrzuty ekranu | Przeglądarka z rodziny Chrome; [analiza w przeglądarce](https://rea.tools/guides/browser/) |
|
||||
| Zapisane przechwycenia ruchu sieciowego | Żądania, odpowiedzi, dostępne dane przesyłane i lokalizacje źródłowe | HAR; mitmdump w systemie Linux dla natywnych przechwyceń mitmproxy; [przewodnik przechwytywania](docs/web-network-captures.md) |
|
||||
| Zestawy .NET | Metadane, instrukcje CIL, zadeklarowane zależności natywne i porównania kompilacji | Analiza statyczna; [przewodnik kodu zarządzanego](docs/managed-code-analysis.md) |
|
||||
| Pakiety APK dla Androida | Deklaracje manifestu, klasy, zdekompilowane metody i odwołania | JADX bez interfejsu graficznego i pełny JDK w systemie Linux/macOS; [przewodnik Androida](docs/android-analysis.md) |
|
||||
| Pakiety APK dla Androida | Deklaracje manifestu, klasy, zdekompilowane metody i odwołania | JADX bez interfejsu graficznego i pełny JDK w systemach Linux/macOS/Windows x64; [przewodnik Androida](docs/android-analysis.md) |
|
||||
| Urządzenia z Androidem | Urządzenia, pakiety, procesy, logi, zrzuty, zrzuty ekranu oraz transfery APK/plików | adb dostarczony przez wywołującego (emulator lub urządzenie); [przewodnik ADB](docs/adb-device-analysis.md) |
|
||||
| Projekty JEB | Jednostki projektu obsługiwane przez silnik, skróty artefaktów i pseudokod typów/metod | Uruchomiony przez wywołującego klient JEB udostępniający MCP, wybrany przez REA_JEB_MCP_URL; [przewodnik JEB](docs/jeb-analysis.md) |
|
||||
| Zasoby Androida | Zdekodowany manifest, tablice ciągów, języki i informacje o wersji | Należący do wywołującego launcher Apktool; [przewodnik zasobów](docs/apktool-resource-analysis.md) |
|
||||
| Oprogramowanie układowe | Regiony, wyniki wyodrębniania i przekazanie do analizy kodu natywnego | Binwalk / Unblob w systemie Linux; [przewodnik oprogramowania układowego](docs/firmware-analysis.md) |
|
||||
| Pakiety i zasoby | Spisy plików, skróty, pliki plist, struktura pakietów Apple i wyodrębnione zasoby | [Przewodnik artefaktów i JavaScript](docs/javascript-artifact-reconstruction.md), [aplikacje Apple](docs/apple-application-analysis.md) |
|
||||
| Zachowanie procesów | Wyjście terminala, interakcje, obserwacje zakończenia i systemu plików oraz porównania uruchomień | Linux/macOS z natywnym PTY; [przechwytywanie procesów](docs/process-capture.md) |
|
||||
|
||||
+7
-4
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · **Português (Brasil)** · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · **Português (Brasil)** · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
|
||||
|
||||
# REA: Faça engenharia reversa de qualquer coisa
|
||||
|
||||
@@ -135,13 +135,16 @@ O REA requer Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, além do npm. As fer
|
||||
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Binários nativos | Pseudocódigo, assembly, strings, símbolos, chamadas e referências | Hopper, Ghidra ou IDA; [análise nativa](https://rea.tools/guides/native/) |
|
||||
| Estrutura ELF sem execução | Seções, segmentos, símbolos/relocações originais e possíveis mecanismos de mitigação identificados por análise estática | pwntools fornecido pelo chamador no Linux x64; [diagnóstico de binários](docs/binary-diagnostics.md) |
|
||||
| Bytecode EVM | Seletores de despacho, offsets de bytes, argumentos inferidos e mutabilidade | Entrada local com bytes brutos ou representação hexadecimal; [guia de EVM sem execução](docs/evm-bytecode.md) |
|
||||
| Falhas do Linux registradas | Registros note brutos, registradores/sinais de cada thread registrada e candidatos opcionais a mapeamentos | pwntools fornecido pelo chamador; GDB/pwndbg opcionais; [falhas registradas](docs/recorded-crashes.md) |
|
||||
| Bytecode EVM | Seletores de despacho, offsets de bytes, argumentos inferidos e mutabilidade | Entrada local com bytes brutos ou representação hexadecimal no Linux x64; [guia de EVM sem execução](docs/evm-bytecode.md) |
|
||||
| Falhas do Linux registradas | Registros note brutos, registradores/sinais de cada thread registrada e candidatos opcionais a mapeamentos | pwntools fornecido pelo chamador no Linux x64; GDB/pwndbg opcionais; [falhas registradas](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Módulos, importações, source maps, rotas, IPC e relações com complementos nativos | Node.js e npm; [análise de aplicativos](https://rea.tools/guides/javascript/) |
|
||||
| Sites | Estrutura da página, scripts, observações de rede e capturas de tela solicitadas | Um navegador da família Chrome; [análise do navegador](https://rea.tools/guides/browser/) |
|
||||
| Capturas de rede salvas | Requisições, respostas, payloads acessíveis e localizações de origem | HAR; mitmdump no Linux para capturas no formato nativo do mitmproxy; [guia de capturas](docs/web-network-captures.md) |
|
||||
| Assemblies .NET | Metadados, instruções CIL, dependências nativas declaradas e comparações de builds | Inspeção estática; [guia de código gerenciado](docs/managed-code-analysis.md) |
|
||||
| APKs Android | Declarações do manifesto, classes, métodos descompilados e referências | JADX sem interface gráfica e um JDK completo no Linux/macOS; [guia de Android](docs/android-analysis.md) |
|
||||
| APKs Android | Declarações do manifesto, classes, métodos descompilados e referências | JADX sem interface gráfica e um JDK completo no Linux/macOS/Windows x64; [guia de Android](docs/android-analysis.md) |
|
||||
| Dispositivos Android | Dispositivos, pacotes, processos, logs, despejos, capturas de tela e transferências de APK/arquivos | adb fornecido pelo chamador (emulador ou aparelho); [guia de ADB](docs/adb-device-analysis.md) |
|
||||
| Projetos JEB | Unidades de projeto do motor, hashes de artefatos e pseudocódigo de tipos/métodos | Um cliente JEB iniciado pelo chamador servindo MCP, selecionado com REA_JEB_MCP_URL; [guia de JEB](docs/jeb-analysis.md) |
|
||||
| Recursos do Android | Manifesto decodificado, tabelas de strings, idiomas e fatos de versão | Lançador do Apktool fornecido pelo chamador; [guia de recursos](docs/apktool-resource-analysis.md) |
|
||||
| Firmware | Regiões, resultados de extração e encaminhamentos à análise nativa | Binwalk / Unblob no Linux; [guia de firmware](docs/firmware-analysis.md) |
|
||||
| Pacotes e recursos | Inventários de arquivos, digests, plists, estrutura de bundles Apple e recursos extraídos | [Guia de artefatos e JavaScript](docs/javascript-artifact-reconstruction.md), [aplicativos Apple](docs/apple-application-analysis.md) |
|
||||
| Comportamento de processos | Saída do terminal, interações, observações de encerramento e do sistema de arquivos, e comparações entre execuções | Linux/macOS com PTY nativo; [captura de processos](docs/process-capture.md) |
|
||||
|
||||
+7
-4
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · **Русский** · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · **Русский** · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
|
||||
|
||||
# REA: Реверс-инжиниринг чего угодно
|
||||
|
||||
@@ -135,13 +135,16 @@ REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а так
|
||||
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Нативные бинарные файлы | Псевдокод, ассемблер, строки, символы, вызовы и ссылки | Hopper, Ghidra или IDA; [нативный анализ](https://rea.tools/guides/native/) |
|
||||
| Структура ELF без запуска | Секции, сегменты, исходные символы/релокации и возможные механизмы защиты по данным статического анализа | pwntools, предоставленный вызывающей стороной, в Linux x64; [диагностика бинарных файлов](docs/binary-diagnostics.md) |
|
||||
| Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление; [руководство по EVM без запуска](docs/evm-bytecode.md) |
|
||||
| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной; необязательные GDB/pwndbg; [записанные сбои](docs/recorded-crashes.md) |
|
||||
| Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление в Linux x64; [руководство по EVM без запуска](docs/evm-bytecode.md) |
|
||||
| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](https://rea.tools/guides/javascript/) |
|
||||
| Сайты | Структура страницы, скрипты, наблюдения за сетью и запрошенные снимки экрана | Браузер семейства Chrome; [анализ браузера](https://rea.tools/guides/browser/) |
|
||||
| Сохранённые сетевые захваты | Запросы, ответы, доступные полезные данные и позиции в источнике | HAR; mitmdump в Linux для захватов в собственном формате mitmproxy; [руководство по сетевым захватам](docs/web-network-captures.md) |
|
||||
| Сборки .NET | Метаданные, инструкции CIL, объявленные нативные зависимости и сравнения сборок | Статическое исследование; [руководство по управляемому коду](docs/managed-code-analysis.md) |
|
||||
| Android APK | Объявления манифеста, классы, декомпилированные методы и ссылки | JADX без графического интерфейса и полный JDK в Linux/macOS; [руководство по Android](docs/android-analysis.md) |
|
||||
| Android APK | Объявления манифеста, классы, декомпилированные методы и ссылки | JADX без графического интерфейса и полный JDK в Linux/macOS/Windows x64; [руководство по Android](docs/android-analysis.md) |
|
||||
| Устройства Android | Устройства, пакеты, процессы, журналы, дампы, снимки экрана и передача APK/файлов | adb, предоставленный вызывающей стороной (эмулятор или устройство); [руководство по ADB](docs/adb-device-analysis.md) |
|
||||
| Проекты JEB | Единицы проекта от движка, дайджесты артефактов и псевдокод типов/методов | Запущенный вызывающей стороной клиент JEB с интерфейсом MCP, выбирается через REA_JEB_MCP_URL; [руководство по JEB](docs/jeb-analysis.md) |
|
||||
| Ресурсы Android | Декодированный манифест, таблицы строк, языки и сведения о версии | Запуск Apktool, предоставленный вызывающей стороной; [руководство по ресурсам](docs/apktool-resource-analysis.md) |
|
||||
| Прошивки | Области, результаты извлечения и передача в нативный анализ | Binwalk / Unblob в Linux; [руководство по прошивкам](docs/firmware-analysis.md) |
|
||||
| Пакеты и ресурсы | Списки файлов, хеши, plist, структура пакетов Apple и извлечённые ресурсы | [Руководство по артефактам и JavaScript](docs/javascript-artifact-reconstruction.md), [приложения Apple](docs/apple-application-analysis.md) |
|
||||
| Поведение процессов | Вывод терминала, взаимодействия, наблюдения за завершением и файловой системой, сравнения запусков | Linux/macOS с нативным PTY; [захват процессов](docs/process-capture.md) |
|
||||
|
||||
+7
-4
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · **ไทย** · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · **ไทย** · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
|
||||
|
||||
# REA: Reverse Engineer Anything
|
||||
|
||||
@@ -135,13 +135,16 @@ REA ต้องใช้ Node.js 22.x (>=22.19), 24.x (>=24.11) หรือ 26
|
||||
| ---------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| ไบนารีเนทีฟ | โค้ดเทียม แอสเซมบลี สตริง สัญลักษณ์ การเรียก และการอ้างอิง | Hopper, Ghidra หรือ IDA; [การวิเคราะห์เนทีฟ](https://rea.tools/guides/native/) |
|
||||
| โครงสร้าง ELF แบบออฟไลน์ | เซกชัน เซกเมนต์ สัญลักษณ์/รีโลเคชันต้นฉบับ และตัวเลือกกลไกป้องกันที่พบจากการวิเคราะห์แบบสถิต | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; [การวินิจฉัยไบนารี](docs/binary-diagnostics.md) |
|
||||
| ไบต์โค้ด EVM | ตัวเลือกฟังก์ชันสำหรับการเรียก ออฟเซ็ตไบต์ อาร์กิวเมนต์และความสามารถในการเปลี่ยนสถานะที่อนุมานได้ | ไฟล์บนเครื่องที่มีไบต์ดิบหรือข้อมูลเลขฐานสิบหก; [คู่มือ EVM แบบออฟไลน์](docs/evm-bytecode.md) |
|
||||
| ข้อมูลการแครชของ Linux ที่บันทึกไว้ | ระเบียน note ดิบ รีจิสเตอร์/สัญญาณของทุกเธรดที่บันทึกไว้ และตัวเลือกการแมปเพิ่มเติม | pwntools ที่ผู้ใช้จัดเตรียม; เลือกใช้ GDB/pwndbg เพิ่มเติมได้; [การแครชที่บันทึกไว้](docs/recorded-crashes.md) |
|
||||
| ไบต์โค้ด EVM | ตัวเลือกฟังก์ชันสำหรับการเรียก ออฟเซ็ตไบต์ อาร์กิวเมนต์และความสามารถในการเปลี่ยนสถานะที่อนุมานได้ | ไฟล์บนเครื่องที่มีไบต์ดิบหรือข้อมูลเลขฐานสิบหกบน Linux x64; [คู่มือ EVM แบบออฟไลน์](docs/evm-bytecode.md) |
|
||||
| ข้อมูลการแครชของ Linux ที่บันทึกไว้ | ระเบียน note ดิบ รีจิสเตอร์/สัญญาณของทุกเธรดที่บันทึกไว้ และตัวเลือกการแมปเพิ่มเติม | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; เลือกใช้ GDB/pwndbg เพิ่มเติมได้; [การแครชที่บันทึกไว้](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | โมดูล การนำเข้า ซอร์สแมป เส้นทาง IPC และความสัมพันธ์กับส่วนเสริมเนทีฟ | Node.js และ npm; [การวิเคราะห์แอปพลิเคชัน](https://rea.tools/guides/javascript/) |
|
||||
| เว็บไซต์ | โครงสร้างหน้า สคริปต์ ข้อมูลเครือข่ายที่สังเกตได้ และภาพหน้าจอที่ร้องขอ | เบราว์เซอร์ในตระกูล Chrome; [การวิเคราะห์ผ่านเบราว์เซอร์](https://rea.tools/guides/browser/) |
|
||||
| ข้อมูลเครือข่ายที่บันทึกไว้ | คำขอ คำตอบ เพย์โหลดที่ปรากฏ และตำแหน่งต้นทาง | HAR; mitmdump บน Linux สำหรับข้อมูลที่บันทึกในรูปแบบเนทีฟของ mitmproxy; [คู่มือการบันทึกข้อมูล](docs/web-network-captures.md) |
|
||||
| แอสเซมบลี .NET | เมทาดาทา คำสั่ง CIL การพึ่งพาเนทีฟที่ประกาศไว้ และการเปรียบเทียบผลการบิลด์ | การตรวจสอบแบบสถิต; [คู่มือโค้ดแบบจัดการ](docs/managed-code-analysis.md) |
|
||||
| APK ของ Android | การประกาศใน manifest คลาส เมธอดที่ดีคอมไพล์ และการอ้างอิง | JADX แบบไม่มี GUI และ JDK แบบเต็มบน Linux/macOS; [คู่มือ Android](docs/android-analysis.md) |
|
||||
| APK ของ Android | การประกาศใน manifest คลาส เมธอดที่ดีคอมไพล์ และการอ้างอิง | JADX แบบไม่มี GUI และ JDK แบบเต็มบน Linux/macOS/Windows x64; [คู่มือ Android](docs/android-analysis.md) |
|
||||
| อุปกรณ์ Android | อุปกรณ์ แพ็กเกจ โพรเซส บันทึก ดัมพ์ ภาพหน้าจอ และการถ่ายโอน APK/ไฟล์ | adb ที่ผู้เรียกจัดเตรียม (โปรแกรมจำลองหรืออุปกรณ์จริง); [คู่มือ ADB](docs/adb-device-analysis.md) |
|
||||
| โปรเจกต์ JEB | หน่วยโปรเจกต์จากเอนจิน, แฮชของอาร์ติแฟกต์ และซูโดโค้ดของไทป์/เมท็อด | ไคลเอนต์ JEB ที่ผู้เรียกเริ่มและให้บริการ MCP เลือกด้วย REA_JEB_MCP_URL; [คู่มือ JEB](docs/jeb-analysis.md) |
|
||||
| ทรัพยากร Android | ไฟล์ manifest ที่ถอดรหัสแล้ว ตารางสตริง ภาษา และข้อมูลเวอร์ชัน | ตัวเรียก Apktool ที่ผู้เรียกจัดเตรียม; [คู่มือทรัพยากร](docs/apktool-resource-analysis.md) |
|
||||
| เฟิร์มแวร์ | พื้นที่ข้อมูล ผลการแตกไฟล์ และการส่งต่อไปวิเคราะห์เนทีฟ | Binwalk / Unblob บน Linux; [คู่มือเฟิร์มแวร์](docs/firmware-analysis.md) |
|
||||
| แพ็กเกจและทรัพยากร | รายการไฟล์ ค่าแฮช plist โครงสร้างบันเดิลของ Apple และทรัพยากรที่แตกออกมา | [คู่มืออาร์ติแฟกต์และ JavaScript](docs/javascript-artifact-reconstruction.md), [แอปพลิเคชัน Apple](docs/apple-application-analysis.md) |
|
||||
| พฤติกรรมของโปรเซส | เอาต์พุตเทอร์มินัล การโต้ตอบ ข้อมูลการสิ้นสุดและระบบไฟล์ที่สังเกตได้ และการเปรียบเทียบการรัน | Linux/macOS ที่มี PTY เนทีฟ; [การบันทึกโปรเซส](docs/process-capture.md) |
|
||||
|
||||
+7
-4
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · **Türkçe** · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · **Türkçe** · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
|
||||
|
||||
# REA: Her şeyi tersine mühendislikle inceleyin
|
||||
|
||||
@@ -135,13 +135,16 @@ REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araç
|
||||
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Yerel makine kodu içeren ikili dosyalar | Sözde kod, assembly kodu, dizeler, semboller, çağrılar ve referanslar | Hopper, Ghidra veya IDA; [yerel kod analizi](https://rea.tools/guides/native/) |
|
||||
| Çevrimdışı ELF yapısı | Bölümler, segmentler, özgün semboller/yeniden konumlandırmalar ve statik analizden olası güvenlik önlemleri | Linux x64 üzerinde çağıranın sağladığı pwntools; [ikili dosya tanılama](docs/binary-diagnostics.md) |
|
||||
| EVM bayt kodu | Yönlendirme seçicileri, bayt ofsetleri, çıkarımla belirlenen argümanlar ve durum değiştirilebilirliği | Yerel ham bayt/onaltılık girdi; [çevrimdışı EVM rehberi](docs/evm-bytecode.md) |
|
||||
| Kaydedilmiş Linux çökmeleri | Ham note kayıtları, kaydedilen her iş parçacığının yazmaçları/sinyalleri ve isteğe bağlı eşleme adayları | Çağıranın sağladığı pwntools; isteğe bağlı GDB/pwndbg; [kaydedilmiş çökmeler](docs/recorded-crashes.md) |
|
||||
| EVM bayt kodu | Yönlendirme seçicileri, bayt ofsetleri, çıkarımla belirlenen argümanlar ve durum değiştirilebilirliği | Linux x64 üzerinde yerel ham bayt/onaltılık girdi; [çevrimdışı EVM rehberi](docs/evm-bytecode.md) |
|
||||
| Kaydedilmiş Linux çökmeleri | Ham note kayıtları, kaydedilen her iş parçacığının yazmaçları/sinyalleri ve isteğe bağlı eşleme adayları | Linux x64 üzerinde çağıranın sağladığı pwntools; isteğe bağlı GDB/pwndbg; [kaydedilmiş çökmeler](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Modüller, içe aktarmalar, kaynak haritaları, rotalar, IPC ve yerel eklenti ilişkileri | Node.js ve npm; [uygulama analizi](https://rea.tools/guides/javascript/) |
|
||||
| Web siteleri | Sayfa yapısı, betikler, ağ gözlemleri ve istenen ekran görüntüleri | Chrome ailesinden bir tarayıcı; [tarayıcı analizi](https://rea.tools/guides/browser/) |
|
||||
| Kaydedilmiş ağ yakalamaları | İstekler, yanıtlar, erişilebilir yük içerikleri ve kaynak konumları | HAR; mitmproxy'nin kendi biçimindeki yakalamalar için Linux üzerinde mitmdump; [ağ yakalama rehberi](docs/web-network-captures.md) |
|
||||
| .NET derlemeleri | Meta veriler, CIL talimatları, bildirilmiş yerel bağımlılıklar ve derleme karşılaştırmaları | Statik inceleme; [yönetilen kod rehberi](docs/managed-code-analysis.md) |
|
||||
| Android APK'leri | Manifest bildirimleri, sınıflar, tersine derlenmiş metotlar ve referanslar | Linux/macOS üzerinde arayüzsüz JADX ve tam bir JDK; [Android rehberi](docs/android-analysis.md) |
|
||||
| Android APK'leri | Manifest bildirimleri, sınıflar, tersine derlenmiş metotlar ve referanslar | Linux/macOS/Windows x64 üzerinde arayüzsüz JADX ve tam bir JDK; [Android rehberi](docs/android-analysis.md) |
|
||||
| Android cihazları | Cihazlar, paketler, süreçler, günlükler, dökümler, ekran görüntüleri ve APK/dosya aktarımları | Çağıranın sağladığı adb (emülatör veya cihaz); [ADB rehberi](docs/adb-device-analysis.md) |
|
||||
| JEB projeleri | Motor destekli proje birimleri, artefakt özetleri ve tür/metot sözdekodu | Çağıranın başlattığı, MCP sunan bir JEB istemcisi, REA_JEB_MCP_URL ile seçilir; [JEB rehberi](docs/jeb-analysis.md) |
|
||||
| Android kaynakları | Çözülmüş manifest, dize tabloları, diller ve sürüm bilgileri | Çağıranın sağladığı Apktool başlatıcısı; [kaynak rehberi](docs/apktool-resource-analysis.md) |
|
||||
| Donanım yazılımı | Bölgeler, çıkarma sonuçları ve yerel kod analizine devirler | Linux üzerinde Binwalk / Unblob; [donanım yazılımı rehberi](docs/firmware-analysis.md) |
|
||||
| Paketler ve kaynaklar | Dosya envanterleri, özet değerleri, plist'ler, Apple paket yapısı ve çıkarılmış kaynaklar | [Artefakt ve JavaScript rehberi](docs/javascript-artifact-reconstruction.md), [Apple uygulamaları](docs/apple-application-analysis.md) |
|
||||
| Süreç davranışı | Terminal çıktısı, etkileşimler, sonlanma ve dosya sistemi gözlemleri, çalıştırma karşılaştırmaları | Yerel PTY ile Linux/macOS; [süreç yakalama](docs/process-capture.md) |
|
||||
|
||||
+18
-15
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · **Українська** · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · **Українська** · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
|
||||
|
||||
# REA: Реверс-інжиніринг будь-чого
|
||||
|
||||
@@ -131,20 +131,23 @@ npx rea-agents@latest setup
|
||||
|
||||
REA потребує Node.js 22.x (>=22.19), 24.x (>=24.11) або 26+, а також npm. Додаткові інструменти й підтримувані системи залежать від цілі:
|
||||
|
||||
| Ціль | Що повертає REA | Вимоги й посібник |
|
||||
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](https://rea.tools/guides/native/) |
|
||||
| Структура ELF без запуску | Секції, сегменти, початкові символи/релокації та можливі захисні механізми за даними статичного аналізу | pwntools, наданий стороною, що викликає інструмент, у Linux x64; [діагностика бінарних файлів](docs/binary-diagnostics.md) |
|
||||
| Байт-код EVM | Селектори диспетчеризації, зміщення байтів, виведені припущення про аргументи та змінюваність стану | Локальні необроблені байти або шістнадцяткове представлення; [посібник EVM без запуску](docs/evm-bytecode.md) |
|
||||
| Записані аварійні завершення в Linux | Необроблені записи note, регістри/сигнали кожного записаного потоку й необов'язкові кандидати зіставлення пам'яті | pwntools, наданий стороною, що викликає інструмент; необов'язкові GDB/pwndbg; [записані аварійні завершення](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](https://rea.tools/guides/javascript/) |
|
||||
| Сайти | Структура сторінки, скрипти, мережеві спостереження й запитані знімки екрана | Браузер сімейства Chrome; [аналіз браузера](https://rea.tools/guides/browser/) |
|
||||
| Збережені мережеві захоплення | Запити, відповіді, доступні корисні дані та місця в джерелі | HAR; mitmdump у Linux для захоплень у власному форматі mitmproxy; [посібник мережевих захоплень](docs/web-network-captures.md) |
|
||||
| Збірки .NET | Метадані, інструкції CIL, оголошені нативні залежності й порівняння збірок | Статична перевірка; [посібник керованого коду](docs/managed-code-analysis.md) |
|
||||
| Android APK | Оголошення маніфесту, класи, декомпільовані методи й посилання | JADX без графічного інтерфейсу й повний JDK у Linux/macOS; [посібник Android](docs/android-analysis.md) |
|
||||
| Прошивки | Області, результати видобування й передавання до нативного аналізу | Binwalk / Unblob у Linux; [посібник прошивок](docs/firmware-analysis.md) |
|
||||
| Пакети й ресурси | Переліки файлів, хеші, plist, структура пакетів Apple та видобуті ресурси | [Посібник артефактів і JavaScript](docs/javascript-artifact-reconstruction.md), [застосунки Apple](docs/apple-application-analysis.md) |
|
||||
| Поведінка процесів | Вивід термінала, взаємодії, спостереження за завершенням і файловою системою та порівняння запусків | Linux/macOS із нативним PTY; [захоплення процесів](docs/process-capture.md) |
|
||||
| Ціль | Що повертає REA | Вимоги й посібник |
|
||||
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](https://rea.tools/guides/native/) |
|
||||
| Структура ELF без запуску | Секції, сегменти, початкові символи/релокації та можливі захисні механізми за даними статичного аналізу | pwntools, наданий стороною, що викликає інструмент, у Linux x64; [діагностика бінарних файлів](docs/binary-diagnostics.md) |
|
||||
| Байт-код EVM | Селектори диспетчеризації, зміщення байтів, виведені припущення про аргументи та змінюваність стану | Локальні необроблені байти або шістнадцяткове представлення у Linux x64; [посібник EVM без запуску](docs/evm-bytecode.md) |
|
||||
| Записані аварійні завершення в Linux | Необроблені записи note, регістри/сигнали кожного записаного потоку й необов'язкові кандидати зіставлення пам'яті | pwntools, наданий стороною, що викликає інструмент, у Linux x64; необов'язкові GDB/pwndbg; [записані аварійні завершення](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](https://rea.tools/guides/javascript/) |
|
||||
| Сайти | Структура сторінки, скрипти, мережеві спостереження й запитані знімки екрана | Браузер сімейства Chrome; [аналіз браузера](https://rea.tools/guides/browser/) |
|
||||
| Збережені мережеві захоплення | Запити, відповіді, доступні корисні дані та місця в джерелі | HAR; mitmdump у Linux для захоплень у власному форматі mitmproxy; [посібник мережевих захоплень](docs/web-network-captures.md) |
|
||||
| Збірки .NET | Метадані, інструкції CIL, оголошені нативні залежності й порівняння збірок | Статична перевірка; [посібник керованого коду](docs/managed-code-analysis.md) |
|
||||
| Android APK | Оголошення маніфесту, класи, декомпільовані методи й посилання | JADX без графічного інтерфейсу й повний JDK у Linux/macOS/Windows x64; [посібник Android](docs/android-analysis.md) |
|
||||
| Пристрої Android | Пристрої, пакети, процеси, журнали, дампи, знімки екрана та передавання APK/файлів | adb, наданий викликаючою стороною (емулятор або пристрій); [підручник ADB](docs/adb-device-analysis.md) |
|
||||
| Проєкти JEB | Єдиниці проєкту від рушія, дайджести артефактів і псевдокод типів/методів | Запущений викликаною стороною клієнт JEB з MCP, обирається через REA_JEB_MCP_URL; [посібник JEB](docs/jeb-analysis.md) |
|
||||
| Ресурси Android | Декодований маніфест, таблиці рядків, мови та відомості про версію | Засіб запуску Apktool, наданий викликаючою стороною; [підручник ресурсів](docs/apktool-resource-analysis.md) |
|
||||
| Прошивки | Області, результати видобування й передавання до нативного аналізу | Binwalk / Unblob у Linux; [посібник прошивок](docs/firmware-analysis.md) |
|
||||
| Пакети й ресурси | Переліки файлів, хеші, plist, структура пакетів Apple та видобуті ресурси | [Посібник артефактів і JavaScript](docs/javascript-artifact-reconstruction.md), [застосунки Apple](docs/apple-application-analysis.md) |
|
||||
| Поведінка процесів | Вивід термінала, взаємодії, спостереження за завершенням і файловою системою та порівняння запусків | Linux/macOS із нативним PTY; [захоплення процесів](docs/process-capture.md) |
|
||||
|
||||
Статична перевірка JavaScript і .NET читає надані файли, не запускаючи застосунок. Захоплення під час виконання запускає вибрану ціль або взаємодіє з нею з правами вашого користувача; відповідні посібники описують його вплив.
|
||||
|
||||
|
||||
+7
-4
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · **Tiếng Việt** · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · **Tiếng Việt** · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
|
||||
|
||||
# REA: Kỹ thuật dịch ngược cho mọi thứ
|
||||
|
||||
@@ -135,13 +135,16 @@ REA yêu cầu Node.js 22.x (>=22.19), 24.x (>=24.11) hoặc 26+, cùng với np
|
||||
| ----------------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Tệp nhị phân mã máy | Mã giả, hợp ngữ, chuỗi, ký hiệu, lời gọi và tham chiếu | Hopper, Ghidra hoặc IDA; [phân tích mã máy](https://rea.tools/guides/native/) |
|
||||
| Bố cục ELF ngoại tuyến | Các section, segment, ký hiệu/thông tin tái định vị gốc và các biện pháp giảm thiểu có thể có từ phân tích tĩnh | pwntools do bên gọi cung cấp trên Linux x64; [chẩn đoán tệp nhị phân](docs/binary-diagnostics.md) |
|
||||
| Bytecode EVM | Bộ chọn điều phối, độ lệch byte, tham số suy luận và khả năng thay đổi trạng thái | Đầu vào cục bộ chứa byte thô hoặc dạng thập lục phân; [hướng dẫn EVM ngoại tuyến](docs/evm-bytecode.md) |
|
||||
| Sự cố Linux đã ghi lại | Bản ghi note thô, thanh ghi/tín hiệu của mọi luồng được ghi lại và các ứng viên ánh xạ tùy chọn | pwntools do bên gọi cung cấp; GDB/pwndbg tùy chọn; [sự cố đã ghi lại](docs/recorded-crashes.md) |
|
||||
| Bytecode EVM | Bộ chọn điều phối, độ lệch byte, tham số suy luận và khả năng thay đổi trạng thái | Đầu vào cục bộ chứa byte thô hoặc dạng thập lục phân trên Linux x64; [hướng dẫn EVM ngoại tuyến](docs/evm-bytecode.md) |
|
||||
| Sự cố Linux đã ghi lại | Bản ghi note thô, thanh ghi/tín hiệu của mọi luồng được ghi lại và các ứng viên ánh xạ tùy chọn | pwntools do bên gọi cung cấp trên Linux x64; GDB/pwndbg tùy chọn; [sự cố đã ghi lại](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Mô-đun, các khai báo import, source map, tuyến, IPC và quan hệ với tiện ích mở rộng mã máy | Node.js và npm; [phân tích ứng dụng](https://rea.tools/guides/javascript/) |
|
||||
| Trang web | Cấu trúc trang, script, quan sát mạng và ảnh chụp màn hình được yêu cầu | Trình duyệt thuộc họ Chrome; [phân tích trình duyệt](https://rea.tools/guides/browser/) |
|
||||
| Bản ghi lưu lượng mạng đã lưu | Yêu cầu, phản hồi, payload có thể truy cập và vị trí nguồn | HAR; mitmdump trên Linux cho bản ghi định dạng gốc của mitmproxy; [hướng dẫn bản ghi mạng](docs/web-network-captures.md) |
|
||||
| Assembly .NET | Siêu dữ liệu, lệnh CIL, các phụ thuộc mã máy được khai báo và so sánh bản dựng | Kiểm tra tĩnh; [hướng dẫn mã được quản lý](docs/managed-code-analysis.md) |
|
||||
| APK Android | Khai báo manifest, lớp, phương thức được dịch ngược và tham chiếu | JADX không giao diện đồ họa và JDK đầy đủ trên Linux/macOS; [hướng dẫn Android](docs/android-analysis.md) |
|
||||
| APK Android | Khai báo manifest, lớp, phương thức được dịch ngược và tham chiếu | JADX không giao diện đồ họa và JDK đầy đủ trên Linux/macOS/Windows x64; [hướng dẫn Android](docs/android-analysis.md) |
|
||||
| Thiết bị Android | Thiết bị, gói, tiến trình, nhật ký, dump, ảnh chụp màn hình và truyền APK/tệp | adb do nơi gọi cung cấp (trình giả lập hoặc thiết bị); [hướng dẫn ADB](docs/adb-device-analysis.md) |
|
||||
| Dự án JEB | Đơn vị dự án từ engine, hash của artifact và mã giả của kiểu/phương thức | Một ứng dụng JEB do nơi gọi khởi động, phục vụ MCP, chọn bằng REA_JEB_MCP_URL; [hướng dẫn JEB](docs/jeb-analysis.md) |
|
||||
| Tài nguyên Android | Manifest đã giải mã, bảng chuỗi, ngôn ngữ và thông tin phiên bản | Trình khởi chạy Apktool do nơi gọi cung cấp; [hướng dẫn tài nguyên](docs/apktool-resource-analysis.md) |
|
||||
| Phần sụn | Các vùng, kết quả trích xuất và dữ liệu chuyển sang phân tích mã máy | Binwalk / Unblob trên Linux; [hướng dẫn phần sụn](docs/firmware-analysis.md) |
|
||||
| Gói và tài nguyên | Danh sách tệp, giá trị băm, plist, cấu trúc bundle Apple và tài nguyên được trích xuất | [Hướng dẫn hiện vật phần mềm và JavaScript](docs/javascript-artifact-reconstruction.md), [ứng dụng Apple](docs/apple-application-analysis.md) |
|
||||
| Hành vi tiến trình | Đầu ra terminal, tương tác, quan sát khi thoát và hệ thống tệp, cùng so sánh các lần chạy | Linux/macOS có PTY gốc; [ghi lại tiến trình](docs/process-capture.md) |
|
||||
|
||||
+8
-5
@@ -1,8 +1,8 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · **繁體中文** · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · **繁體中文** · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
|
||||
|
||||
# REA:逆向分析一切
|
||||
# REA:逆向一切
|
||||
|
||||
### 通過一個 MCP 服務,逆向分析二進位檔案、應用程式和執行階段行為。
|
||||
|
||||
@@ -135,13 +135,16 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
|
||||
| --------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| 原生二進位檔案 | 偽程式碼、組合語言、字串、符號、呼叫和引用 | Hopper、Ghidra 或 IDA;[原生分析](https://rea.tools/guides/native/) |
|
||||
| 離線 ELF 佈局 | 節、段、原始符號/重定位資訊及靜態防護機制候選項 | Linux x64 上由呼叫方提供的 pwntools;[二進位診斷](docs/binary-diagnostics.md) |
|
||||
| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | 本地原始位元組/十六進位輸入載體;[離線 EVM 指南](docs/evm-bytecode.md) |
|
||||
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | 呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](docs/recorded-crashes.md) |
|
||||
| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | Linux x64 上的本地原始位元組/十六進位輸入載體;[離線 EVM 指南](docs/evm-bytecode.md) |
|
||||
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](https://rea.tools/guides/javascript/) |
|
||||
| 網站 | 頁面結構、腳本、網路觀察結果和按請求獲取的螢幕擷取畫面 | Chrome 系瀏覽器;[瀏覽器分析](https://rea.tools/guides/browser/) |
|
||||
| 已保存的網路擷取 | 請求、回應、可訪問的酬載和來源位置 | HAR;原生 mitmproxy 擷取需要 Linux 上的 mitmdump;[擷取指南](docs/web-network-captures.md) |
|
||||
| .NET 程式集 | 中繼資料、CIL 指令、聲明的原生依賴和建置對比 | 靜態檢查;[托管程式碼指南](docs/managed-code-analysis.md) |
|
||||
| Android APK | 清單聲明、類別、反編譯的方法和引用 | Linux/macOS 上的無界面 JADX 和完整 JDK;[Android 指南](docs/android-analysis.md) |
|
||||
| Android APK | 清單聲明、類別、反編譯的方法和引用 | Linux/macOS/Windows x64 上的無界面 JADX 和完整 JDK;[Android 指南](docs/android-analysis.md) |
|
||||
| Android 裝置 | 裝置、套件、程序、日誌、傾印、螢幕擷取以及 APK/檔案傳輸 | 呼叫端提供的 adb(模擬器或實體裝置);[ADB 指南](docs/adb-device-analysis.md) |
|
||||
| JEB 專案 | 引擎支援的專案單元、成品摘要和型別/方法的虛擬碼 | 由呼叫端啟動、提供 MCP 的 JEB 用戶端,透過 REA_JEB_MCP_URL 選擇;[JEB 指南](docs/jeb-analysis.md) |
|
||||
| Android 資源 | 解碼後的清單、字串表、語系和版本資訊 | 呼叫端提供的 Apktool 啟動器;[資源指南](docs/apktool-resource-analysis.md) |
|
||||
| 韌體 | 區域、提取結果和轉交原生分析的內容 | Linux 上的 Binwalk / Unblob;[韌體指南](docs/firmware-analysis.md) |
|
||||
| 軟體包與資源 | 檔案清單、摘要、plist、Apple bundle 結構和提取的資源 | [製品與 JavaScript 指南](docs/javascript-artifact-reconstruction.md)、[Apple 應用](docs/apple-application-analysis.md) |
|
||||
| 處理程序行為 | 終端輸出、互動、退出和檔案系統觀察結果,以及執行對比 | 支援原生 PTY 的 Linux/macOS;[處理程序擷取](docs/process-capture.md) |
|
||||
|
||||
+8
-5
@@ -1,8 +1,8 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · **简体中文** · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · **简体中文** · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
|
||||
|
||||
# REA:逆向分析一切
|
||||
# REA:逆向一切
|
||||
|
||||
### 通过一个 MCP 服务,逆向分析二进制文件、应用程序和运行时行为。
|
||||
|
||||
@@ -135,13 +135,16 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
|
||||
| --------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](https://rea.tools/guides/native/) |
|
||||
| 离线 ELF 布局 | 节、段、原始符号/重定位信息及静态防护机制候选项 | Linux x64 上由调用方提供的 pwntools;[二进制诊断](docs/binary-diagnostics.md) |
|
||||
| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | 本地原始字节/十六进制输入载体;[离线 EVM 指南](docs/evm-bytecode.md) |
|
||||
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | 调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](docs/recorded-crashes.md) |
|
||||
| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | Linux x64 上的本地原始字节/十六进制输入载体;[离线 EVM 指南](docs/evm-bytecode.md) |
|
||||
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](https://rea.tools/guides/javascript/) |
|
||||
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](https://rea.tools/guides/browser/) |
|
||||
| 已保存的网络捕获 | 请求、响应、可访问的载荷和来源位置 | HAR;原生 mitmproxy 捕获需要 Linux 上的 mitmdump;[捕获指南](docs/web-network-captures.md) |
|
||||
| .NET 程序集 | 元数据、CIL 指令、声明的原生依赖和构建对比 | 静态检查;[托管代码指南](docs/managed-code-analysis.md) |
|
||||
| Android APK | 清单声明、类、反编译的方法和引用 | Linux/macOS 上的无界面 JADX 和完整 JDK;[Android 指南](docs/android-analysis.md) |
|
||||
| Android APK | 清单声明、类、反编译的方法和引用 | Linux/macOS/Windows x64 上的无界面 JADX 和完整 JDK;[Android 指南](docs/android-analysis.md) |
|
||||
| Android 设备 | 设备、软件包、进程、日志、转储、屏幕截图以及 APK/文件传输 | 调用方提供的 adb(模拟器或实体设备);[ADB 指南](docs/adb-device-analysis.md) |
|
||||
| JEB 项目 | 引擎支持的项目单元、工件摘要和类型/方法的伪代码 | 由调用方启动、提供 MCP 的 JEB 客户端,通过 REA_JEB_MCP_URL 选择;[JEB 指南](docs/jeb-analysis.md) |
|
||||
| Android 资源 | 解码后的清单、字符串表、语言区域和版本信息 | 调用方提供的 Apktool 启动器;[资源指南](docs/apktool-resource-analysis.md) |
|
||||
| 固件 | 区域、提取结果和转交原生分析的内容 | Linux 上的 Binwalk / Unblob;[固件指南](docs/firmware-analysis.md) |
|
||||
| 软件包与资源 | 文件清单、摘要、plist、Apple bundle 结构和提取的资源 | [制品与 JavaScript 指南](docs/javascript-artifact-reconstruction.md)、[Apple 应用](docs/apple-application-analysis.md) |
|
||||
| 进程行为 | 终端输出、交互、退出和文件系统观察结果,以及运行对比 | 支持原生 PTY 的 Linux/macOS;[进程捕获](docs/process-capture.md) |
|
||||
|
||||
@@ -841,14 +841,13 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
|
||||
private JsonObject instructionReference(Reference reference) {
|
||||
JsonObject item = new JsonObject();
|
||||
RefType type = reference.getReferenceType();
|
||||
JsonObject kind = referenceKind(reference);
|
||||
item.addProperty("source_address", canonicalAddress(reference.getFromAddress()));
|
||||
item.addProperty("target_address", canonicalAddress(reference.getToAddress()));
|
||||
item.addProperty("type", type.getName());
|
||||
item.addProperty("call", type.isCall());
|
||||
item.addProperty("jump", type.isJump());
|
||||
item.addProperty("indirect", type.isIndirect());
|
||||
item.addProperty("computed", type.isComputed());
|
||||
item.addProperty("operand_index", reference.getOperandIndex());
|
||||
for (String field : List.of("type", "call", "jump", "indirect", "computed",
|
||||
"operand_index", "data", "read", "write", "primary", "provenance"))
|
||||
item.add(field, kind.get(field));
|
||||
item.addProperty("source", normalizedSource(reference.getSource()));
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import ghidra.app.cmd.function.CreateFunctionCmd;
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.*;
|
||||
import ghidra.program.model.symbol.FlowType;
|
||||
import ghidra.program.model.pcode.PcodeOp;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Undo wrong no-return flags from auto-analysis. A function flagged no-return that contains a
|
||||
* real RET is an ordinary function (stream readers, cxa_guard_acquire, ...). Callers treated the
|
||||
* call as a terminator and the decompiler truncated them. Clear the flag, then re-disassemble only
|
||||
* the call sites of the functions that were cleared and refit their owning functions in place.
|
||||
*/
|
||||
public final class ReaGhidraNoReturnFix extends GhidraScript {
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
// Windows P0 does not admit analysis-database mutation.
|
||||
if (System.getProperty("os.name", "").startsWith("Windows")) return;
|
||||
FunctionManager fm = currentProgram.getFunctionManager();
|
||||
Listing li = currentProgram.getListing();
|
||||
Set<Address> cleared = new HashSet<>();
|
||||
for (Function f : fm.getFunctions(true)) {
|
||||
monitor.checkCancelled();
|
||||
if (!f.hasNoReturn() || f.isThunk()) continue;
|
||||
for (Instruction ins : li.getInstructions(f.getBody(), true)) {
|
||||
FlowType ft = ins.getFlowType();
|
||||
boolean decodedReturn = false;
|
||||
for (PcodeOp operation : ins.getPcode()) {
|
||||
if (operation.getOpcode() == PcodeOp.RETURN) decodedReturn = true;
|
||||
}
|
||||
if (decodedReturn && !ft.isJump() && !ft.isCall()) {
|
||||
f.setNoReturn(false);
|
||||
cleared.add(f.getEntryPoint());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// A thunk inherits the flag of the function it forwards to.
|
||||
for (Function f : fm.getFunctions(true)) {
|
||||
monitor.checkCancelled();
|
||||
if (!f.isThunk()) continue;
|
||||
Function target = f.getThunkedFunction(true);
|
||||
if (target != null && cleared.contains(target.getEntryPoint())) {
|
||||
cleared.add(f.getEntryPoint());
|
||||
}
|
||||
}
|
||||
int redone = 0;
|
||||
TreeSet<Address> owners = new TreeSet<>();
|
||||
if (!cleared.isEmpty()) {
|
||||
List<Address> sites = new ArrayList<>();
|
||||
for (Instruction ins : li.getInstructions(true)) {
|
||||
monitor.checkCancelled();
|
||||
FlowType ft = ins.getFlowType();
|
||||
if (!(ft.isCall() && ft.isTerminal())) continue;
|
||||
Address[] flows = ins.getFlows();
|
||||
if (flows.length == 0 || !cleared.contains(flows[0])) continue;
|
||||
// Do not redisassemble into an independently identified function.
|
||||
Address next = ins.getMaxAddress().add(1);
|
||||
if (fm.getFunctionAt(next) != null || !currentProgram.getMemory().contains(next)) continue;
|
||||
sites.add(ins.getAddress());
|
||||
}
|
||||
for (Address s : sites) {
|
||||
monitor.checkCancelled();
|
||||
Instruction ins = li.getInstructionAt(s);
|
||||
if (ins == null) continue;
|
||||
Function owner = fm.getFunctionContaining(s);
|
||||
if (owner != null && !owner.isThunk()) owners.add(owner.getEntryPoint());
|
||||
clearListing(ins.getMinAddress(), ins.getMaxAddress());
|
||||
disassemble(s);
|
||||
redone++;
|
||||
}
|
||||
for (Address e : owners) {
|
||||
monitor.checkCancelled();
|
||||
Function f = fm.getFunctionAt(e);
|
||||
// Refit the body in place so the name, signature and calling convention survive.
|
||||
if (f != null) CreateFunctionCmd.fixupFunctionBody(currentProgram, f, monitor);
|
||||
}
|
||||
// No analyzeChanges here: on libpl2.so it adds about 70 s, which pushes startup past the 330 s deadline.
|
||||
}
|
||||
println(
|
||||
"REA no-return fix: cleared=" + cleared.size() + " sites=" + redone + " refit=" + owners.size());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
# ADB device inspection and acquisition
|
||||
|
||||
REA inspects connected Android devices, reads device state, and transfers
|
||||
files and packages through the caller's `adb` binary. Emulators, USB
|
||||
devices, and network devices participate alike: REA neither starts nor
|
||||
manages them and never installs platform tools. Every operation composes a
|
||||
fixed adb argument vector — caller values occupy positional arguments only
|
||||
and are quoted when adb forwards them to the device shell.
|
||||
|
||||
The adb client may start its local server on first device query; that is adb
|
||||
client behavior, recorded in the tool result when observed.
|
||||
`inspect_adb_client` probes only the binary and never contacts the server.
|
||||
|
||||
## Supply tools
|
||||
|
||||
Use any Android platform-tools `adb` (verified with 34.0.5). Select it
|
||||
through the environment:
|
||||
|
||||
```sh
|
||||
# Optional: absolute path to the adb binary; without it, adb resolves from PATH.
|
||||
export REA_ADB_PATH=/absolute/path/to/adb
|
||||
```
|
||||
|
||||
REA never installs the Android SDK, an emulator, or a system image.
|
||||
Readiness is `rea inspect-adb-client`: it runs `adb version` and reports the
|
||||
resolved binary, its version, and where it was selected from. A missing
|
||||
binary reports `capability_unavailable` with the `REA_ADB_PATH` remediation
|
||||
rather than attempting an installation.
|
||||
|
||||
## Answer an analyst question
|
||||
|
||||
```sh
|
||||
# Device identity
|
||||
rea inspect-adb-client
|
||||
rea list-adb-devices
|
||||
rea inspect-adb-device emulator-5554
|
||||
rea inspect-adb-display emulator-5554
|
||||
rea list-adb-features emulator-5554
|
||||
rea list-adb-services emulator-5554
|
||||
|
||||
# Find and acquire an app (the easy-get flow)
|
||||
rea resolve-adb-packages emulator-5554 whatsapp
|
||||
rea list-adb-packages emulator-5554 --scope third_party
|
||||
rea inspect-adb-package emulator-5554 com.whatsapp
|
||||
rea pull-adb-package emulator-5554 com.whatsapp --output-directory /tmp/acq
|
||||
rea inspect-android-package /tmp/acq/com.whatsapp/base.apk
|
||||
|
||||
# Device state and files
|
||||
rea list-adb-processes emulator-5554
|
||||
rea inspect-adb-window emulator-5554
|
||||
rea read-adb-setting emulator-5554 global adb_enabled
|
||||
rea read-adb-logcat emulator-5554 --count 200
|
||||
rea list-adb-directory emulator-5554 /data/data/com.whatsapp
|
||||
rea pull-adb-file emulator-5554 /data/local/tmp/notes.txt --output-directory /tmp/acq
|
||||
rea push-adb-file emulator-5554 /tools/frida-server /data/local/tmp/frida-server
|
||||
rea capture-adb-screen emulator-5554 --output-directory /tmp/shots
|
||||
rea collect-adb-bugreport emulator-5554 --output-directory /tmp/bugs
|
||||
|
||||
# Package lifecycle and dynamic analysis (device-mutating)
|
||||
rea install-adb-package emulator-5554 /tmp/app.apk --replace
|
||||
rea start-adb-app emulator-5554 com.example.app
|
||||
rea read-adb-logcat emulator-5554 --pid 12345
|
||||
rea start-adb-activity emulator-5554 android.intent.action.VIEW --data-uri https://example.com
|
||||
rea stop-adb-app emulator-5554 com.example.app
|
||||
rea uninstall-adb-package emulator-5554 com.example.app
|
||||
```
|
||||
|
||||
Equivalent MCP methods use the snake_case names (`list_adb_devices`,
|
||||
`pull_adb_package`, `resolve_adb_packages`, …). Device operations take
|
||||
`serial` exactly as `adb devices` reports it. The pulled APK set feeds the
|
||||
existing analysis families directly, for example
|
||||
`rea inspect-android-package /tmp/acq/<package>/base.apk`.
|
||||
|
||||
## Operations
|
||||
|
||||
| Tool | Kind | What it does |
|
||||
| ----------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `inspect_adb_client` | read | Resolved binary, version, install path; never contacts the server |
|
||||
| `list_adb_devices` | read | Serial, state, transport, model metadata, inferred emulator/physical kind with basis |
|
||||
| `inspect_adb_device` | read | Fixed `getprop` whitelist: build, SDK, patch, fingerprint, product, ABIs, debuggable, `ro.kernel.qemu` |
|
||||
| `inspect_adb_display` | read | `wm size` and `wm density` as declared |
|
||||
| `list_adb_features` | read | `pm list features` (hardware/software features, hex GL versions) |
|
||||
| `list_adb_services` | read | `service list` binder services and interfaces |
|
||||
| `list_adb_processes` | read | `ps -A`: user, pid, ppid, rss, state, name |
|
||||
| `inspect_adb_window` | read | `dumpsys window` focus declarations (what is on screen) |
|
||||
| `read_adb_setting` | read | One `settings get` value; device-side null is reported as such |
|
||||
| `read_adb_logcat` | read | Bounded `logcat -d -t` dump for one buffer, optional pid filter |
|
||||
| `list_adb_packages` | read | `pm list packages -f` with scope; base APK device paths |
|
||||
| `resolve_adb_packages` | read | Case-insensitive package-name substring search with exact-match flag |
|
||||
| `inspect_adb_package` | read | `dumpsys package` projection: versions, install times, installer, flags, permission count |
|
||||
| `list_adb_directory` | read | `ls -la`: kinds, permissions, owner, group, sizes, symlink targets |
|
||||
| `pull_adb_package` | read | Complete split APK set from `pm path`, each file digested |
|
||||
| `pull_adb_file` | read | One absolute device path pulled and digested |
|
||||
| `capture_adb_screen` | read | One `screencap -p` frame (exec-out, no device-side file) with dimensions |
|
||||
| `collect_adb_bugreport` | read | Device-generated bugreport zip, digested |
|
||||
| `push_adb_file` | **mutates device** | One local file to a device path (e.g. pushing tools into `/data/local/tmp`); refuses existing targets without explicit overwrite; verifies via the device's own sha256sum |
|
||||
| `install_adb_package` | **mutates device** | Local APK via `adb install [-r]`; preserves the device's Success/Failure verdict |
|
||||
| `uninstall_adb_package` | **mutates device** | Removes a package and its data for user 0 |
|
||||
| `start_adb_app` | **mutates device** | Resolves the package's launcher activity (`cmd package resolve-activity --brief`) and starts it (`am start -n`) |
|
||||
| `start_adb_activity` | **mutates device** | Starts an intent (`am start -a` with optional URI, component, and up to 16 typed extras); preserves the activity manager's verdict |
|
||||
| `stop_adb_app` | **mutates device** | Force-stops every process of a package, ending a dynamic-analysis run |
|
||||
|
||||
## Interpret the result
|
||||
|
||||
- **Inferences carry their basis.** Device `kind` reports
|
||||
`kind_basis` (serial prefix or USB transport); APK `role` reports
|
||||
`role_basis: file_name`; `emulator_observed` is the build's own
|
||||
`ro.kernel.qemu` value, `null` when absent.
|
||||
- **Device paths are not host paths.** `base_apk_device_path` values live on
|
||||
the device filesystem. Only pulls and captures produce host-local paths,
|
||||
each paired with byte size and SHA-256 digest.
|
||||
- **Splits are part of the answer.** `pm path` enumerates the base APK and
|
||||
every installed split; pulls cover the whole set. System packages whose
|
||||
APKs keep other names report the `unknown` role rather than guessing.
|
||||
- **Partial results keep facts.** Failed individual pulls return successful
|
||||
artifacts with `coverage: "partial"` and per-file `failures`. Listings
|
||||
that contain unrecognized lines report them as `unparsed_lines` and mark
|
||||
partial coverage instead of dropping rows.
|
||||
- **Mutations are explicit.** `push_adb_file`, `install_adb_package`, and
|
||||
`uninstall_adb_package` declare `mutatesTarget` and never run implicitly:
|
||||
pushes refuse existing device files unless `overwrite` is set and verify
|
||||
the transfer with the device's own `sha256sum`; installs preserve the
|
||||
device's `Failure [...]` reason verbatim.
|
||||
- **Producer identity stays attached.** Every result embeds the producing
|
||||
adb client (`binary_path`, `path_source`, `version`, `installed_path`).
|
||||
|
||||
## Resource and lifecycle limits
|
||||
|
||||
- Caller values reach adb only as positional arguments in fixed vectors;
|
||||
no tool accepts an arbitrary shell command line, and UI interaction
|
||||
(`input`, `am`, `monkey`) is deliberately not exposed.
|
||||
- Per-invocation budgets: client/devices 10-20 s, shell reads 20-60 s with
|
||||
64 KiB-8 MiB capture caps (logcat 4 MiB, screen 32 MiB), transfers and
|
||||
bugreports 600 s. Exceeded budgets report `resource_constraint`; stopped
|
||||
commands report `provider_timeout`.
|
||||
- Pull and capture outputs are additive: the destination must not already
|
||||
exist. Re-running a completed pull refuses the existing directory instead
|
||||
of overwriting evidence.
|
||||
- The provider holds no long-lived processes; every invocation is one
|
||||
bounded adb run. The adb server belongs to the caller.
|
||||
|
||||
## Related capability and follow-up lanes
|
||||
|
||||
UI interaction beyond intent starts (tapping, typing, monkey fuzzing, screen
|
||||
recording) remains a separate lane: candidates such as
|
||||
[mobile-device-mcp](https://github.com/srmorete/mobile-device-mcp) install
|
||||
driver software on the device and pair with the Frida instrumentation lane
|
||||
(#1359) behind explicit mutation-authority contracts. Network transports
|
||||
beyond already-connected devices (`adb connect`) are not exposed yet.
|
||||
|
||||
## Real verification
|
||||
|
||||
`npm run verify:adb` exercises every operation against a live device. It
|
||||
requires `adb` (or `REA_ADB_PATH`) and any attached emulator or phone;
|
||||
`--serial` selects one when several are attached, `--pull PACKAGE` pulls a
|
||||
real package and verifies the returned digests against the files on disk,
|
||||
and `--install-apk PATH` drives a full install → resolve → uninstall
|
||||
lifecycle with a lane-owned probe.
|
||||
|
||||
Record: verified against adb 34.0.5 on Linux against an Android 14
|
||||
(API 34) x86_64 emulator — all operations above, a 224-package inventory,
|
||||
287 services, 92 features, a real two-APK split set pulled with byte-exact
|
||||
digests, a verified push/pull roundtrip, a 1.3 MB screen capture, and the
|
||||
complete package lifecycle; see `docs/testing.md` for the lane summary.
|
||||
@@ -126,8 +126,17 @@ also stops target hashing before provider admission.
|
||||
|
||||
These operations do not execute the APK or provide split APK/AAB handling,
|
||||
signature validation, full resource-table semantics, native-library analysis or
|
||||
Android runtime capture. Existing artifact inventory/extraction tools can supply
|
||||
archive evidence. Project that Evidence with the execution-free inventory tool:
|
||||
Android runtime capture. An `.aab` suffix is a ZIP archive, not an APK, so
|
||||
Android tools and the application graph do not treat it as a standalone APK.
|
||||
Existing artifact inventory/extraction tools can supply archive evidence.
|
||||
Inventory records an APK root only for ZIP bytes whose name ends in `.apk`.
|
||||
The projection adds a limitation when no root `AndroidManifest.xml` was
|
||||
observed, because such a ZIP may not be an APK. Nested members take a ZIP or
|
||||
ASAR family from their bytes; an unread member keeps an unknown format.
|
||||
Inventory keeps ZIP names that differ only in case and records that a
|
||||
case-insensitive destination cannot store both spellings. Extraction fails
|
||||
only when that destination directory cannot keep both names. Project that
|
||||
Evidence with the execution-free inventory tool:
|
||||
|
||||
```sh
|
||||
rea project-android-application-graph '{"inventory_evidence":[<inventory_artifact Evidence>]}'
|
||||
@@ -144,7 +153,12 @@ rea project-android-application-graph '{"inventory_evidence":[<inventory_artifac
|
||||
|
||||
The projection reports exact component paths and hashes, runtime-family hints,
|
||||
and path-based bridge hypotheses. It does not decode DEX or claim observed
|
||||
runtime calls. Bridge hypotheses form a DEX/JVM-class by native-library
|
||||
runtime calls. Framework hints match complete native library names, such as
|
||||
`libreactnativejni.so`, `libreactnative.so`, `libhermes.so`,
|
||||
`libhermes_executor.so`, `libflutter.so`, and `libunity.so`; unrelated names
|
||||
such as `libcommunity.so` and `libreactivity.so` do not identify those families.
|
||||
The bridge basis is inferred from the native path and repeated for each managed
|
||||
component, so it does not identify a specific JNI binding. Bridge hypotheses form a DEX/JVM-class by native-library
|
||||
Cartesian product. The projection computes the product and exact serialized
|
||||
candidate-array size before creating candidate objects, then retains a
|
||||
deterministic prefix within a 2 MiB UTF-8 JSON budget. If candidates are omitted,
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# Apktool resource decoding
|
||||
|
||||
REA decodes Android resource tables and the binary manifest through the
|
||||
caller's Apktool launcher. This completes the static triad for Android
|
||||
targets: Java/Kotlin sources through the JADX family, engine-grade
|
||||
decompilation through JEB, and resources through this family. REA never
|
||||
installs Apktool or a Java runtime.
|
||||
|
||||
## Supply tools
|
||||
|
||||
Use Apktool 2.7.0 or newer (verified with 2.7.0) and a Java runtime the
|
||||
launcher can find. Select the launcher through the environment:
|
||||
|
||||
```sh
|
||||
# Optional: absolute path to the apktool launcher (upstream wrapper or a
|
||||
# java -jar script); without it, apktool resolves from PATH.
|
||||
export REA_APKTOOL_COMMAND=/absolute/path/apktool
|
||||
```
|
||||
|
||||
Readiness is `rea inspect-apktool-client`: it runs `apktool --version` and
|
||||
reports the resolved launcher, its selection source, and its version. A
|
||||
missing launcher reports `capability_unavailable` with the
|
||||
`REA_APKTOOL_COMMAND` remediation rather than attempting an installation.
|
||||
|
||||
## Answer an analyst question
|
||||
|
||||
```sh
|
||||
rea inspect-apktool-client
|
||||
rea decode-android-resources /targets/Example.apk
|
||||
rea decode-android-resources /targets/Example.apk --locale de
|
||||
rea decode-android-resources /targets/Example.apk --no-strings
|
||||
```
|
||||
|
||||
Equivalent MCP methods are `inspect_apktool_client` and
|
||||
`decode_android_resources`. The decode request takes `path`, an optional
|
||||
`locale` (project `res/values-<locale>/strings.xml` instead of the default
|
||||
table), and `include_strings` (default true). Regional locales such as `pt-BR`
|
||||
select Android's `values-pt-rBR`; Android qualifiers such as `pt-rBR` and
|
||||
`b+zh+Hans+CN` may also be supplied directly.
|
||||
|
||||
## Interpret the result
|
||||
|
||||
- **An owned APK snapshot is digested and decoded.** `target.sha256` and
|
||||
`target.bytes` bind the observation to the exact APK that was decoded.
|
||||
- **Metadata comes from apktool.yml as apktool printed it.** Version codes
|
||||
keep their own spelling (`'7'` versus `7`); `package_name` is read from
|
||||
the decoded manifest, not inferred.
|
||||
- **The manifest is the decoded text**, bounded at 512 KiB and reported as
|
||||
truncated when larger.
|
||||
- **Strings are projected verbatim.** Escaped entities stay escaped, and
|
||||
entries containing nested markup (`<b>`, `<xliff:g>`) are counted as
|
||||
unparsed rather than flattened — the limitation names the count. The
|
||||
default table is bounded to 2000 entries with partial coverage when more
|
||||
exist; `locales` lists every `values-<locale>` that has a strings table.
|
||||
- **Smali is skipped by design** (`apktool d --no-src`): Java-source
|
||||
recovery belongs to the JADX family. Resource smali edits and rebuild are
|
||||
a follow-up lane with mutation authority.
|
||||
- **Workspaces are owned and short-lived.** Decoding happens in a
|
||||
provider-owned temporary directory that is removed before the result is
|
||||
returned. The APK snapshot and framework cache both live inside this directory.
|
||||
If removal fails, the result names the residual path and the provider retains
|
||||
ownership for a cleanup retry on close, including failed decodes.
|
||||
|
||||
## Resource and lifecycle limits
|
||||
|
||||
- The decode runs with a 600 s deadline and a 4 MiB output capture budget;
|
||||
a stopped decode reports `provider_timeout` and exceeded budgets report
|
||||
`resource_constraint`.
|
||||
- Manifest reads are bounded to 512 KiB before decoding UTF-8. Strings XML
|
||||
reads are bounded to 8 MiB; larger documents are skipped with partial
|
||||
coverage and an explicit limit, rather than reported as missing.
|
||||
- `decoded_file_count` is capped at 100,000 files; the cap is reported in
|
||||
the limitations when hit.
|
||||
- Apktool's own decode failures (`AndrolibException`, directory traversal
|
||||
refusals) surface as `unsupported_target` with apktool's reason preserved
|
||||
verbatim.
|
||||
|
||||
## Real verification
|
||||
|
||||
`npm run verify:apktool -- --apk PATH` exercises both operations against a
|
||||
real launcher. Record: apktool 2.7.0-dirty (Debian packaging) on Linux with
|
||||
OpenJDK 25 against a signed aapt2-built probe APK carrying two resource
|
||||
locales — launcher version, on-disk digest agreement, apktool.yml metadata
|
||||
(1.2.3, SDK 24–34), manifest package agreement, default and `de` string
|
||||
tables projected. Parser goldens in the test suite come from the same
|
||||
decode. See `docs/testing.md` for the lane summary.
|
||||
@@ -7,6 +7,16 @@ hashes, detected formats, bundle anatomy, runtime-family hints, and path-based
|
||||
bridge hypotheses. It does not parse plist or CMS semantics or claim observed
|
||||
runtime calls.
|
||||
|
||||
Framework hints match complete path segments such as `React.framework`,
|
||||
`ReactNative.framework`, `hermes.framework`, `Flutter.framework`, and
|
||||
`UnityFramework.framework`. Substrings in `Preact.framework`,
|
||||
`Community.framework`, or `MyApp.framework` do not identify those families.
|
||||
A generic `App.framework` also does not identify Flutter by itself. Runtime
|
||||
families remain inventory-derived hints, not observed loading. Generic framework
|
||||
and dylib paths do not establish Swift or Objective-C; this projection does not
|
||||
decode language metadata. A bridge basis is inferred from the native path and
|
||||
repeated for each paired script, rather than proving a particular binding.
|
||||
|
||||
```sh
|
||||
rea project-apple-application-graph '{"inventory_evidence":[<inventory_artifact Evidence>]}'
|
||||
```
|
||||
|
||||
@@ -91,6 +91,12 @@ one-shot invocations pass portable inline Evidence JSON; an MCP session uses
|
||||
the retained `evidence_id`. Page `limit` is required and bounded by the
|
||||
measured MCP stdio budget documented on the tool contract.
|
||||
|
||||
Over MCP, `inspect_binary_layout` with `"detail": "summary"` retains the
|
||||
complete layout Evidence in the session and returns only its summary view.
|
||||
Its `normalized_result.parent_evidence_id` names the retained layout for later
|
||||
section, symbol, mitigation or linkage views. The default `complete` detail and
|
||||
CLI output are unchanged.
|
||||
|
||||
Complete results have a 32 MiB input, 64 MiB reply, 1 MiB combined diagnostics
|
||||
and 30-second owned command deadline. The reply budget applies to the decoder
|
||||
record; the CLI/MCP Evidence envelope adds copies and encoding overhead. The
|
||||
@@ -136,3 +142,48 @@ malformed limit reports remain unknown with their read failure preserved. A
|
||||
received signal alone does not establish its exact cause. Dynamic tags require
|
||||
a complete DT_NULL inside PT_DYNAMIC; interpreter names and ranges end at the
|
||||
first NUL, with any padding still represented by the original segment range.
|
||||
|
||||
## Portable PE resources
|
||||
|
||||
`inspect_pe_resources` / `inspect-pe-resources` inspects an explicit PE32 or
|
||||
PE32+ file without an active target, native engine, Python, or Windows SDK:
|
||||
|
||||
```sh
|
||||
rea inspect-pe-resources ./selected.exe --json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "inspect_pe_resources",
|
||||
"arguments": { "path": "/artifacts/selected.exe" }
|
||||
}
|
||||
```
|
||||
|
||||
The complete Evidence identifies the stable artifact snapshot by path, byte
|
||||
count and SHA-256. Resources retain separate type, name and language identities:
|
||||
numeric IDs are distinct from UTF-16 names, whose original bytes are also
|
||||
reported. Directory headers, entries, data entries and payloads retain original
|
||||
file offsets and lengths. Each opaque payload has its own SHA-256; code page and
|
||||
reserved data-entry fields are preserved.
|
||||
|
||||
RT_GROUP_ICON records expose original image descriptors and RT_ICON candidates.
|
||||
A reference resolves only when an icon with the same numeric ID and language
|
||||
exists. Missing references, other-language candidates and declared-size mismatch
|
||||
remain explicit. Encoded dimensions are preserved (zero denotes 256 pixels).
|
||||
This is static reference inspection; no image decoding, language fallback,
|
||||
resource loading or selected-file execution occurs.
|
||||
|
||||
An absent resource data directory is a complete empty inventory. Malformed
|
||||
headers, truncated tables, cycles, duplicate sibling identities, unbacked or
|
||||
ambiguous RVA mappings, nonidentical overlapping ranges and unsupported tree
|
||||
shapes fail explicitly, rather than returning partial inventories. Exact shared
|
||||
payload ranges are supported. The supported tree has three levels: type, name,
|
||||
and language. Other resource payload formats remain opaque.
|
||||
|
||||
`max_file_bytes` defaults to 64 MiB (maximum 512 MiB); `max_entries` defaults to
|
||||
4096 (maximum 65536). CLI options are `--max-file-bytes` and `--max-entries`.
|
||||
Metadata is bounded to 8 MiB; exceeding any budget fails without truncation.
|
||||
Large Evidence envelopes may require a caller-configured MCP transport receive
|
||||
buffer because envelope copies add overhead. Requests honor cancellation during
|
||||
snapshot reading, directory traversal and payload hashing. The operation reads
|
||||
regular files only and rejects symbolic links.
|
||||
|
||||
@@ -15,7 +15,11 @@ when both Evidence sets already exist.
|
||||
|
||||
The request supplies a literal-port loopback endpoint and, for inspection, a
|
||||
target ID. REA accepts local hostless `file://` URLs that resolve to regular
|
||||
files; remote hosts, encoded path separators, and nonexistent paths are
|
||||
files, including pages a packaged application serves from inside `app.asar`.
|
||||
For those, the first `.asar` path component that is a regular file is the
|
||||
archive, the remainder must name a regular file in its header, and the reported
|
||||
path is the canonical archive path joined with that member. Remote hosts,
|
||||
encoded path separators, ASAR directories or links, and nonexistent paths are
|
||||
rejected. The endpoint exposes every eligible target and its local path
|
||||
metadata.
|
||||
|
||||
@@ -120,3 +124,5 @@ projection is intentionally target-only and partial: it binds the selected
|
||||
application path and capture outcome to the static graph, while frames, scripts,
|
||||
workers, and execution claims remain unavailable until a separate passive runtime
|
||||
capture provides them.
|
||||
|
||||
ASAR observation reads the header through one open file handle, validates its declared lengths against the archive size, and limits encoded JSON headers to 16 MiB before parsing. The header cache also retains at most 16 MiB of encoded headers across eight archives. Oversized headers produce a scope refusal.
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# MIPS ELF analysis through Ghidra
|
||||
|
||||
This lane extends native ELF analysis; it does not execute MIPS code or rehost
|
||||
firmware. It advances the generic-MIPS portion of
|
||||
[#718](https://github.com/morluto/rea/issues/718), not the whole firmware roadmap.
|
||||
PSP/Allegrex remains a separate specialization tracked in
|
||||
[#1330](https://github.com/morluto/rea/issues/1330).
|
||||
|
||||
## Admission boundary
|
||||
|
||||
ELF `EM_MIPS` is retained as the provider-neutral `mips` family. The target also
|
||||
retains class, byte order, `e_type`, raw `e_flags`, and inspected ABI declarations.
|
||||
Recognition does not mean every provider or MIPS variant is supported. Existing
|
||||
PE, managed and Mach-O architecture sets are unchanged.
|
||||
|
||||
The first Ghidra lane admits ELF32 `ET_EXEC`, little or big endian, with explicit
|
||||
o32 and `EF_MIPS_ARCH_32R2` flags and no machine-specific or ASE encoding flags.
|
||||
These are header declarations, not proof that every instruction conforms to
|
||||
that ISA. ELF64, n32/n64, unspecified ABI, MIPS16/microMIPS, machine-specific
|
||||
variants, shared/relocatable objects and PSP PRX are not covered by this lane.
|
||||
Their family may be identified, but provider selection returns the specific
|
||||
unsupported constraint rather than guessing a compatible profile.
|
||||
|
||||
Explicit `EF_MIPS_FP64` and `EF_MIPS_NAN2008` declarations are refused. Their
|
||||
absence is insufficient: Clang can emit identical `e_flags` for FP32, FP64 and
|
||||
soft-float targets. Target resolution also reads the 24-byte ABI flags record
|
||||
identified by `PT_MIPS_ABIFLAGS` and/or `SHT_MIPS_ABIFLAGS`, without relying on
|
||||
section names or the record being inside the initial 4 KiB header probe.
|
||||
Program and section declarations must agree; duplicate, conflicting, truncated
|
||||
or out-of-file records are rejected. Stripped section tables and section-only
|
||||
records are supported. Extended table numbering is explicitly unsupported.
|
||||
The field layout follows
|
||||
[`Elf_MIPS_ABIFlags_v0`](https://github.com/bminor/glibc/blob/master/elf/elf.h).
|
||||
|
||||
Provider admission requires record version 0, MIPS32r2, 32-bit GPR/CPR1, no CPR2,
|
||||
and a double-precision or FPXX ABI declaration with no ISA extension, ASE or
|
||||
unknown general flags. Missing records remain unknown rather than defaulting
|
||||
to a compatible ABI, and this initial lane refuses them. Unknown record values
|
||||
are retained for diagnosis but do not gain provider support. These checks bind
|
||||
the declared interpretation; the integer-only fixture does not establish full
|
||||
floating-point behavioral conformance.
|
||||
|
||||
MIPS header and ABI record facts are committed in the analysis profile so
|
||||
interpretation changes invalidate profile-bound snapshots. The support-lane
|
||||
identity is versioned independently of file identity. Evidence, lifecycle
|
||||
outputs and saved snapshots preserve the `mips` family. Hopper and IDA adapters
|
||||
do not implicitly gain MIPS support. Windows Ghidra P0 remains PE x86/x86-64 only.
|
||||
|
||||
## Use
|
||||
|
||||
With a caller-installed Ghidra and compatible JDK on a supported Linux/macOS host:
|
||||
|
||||
```bash
|
||||
rea analyze /absolute/path/to/mips.elf --provider ghidra --json
|
||||
rea function /absolute/path/to/mips.elf function_name --provider ghidra --json
|
||||
```
|
||||
|
||||
MCP uses the existing tools, including `open_binary` with an absolute path and
|
||||
`provider_id: "ghidra"`. No MIPS-specific tool catalog or emulator is introduced.
|
||||
Inspect instructions, caller relationships and data alongside pseudocode. In
|
||||
particular, delay-slot semantics are supplied by Ghidra, not reconstructed by
|
||||
REA's ELF parser.
|
||||
|
||||
## Verification
|
||||
|
||||
The source-owned freestanding fixture is `tests/conformance/c/mips.c`. The
|
||||
optional cross-target lane requires a caller-supplied Clang with MIPS targets,
|
||||
LLD, GNU `readelf`, Ghidra and its compatible JDK; ordinary host-native checks
|
||||
do not acquire these tools. `REA_MIPS_CLANG` selects Clang and
|
||||
`REA_MIPS_READELF` selects GNU readelf (for example `greadelf` on macOS).
|
||||
These are test-time prerequisites, not REA runtime providers.
|
||||
|
||||
```bash
|
||||
GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra npm run verify:ghidra:mips
|
||||
```
|
||||
|
||||
The lane compiles both byte orders and never executes the targets. GNU readelf
|
||||
independently supplies header/ABI facts and symbol addresses, compared with the
|
||||
production REA target resolver before provider startup. Its version, bounded
|
||||
command output and selected tool path are retained in the verification report.
|
||||
The parser deliberately recognizes only this fixture's GNU output, rejecting
|
||||
missing, ambiguous or unexpected fields rather than substituting defaults.
|
||||
|
||||
Existing public MCP operations check Ghidra's **observed** language/compiler
|
||||
specification and loaded-file hashes. The source-owned `mips-probe.S` supplies
|
||||
fixed ADDIU and conditional-branch encodings: verification checks bytes, length,
|
||||
immediate and direct destination, independent of decompiler variable names.
|
||||
Global and string bytes are checked at independently reported symbol addresses.
|
||||
ELF load-image results retain observations even though REA's independent
|
||||
load-image comparison currently covers DOS, not ELF; this lane checks those
|
||||
observations and does not relabel the upstream result as verified.
|
||||
|
||||
CLI/MCP discovery and decompilation, direct calls, target/profile/Evidence
|
||||
identity, snapshot round-trip and final process cleanup remain covered.
|
||||
Pseudocode presence is a liveness check, not proof of semantic equivalence.
|
||||
Instruction decoding and reader agreement likewise do not establish runtime,
|
||||
delay-slot execution or complete floating-point semantics. Assertion regressions
|
||||
reject incorrect byte order, constants, destinations and loaded identities.
|
||||
Parser/profile tests are narrower checks, not substitutes for this real-provider
|
||||
run. Report the actual lanes run and leave unexecuted verification pending.
|
||||
+122
-9
@@ -95,7 +95,7 @@ npx skills add morluto/rea --skill reverse-engineer-anything
|
||||
|
||||
This installs agent instructions and bundled references, not REA MCP
|
||||
registration or analysis engines. Follow the skill's
|
||||
[conditional connection guide](https://github.com/morluto/rea/blob/main/skill-src/reverse-engineer-anything/SKILL.md#connect-only-when-needed).
|
||||
[conditional connection guide](https://github.com/morluto/rea/blob/main/.agents/skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed).
|
||||
Working tools can be used immediately. If tools are missing, inspect the current
|
||||
client's registration with `doctor --client codex --json` (substitute its client
|
||||
ID), then plan repairs with `setup --client codex --dry-run --json`. Show and
|
||||
@@ -135,26 +135,138 @@ listed after the table because its connector is not one of these files:
|
||||
| Antigravity | `antigravity` |
|
||||
| GitHub Copilot CLI | `copilot_cli` |
|
||||
| Command Code | `commandcode` |
|
||||
| Qwen Code | `qwen_code` |
|
||||
| VS Code | `vscode` |
|
||||
| Grok Build | `grok_build` |
|
||||
| OMP | `omp` |
|
||||
| Pi | `pi` |
|
||||
| Hermes | `hermes` |
|
||||
|
||||
For [Pi](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/mcp.md),
|
||||
setup writes a `type: "stdio"` entry under `mcpServers.rea` in the user-level
|
||||
`~/.pi/agent/mcp.json` and installs the bundled skill under `~/.agents/skills`.
|
||||
Use `rea setup --client pi --dry-run --json` to inspect the plan, then
|
||||
`rea setup --client pi --yes --json` to approve it. Check the registration with
|
||||
`rea doctor --client pi --json`. Pi reads `mcp.json` as strict JSON, like its
|
||||
own loader: comments, trailing commas, a byte-order mark, or an empty existing
|
||||
file are invalid, and setup reports them instead of rewriting the file. Repair
|
||||
such a file to plain JSON first, then rerun setup.
|
||||
|
||||
Setup follows Pi's `PI_CODING_AGENT_DIR`: unset or empty selects the default;
|
||||
`~` and `~/` expand to the home directory; absolute paths and file URLs select
|
||||
that directory; relative paths stay relative to the command's working directory.
|
||||
On Windows, native paths, `~\`, and Git Bash/MSYS, Cygwin, and WSL drive paths
|
||||
are normalized as Pi does. Run REA and Pi from the same working directory when
|
||||
using a relative override. Pi has no profiles: `OMP_PROFILE`, `PI_PROFILE`,
|
||||
`PI_CONFIG_DIR`, and OMP's `enabledServers`/`disabledServers` lists do not affect
|
||||
Pi registration. Doctor accepts an omitted `type` for stdio but reports a
|
||||
non-stdio type as invalid; `enabled: false` stays disabled.
|
||||
|
||||
REA configures only the user scope. Pi also reads `.pi/mcp.json` in trusted
|
||||
projects; a same-name project entry can replace the user entry or override its
|
||||
enabled state and exposure. Check that file if the user registration is aligned
|
||||
but unavailable in a project. An extension that registers `/mcp` can replace
|
||||
Pi's built-in MCP support and use its own configuration instead. Run `/reload`
|
||||
in an existing Pi session after changing servers outside the session.
|
||||
|
||||
For Qwen Code, setup registers `rea` in the `mcpServers` table of the
|
||||
user-level `~/.qwen/settings.json`, or `$QWEN_HOME/settings.json` when configured.
|
||||
Setup and doctor follow Qwen Code's tilde and working-directory-relative home
|
||||
overrides. Qwen Code also discovers personal skills in `~/.agents/skills`,
|
||||
where setup installs the bundled workflow.
|
||||
|
||||
Gemini CLI uses `$GEMINI_CLI_HOME/.gemini/settings.json` when that home override
|
||||
is nonempty, including its shared personal skills at
|
||||
`$GEMINI_CLI_HOME/.agents/skills`. Setup and doctor check MCP allow/exclude
|
||||
policies from system defaults, user settings, trusted workspace settings, and
|
||||
system overrides. Workspace trust follows `GEMINI_CLI_TRUST_WORKSPACE`,
|
||||
`GEMINI_RESTRICTED_MODE`, the effective `security.folderTrust.enabled` setting,
|
||||
and the selected `trustedFolders.json` (including
|
||||
`GEMINI_CLI_TRUSTED_FOLDERS_PATH`). Settings in an untrusted workspace do not
|
||||
participate. A workspace or system `mcpServers.rea` replaces the entire user
|
||||
entry; setup stops if the winning definition conflicts with its planned entry.
|
||||
Review those files manually: setup never edits administrator/workspace settings
|
||||
or grants workspace trust. Trust supplied only by a running IDE is outside this
|
||||
standalone CLI check.
|
||||
|
||||
Codex treats an unset or empty `CODEX_HOME` as `~/.codex`. Devin honors
|
||||
`XDG_CONFIG_HOME` for its `devin/mcp_config.json` on non-Windows systems.
|
||||
|
||||
For OMP, setup writes a `type: "stdio"` entry to the user-level
|
||||
`~/.omp/agent/mcp.json`. It follows `PI_CONFIG_DIR`, an absolute
|
||||
`PI_CODING_AGENT_DIR`, and the profile selected by `OMP_PROFILE` or
|
||||
`~/.omp/agent/mcp.json`. It follows `PI_CONFIG_DIR` and
|
||||
`PI_CODING_AGENT_DIR` (relative to the setup working directory), and the profile selected by `OMP_PROFILE` or
|
||||
`PI_PROFILE` (`~/.omp/profiles/<name>/agent/mcp.json`). Setup also removes
|
||||
`rea` from that file's `disabledServers` list, which would otherwise hide the
|
||||
registration. Doctor treats an `enabled: false` entry as active when
|
||||
`enabledServers` lists `rea`, as OMP does, unless `disabledServers` also lists
|
||||
it. Run setup under each profile that should load REA.
|
||||
|
||||
For Hermes, setup writes a `mcp_servers.rea` entry to `config.yaml`, preserving
|
||||
comments and unrelated settings. It resolves that file from `HERMES_HOME`, else
|
||||
from the platform default Hermes itself uses — `%LOCALAPPDATA%\hermes` on
|
||||
Windows, `~/.hermes` elsewhere — including any `HERMES_DATA_DIR_SUFFIX`. Hermes
|
||||
connects an entry unless `enabled` is false, so setup states `enabled: true`.
|
||||
|
||||
Hermes personal skills install under the active Hermes home's `skills` directory.
|
||||
Setup, doctor and uninstall follow the root's `active_profile` selection, as
|
||||
plain Hermes CLI commands do. An explicit `HERMES_HOME` pointing to
|
||||
`profiles/<name>` selects that named profile directly. An invalid, missing or
|
||||
deleted sticky profile is reported instead of writing into the default home;
|
||||
repair the profile or run `hermes profile use default` before retrying.
|
||||
REA follows `HERMES_HOME` whitespace trimming, environment-variable expansion,
|
||||
and home expansion before resolving that profile. Setup previews the resolved
|
||||
configuration and skill destinations; doctor and uninstall use those same paths.
|
||||
|
||||
### DeepSeek Harness
|
||||
|
||||
DeepSeek Harness uses a Cordis YAML patch sequence rather than a `mcpServers`
|
||||
object. REA's `setup --client` does not edit this format. For Harness
|
||||
`0.2.0-rc.2`, append the following entry to `$DSH_HOME/cordis.patch.yml`
|
||||
(`~/.dsh/cordis.patch.yml` when `DSH_HOME` is unset). Preserve existing patches
|
||||
and replace the executable paths with absolute paths from your installation:
|
||||
|
||||
```yaml
|
||||
- insert:
|
||||
- id: rea-mcp
|
||||
name: "@deepseek-ai/dsh-mcp-client"
|
||||
config:
|
||||
serverName: rea
|
||||
transport: stdio
|
||||
command: /absolute/path/to/node
|
||||
args: ["/absolute/path/to/rea/scripts/rea.mjs", mcp]
|
||||
cwd: !!js process.cwd()
|
||||
```
|
||||
|
||||
For a global npm installation, the launcher is under
|
||||
`$(npm root -g)/rea-agents/scripts/rea.mjs`. Check the Node executable with
|
||||
`command -v node`. Register `serverName: rea` only once in each active profile,
|
||||
then restart Harness. It exposes tools as `mcp__rea__<tool_name>`.
|
||||
Run `rea setup --skill --dry-run`, review the destination, then
|
||||
`rea setup --skill --yes` to install the workflow in `~/.agents/skills`.
|
||||
Harness discovers that directory, or `$DSH_AGENTS_HOME/skills` when configured;
|
||||
its native `skill` tool can load `reverse-engineer-anything`.
|
||||
|
||||
REA's optional [native client verification](testing.md#end-to-end-integration-and-golden-evidence)
|
||||
checks headless chat, skill loading, all tool input schemas and actual analysis
|
||||
using the installed Harness and a local model fixture. This establishes the
|
||||
Harness integration, with live DeepSeek API compatibility remaining unverified.
|
||||
See the [Harness MCP guide](https://github.com/deepseek-ai/deepseek-harness/blob/639ed015397290b3745d163aafe02ffee4aa3f84/packages/mcp/mcp-client/README.md)
|
||||
for its native configuration contract.
|
||||
|
||||
### Other client settings
|
||||
|
||||
For OpenCode, setup writes the V1 `mcp.rea` entry, which OpenCode V1 and V2
|
||||
both load. If the configuration already uses OpenCode V2's native
|
||||
`mcp.servers` table, setup registers REA there instead and replaces any earlier
|
||||
`mcp.rea` entry from REA.
|
||||
`mcp.rea` entry from REA. Setup honors `OPENCODE_CONFIG_DIR` and
|
||||
`OPENCODE_CONFIG`, preferring the later-loaded JSONC file when both JSON and
|
||||
JSONC exist. The ordinary XDG global configuration still participates when a
|
||||
custom directory is selected. Doctor checks the merged user registration; uninstall removes
|
||||
REA-owned entries from every applicable user file so an older entry cannot
|
||||
become active again. Malformed applicable files block setup before changes.
|
||||
|
||||
Grok Build loads `[mcp_servers.rea]` from `$GROK_HOME/config.toml`, or from
|
||||
`~/.grok/config.toml` when `GROK_HOME` is unset. Setup edits that server
|
||||
`~/.grok/config.toml` when `GROK_HOME` is unset or empty. Setup edits that server
|
||||
table, `[mcp_servers.rea.env]`, and a root `disabled_mcp_servers` entry that
|
||||
names `rea`. It sets `startup_timeout_sec = 30` and leaves every other name
|
||||
in that list. The shared skill installed under `~/.agents/skills` is already
|
||||
@@ -194,10 +306,11 @@ save verified paths for an existing Ghidra installation.
|
||||
|
||||
The bundled skill is installed where each selected client discovers personal
|
||||
skills: Claude Code uses `~/.claude/skills` (or
|
||||
`$CLAUDE_CONFIG_DIR/skills` when configured), while other supported clients
|
||||
use the shared `~/.agents/skills` directory. A mixed selection plans both
|
||||
paths. Selecting the skill without a client uses the shared directory. Setup
|
||||
leaves existing skill copies in other locations untouched.
|
||||
`$CLAUDE_CONFIG_DIR/skills` when configured). Gemini CLI uses `.agents/skills`
|
||||
under its selected home; Antigravity 2.0 uses `~/.gemini/config/skills`.
|
||||
Other integrations, including Qwen Code, use `~/.agents/skills`.
|
||||
A mixed selection plans each distinct destination. Selecting the skill without a client uses the shared
|
||||
directory. Setup leaves existing skill copies in other locations untouched.
|
||||
|
||||
`doctor --skill --json` verifies the selected copies against the bundled
|
||||
instructions and references. Consumers should use `identity.skill.state`,
|
||||
|
||||
@@ -186,7 +186,10 @@ keys, and parser recovery remain partial or unknown.
|
||||
Objects and arrays passed to calls, constructors, or tagged templates, used
|
||||
as method receivers, or stored through property targets are not assumed
|
||||
unchanged afterward. Property stores conservatively retain reference escape
|
||||
uncertainty rather than pretending to execute later writes. Aliases
|
||||
uncertainty rather than pretending to execute later writes. References
|
||||
handed back by local functions, methods, constructors, async functions, and
|
||||
generators also retain escape uncertainty, including when the call is read-only;
|
||||
this does not establish that a mutation occurred. Aliases
|
||||
and shared children in spread and rest copies retain that uncertainty; copied
|
||||
primitive slots and unrelated containing properties remain known. Object rest
|
||||
excludes consumed keys, array rest excludes the consumed prefix for known
|
||||
@@ -230,7 +233,7 @@ or process workflows.
|
||||
|
||||
## CLI and verification
|
||||
|
||||
All five CLI commands accept inline JSON or a path to a JSON file. The CLI
|
||||
All six CLI commands accept inline JSON or a path to a JSON file. The CLI
|
||||
returns an Evidence record directly. Put the full records in a later CLI input;
|
||||
a separate CLI process has no retained MCP connection state.
|
||||
|
||||
|
||||
@@ -129,6 +129,16 @@ documents. Their runtime string-limit failures report a resource constraint
|
||||
with the selected path, observed bytes and UTF-16 limit, rather than malformed
|
||||
JSON.
|
||||
|
||||
`analyze_javascript_application` returns the complete analysis Evidence by
|
||||
default. Select `"detail": "summary"` to retain that complete Evidence in the
|
||||
current session and receive only its `inspect_analysis_view` summary: artifact
|
||||
identity, statistics, Electron surface counts, application and semantic
|
||||
coverage, limitations, and `normalized_result.parent_evidence_id`. Pass that ID
|
||||
to `inspect_analysis_view` for module pages or items, or to the application
|
||||
workflows as a retained reference. The summary does not repeat or truncate the
|
||||
analysis; a server without session retention refuses summary detail with
|
||||
`capability_unavailable`. CLI output is unchanged and always complete.
|
||||
|
||||
MCP prepares the complete repeated response incrementally against the pinned
|
||||
SDK's 10 MiB stdio receive-buffer budget. Oversized results return an actionable
|
||||
transport constraint and the exact same-session Evidence reference. Use
|
||||
|
||||
@@ -7,19 +7,46 @@ never executes the selected or recovered application code.
|
||||
|
||||
## Caller-supplied engine
|
||||
|
||||
The initial adapter is verified on Linux x64 with Wakaru **1.13.0**. Set
|
||||
`REA_WAKARU_COMMAND` to the absolute path of an executable supplied by the caller.
|
||||
The adapter uses util-linux `prlimit` (default `/usr/bin/prlimit`; override with
|
||||
an absolute `REA_JAVASCRIPT_PRLIMIT_COMMAND`). REA does not install either tool.
|
||||
Engine configuration is checked only when recovery is requested; other REA
|
||||
capabilities remain available without it.
|
||||
REA accepts Wakaru **^1.13.0** on Linux x64. The report parser is verified with
|
||||
Wakaru **1.14.0**. Another release in that range is accepted, the observed
|
||||
version is reported, and a limitation records that the parser was not verified
|
||||
against that release. Wakaru changes `--version`, `--json` and `provenance.json`
|
||||
only by addition within a major version
|
||||
([compatibility policy](https://github.com/pionxzh/wakaru/blob/v1.14.0/docs/cli.md#machine-readable-output-compatibility)),
|
||||
so the parser keeps unknown report fields and reported module statuses.
|
||||
|
||||
Set `REA_WAKARU_COMMAND` to the absolute path of an executable supplied by the
|
||||
caller. The adapter uses util-linux `prlimit` (default `/usr/bin/prlimit`;
|
||||
override with an absolute `REA_JAVASCRIPT_PRLIMIT_COMMAND`). REA does not install
|
||||
either tool. Engine configuration is checked only when recovery is requested;
|
||||
other REA capabilities remain available without it.
|
||||
|
||||
Unmodified upstream source is retained in `third_party/wakaru`, pinned to
|
||||
`6070266d24b32951fa2fe1d2dad5b4313bd5c81b` (v1.13.0, Apache-2.0).
|
||||
`8219bf5016a063d7fb215101a8c45d5959ec733f` (v1.14.0, Apache-2.0).
|
||||
The result reports the executed binary's exact path, SHA-256 and observed version.
|
||||
The audited source revision is distinct from `executed_source_revision: null`:
|
||||
a version string cannot prove which source produced a caller-supplied binary.
|
||||
|
||||
### Install Wakaru
|
||||
|
||||
Use the official release binary, the same one CI verifies:
|
||||
|
||||
```sh
|
||||
curl -fL https://github.com/pionxzh/wakaru/releases/download/v1.14.0/wakaru-linux-x64.tar.gz | tar -xz wakaru
|
||||
export REA_WAKARU_COMMAND="$PWD/wakaru"
|
||||
```
|
||||
|
||||
Or install it from npm. Point at the platform binary, not the Node.js shim in
|
||||
`node_modules/.bin`:
|
||||
|
||||
```sh
|
||||
npm install wakaru@1.14.0
|
||||
export REA_WAKARU_COMMAND="$(realpath node_modules/@wakaru/cli-linux-x64/wakaru)"
|
||||
```
|
||||
|
||||
Both give the same binary (SHA-256
|
||||
`664fa06782a61eda7ed15885134b52525fd565dd77607e642062ceb6cd927c38`).
|
||||
|
||||
## Recover and analyze
|
||||
|
||||
```sh
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# JEB instrumentation and analysis
|
||||
|
||||
REA exposes JEB-backed project, unit, and decompilation inspection through the CLI and MCP, served by JEB's own MCP interface. JEB is a bring-your-own engine: REA never installs it, launches it, or opens its project databases itself.
|
||||
|
||||
## Bring-your-own engine
|
||||
|
||||
Start a JEB client that serves MCP and point REA at it:
|
||||
|
||||
- Default endpoint: `http://127.0.0.1:8425/mcp`. Override with `REA_JEB_MCP_URL` (absolute `http`/`https` URL with literal host `127.0.0.1` or `[::1]`; redirects, remote hosts, credentials, query strings, and fragments are rejected rather than silently dropped).
|
||||
- In the JEB GUI client, start the MCP server from the _File_ menu (or via the VIBRE assistant).
|
||||
- Headless, use JEB's documented programmatic route (`JebMcpServerInstance` from the JEB API) with the JEB client on your classpath; JEB 5.48's `-c` headless mode does not accept `--mcp` directly.
|
||||
|
||||
Verified against JEB 5.48.0 (Linux x64, licensed build) serving `jeb-mcp-server` 1.3.0 over streamable HTTP.
|
||||
|
||||
## Operations
|
||||
|
||||
| MCP tool / CLI command | Purpose |
|
||||
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `inspect_jeb_client` / `inspect-jeb-client` | Engine version, GUI or headless mode, startup time |
|
||||
| `open_jeb_project` / `open-jeb-project` | Open or create a project from an artifact or `.jdb2` on the engine host; returns input-file SHA-256 digests and top-level units |
|
||||
| `list_jeb_units` / `list-jeb-units` | Page units by stable path and type (`filter`, `parent-unit-path`, `index`, `count`) |
|
||||
| `decompile_jeb_item` / `decompile-jeb-item` | Decompile one type or method to engine pseudo-code |
|
||||
|
||||
All four are read-only with respect to the target file; only `open_jeb_project` changes engine-side project state, and Evidence records the endpoint, the caller-selected parameters, and the engine-reported digests. Mutation workflows the engine also offers (renames, comments, `save_project`, `close_project`) are intentionally not exposed yet.
|
||||
|
||||
**Engine coverage note (verified):** JEB 5.48.0 headless MCP instances do not advertise `open_project` — the target must be selected when the engine client starts (`--infile` on the programmatic launcher, or the GUI's file opening). REA preserves that refusal with the engine's reason instead of failing silently; `open_jeb_project` completes only against engine builds that advertise the tool, such as GUI clients.
|
||||
|
||||
## Semantics
|
||||
|
||||
- Paths in `open_jeb_project` are resolved by the JEB process, not by REA; REA does not require the artifact to exist locally.
|
||||
- The engine caps `list_units` pages at 100; a full page is reported as `coverage: "partial"` because more units may exist. An empty remainder is `complete` only when fewer units than requested were returned.
|
||||
- When `decompile_jeb_item` omits `unit_path`, the engine implicitly selects the project's first code unit; REA reports `unit_path: null` and a limitation instead of guessing the effective unit.
|
||||
- Engine refusals (`success: false` envelopes) are preserved as unsupported-target errors carrying the engine's own message, for example a nonexistent item address.
|
||||
- An unreachable endpoint is a provider-availability failure with recovery guidance; REA does not retry or spawn the engine.
|
||||
|
||||
Real-engine verification lives in the JEB lane described in [testing.md](testing.md); mock transport tests do not establish real-engine compatibility.
|
||||
+83
-3
@@ -26,7 +26,16 @@ health transition leaves that catalog unchanged and does not emit
|
||||
Advertised input and output schemas contain no reachable recursive references.
|
||||
Schemas share repeated definitions through schema-local references while
|
||||
preserving complete nested fields and validation rules. Input properties retain
|
||||
their descriptions and literal examples. Root object unions are presented as
|
||||
their descriptions and literal examples.
|
||||
|
||||
Hosts forward input schemas to the model with every request, so advertised
|
||||
examples never copy complete producer Evidence. An embedded Evidence record is
|
||||
shown as its exact `{"kind":"retained-evidence","evidence_id":"ev_..."}`
|
||||
reference where the tool accepts one. An example that can only carry inline
|
||||
Evidence stays in the canonical contract and its executable tests, but is not
|
||||
advertised. Every advertised example is a distinct valid canonical input.
|
||||
|
||||
Root object unions are presented as
|
||||
one object without root `anyOf`, `oneOf`, or `allOf` for model API compatibility.
|
||||
The projection merges properties, retains shared required fields and
|
||||
group-specific field dependencies, and describes the accepted input groups.
|
||||
@@ -36,6 +45,49 @@ after SDK conversion and check advertised validation and actual calls.
|
||||
Individual model APIs can impose additional nesting limits; complete producer
|
||||
captures can exceed ten structural levels.
|
||||
|
||||
### Compact input schema profile
|
||||
|
||||
Some function-calling providers reject a request while any single tool's
|
||||
`parameters` schema exceeds a literal serialized size cap; Moonshot clients
|
||||
such as kimi-cli fail every request while the default catalog is attached
|
||||
([#1484](https://github.com/morluto/rea/issues/1484)). Setting the server's
|
||||
`REA_MCP_INPUT_SCHEMA_PROFILE` environment variable to `compact` renders every
|
||||
advertised input schema within a 13,312-byte budget, measured to sit below
|
||||
Moonshot's observed cap (community measurements: a 13.7 KB schema is accepted
|
||||
while a ~15 KB one is rejected). The setting is read at startup; restart the
|
||||
server to apply changes, as with `REA_MCP_MAX_RESPONSE_BYTES`. The default
|
||||
value `full` keeps the complete advertisement unchanged.
|
||||
|
||||
The compact profile renders the same tool set, output schemas, annotations,
|
||||
and canonical server-side validation. It changes only the advertised input
|
||||
JSON Schema:
|
||||
|
||||
- Annotation prose leaves the advertised form: nested property descriptions,
|
||||
literal examples, defaults, and titles are dropped. The root keeps its
|
||||
description because it states the accepted input groups.
|
||||
- Schema-local references are inlined within the budget. Kimi Code expands
|
||||
references before sending provider requests, so sharing definitions cannot
|
||||
establish the provider-facing size. Expansion stops at the budget before
|
||||
allocating an oversized presentation. Unconstrained JSON fields advertise
|
||||
all JSON value types explicitly so Kimi cannot infer string for Evidence
|
||||
objects or arrays.
|
||||
- The few schemas whose validation structure alone exceeds the budget
|
||||
(`compare_web_captures`, `build_reconstruction_obligation_ledger`,
|
||||
`evaluate_reconstruction_coverage`, `project_managed_application_graph`,
|
||||
`compare_application_versions`, `capture_browser_scenario`) are advertised
|
||||
in a reduced form: property names, reference-resolved types (including union
|
||||
alternatives), first-level guidance, and the root's required-field constraints,
|
||||
with nested validation detail elided. The
|
||||
canonical schema still rejects malformed calls server-side, so callers
|
||||
following the reduced advertisement receive the canonical typed error
|
||||
instead of silent acceptance.
|
||||
|
||||
Boundary tests pin every advertised compact schema to the budget, keep them
|
||||
valid JSON Schema that still accepts every canonical example, and hold the
|
||||
set of six reduced presentations as contracts evolve. Client verification
|
||||
also checks provider-facing sizes and canonical examples after conversion by
|
||||
the actual Kimi Code CLI; local captures do not establish Moonshot acceptance.
|
||||
|
||||
Self-contained output schemas advertise a content-bound `$id`, including their
|
||||
declared dialect. SDK validators can reuse compiled schemas across complete
|
||||
catalog refreshes and equivalent tool outputs; changing the schema changes its
|
||||
@@ -101,6 +153,12 @@ if retention is unavailable or fails, the diagnostic reports that reason.
|
||||
REA accepts ordinary `tools/call` progress tokens. Updates are monotonic,
|
||||
rate-bounded to at most one intermediate update per 100 ms, and always allow a
|
||||
terminal update. Unknown totals are omitted; REA does not fabricate percentages.
|
||||
`capture_process_scenario` uses that boundary only when the client supplies a
|
||||
progress token. Notifications set `progress` to the count of collected terminal
|
||||
frames, process samples, and interaction events, omit `total`, and put the
|
||||
lifecycle phase, elapsed time, and final cleanup status in `message`. A client
|
||||
that does not send a progress token receives no progress notifications. The
|
||||
tool result is unchanged either way.
|
||||
Provider calls receive the request cancellation signal. Artifact traversal,
|
||||
hashing, version comparisons, Hopper requests, and process capture
|
||||
check the same signal. Cancellation is distinct from timeout. A cleanup failure
|
||||
@@ -260,10 +318,20 @@ Ordinary responses keep their existing complete result contract.
|
||||
|
||||
## Retained application Evidence inputs
|
||||
|
||||
`analyze_javascript_application` and `inspect_binary_layout` accept `detail`:
|
||||
`complete` (default) returns the complete analysis Evidence; `summary` records
|
||||
that complete Evidence in the session and returns the derived
|
||||
`inspect_analysis_view` summary Evidence instead. Its
|
||||
`normalized_result.parent_evidence_id` and `evidence_links` name the retained
|
||||
analysis, so later views and application workflows read it without repeating
|
||||
analysis or moving the complete record through the response. A server without
|
||||
session retention refuses summary detail with `capability_unavailable`.
|
||||
|
||||
`inspect_analysis_view` projects a caller-selected view of already completed
|
||||
`inspect_binary_layout`, `analyze_javascript_application`, or `analyze_function`
|
||||
Evidence. Native views select procedure, pseudocode, assembly, callers/callees,
|
||||
references, and high-pcode facets without starting a provider. Native offset
|
||||
Evidence. Native views select procedure, pseudocode, assembly, basic blocks,
|
||||
comments, callers/callees, references, unresolved calls, referenced strings and
|
||||
names, the native API record, and high-pcode facets without starting a provider. Native offset
|
||||
and limit default to 0 and 64; pseudocode uses UTF-16 code units and never
|
||||
splits surrogate pairs, while other pages count rows. Unavailable facts and
|
||||
provider limitations remain explicit. Source is
|
||||
@@ -381,3 +449,15 @@ and reconstruction cannot treat it as unchanged.
|
||||
canonical `integrity_contradictions` records and marks application graph
|
||||
coverage partial when any mismatch is continued. Contradicted nested ASARs
|
||||
remain opaque.
|
||||
|
||||
`extract_artifact` accepts the same policy in MCP and as `--integrity-policy`
|
||||
on `rea extract-artifact`. With `record-and-continue` it writes the observed
|
||||
bytes and returns the `integrity_contradictions` records for the extracted
|
||||
files. An active ASAR unpacked entry whose companion file is absent cannot be
|
||||
materialized; extraction fails as `unavailable` with that logical path.
|
||||
|
||||
Packaging tools commonly sign or strip `.asar.unpacked` native binaries after
|
||||
writing the archive header, so their declared hashes no longer match. Electron
|
||||
does not check unpacked companions against those hashes at runtime. Integrity
|
||||
failures for such entries name this cause and the operation to rerun with
|
||||
`record-and-continue`; REA does not exempt unpacked entries silently.
|
||||
|
||||
@@ -195,6 +195,14 @@ outside-memory and undecodable addresses have separate outcomes. Effective
|
||||
memory base/index/displacement roles and per-instruction context mode remain
|
||||
unavailable; `mode` is the program language variant.
|
||||
|
||||
Compact instruction references and call-target references preserve the source
|
||||
address, operand index, DATA/READ/WRITE classification, primary status, provider
|
||||
`provenance`, and reference `source` (for Ghidra: `analysis`, `default`, `imported`
|
||||
or `user_defined`). Primary status reports the provider's chosen reference; it
|
||||
does not establish literal addressing. Analyzer-inferred indexed targets remain
|
||||
static references, not observed runtime reads or writes. Older evidence may omit
|
||||
the additive fields, and null or omitted facts mean unavailable rather than false.
|
||||
|
||||
Ghidra distinguishes a return using decoded instruction p-code's `RETURN`
|
||||
operation. `flow.classification_evidence` preserves the Listing flow type and
|
||||
the observed return evidence; mnemonic spelling alone is not the classifier.
|
||||
|
||||
+44
-5
@@ -51,6 +51,34 @@ format is TOON. The file contains the complete capture Evidence record.
|
||||
Choose an output file distinct from the scenario input: shell redirection opens
|
||||
and truncates the output before REA reads the scenario.
|
||||
|
||||
While the capture runs, REA writes live status to stderr as one JSON object per
|
||||
line. Stdout stays the final Evidence document:
|
||||
|
||||
```json
|
||||
{
|
||||
"rea_progress": {
|
||||
"phase": "running",
|
||||
"completed": 3,
|
||||
"total": null,
|
||||
"message": "elapsed_ms=1200 frames=1 samples=2 interactions=0",
|
||||
"sequence": 4
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`phase` is the observed lifecycle stage: `prepare`, `running`, `settling`, or
|
||||
`cleanup`. `completed` counts collected terminal frames, process samples, and
|
||||
interaction events. `total` stays null; REA does not invent a unit total or a
|
||||
percentage. `message` reports elapsed time and those counts. The final line uses
|
||||
`phase` `cleanup`, sets `terminal` to true, and adds `disposition` (`exited`,
|
||||
`timeout`, `idle_timeout`, `cancelled`, or `failed`) plus the owned-process,
|
||||
renderer, and temporary-root cleanup states. Updates use the shared progress
|
||||
boundary and are limited to one intermediate line per 100 ms; the terminal line
|
||||
is always emitted. If a receiver is slow, intermediate observations are merged
|
||||
into the latest status, and stale live observations are discarded before final
|
||||
cleanup status. Progress never copies child output. Child output remains in the
|
||||
capture Evidence on stdout.
|
||||
|
||||
For example:
|
||||
|
||||
```json
|
||||
@@ -110,11 +138,22 @@ while comparisons and trace assertions use the normalized text. Retaining the
|
||||
original adds at most `limits.output_bytes` of text, because it comes from the
|
||||
same admitted PTY chunks rather than a second unbounded stream.
|
||||
|
||||
Rendered frame `lines` hold the visible rows after normalization, without
|
||||
trailing spaces; pad a line to the frame's `columns` to restore the fixed-width
|
||||
row. `serialized_state` keeps the exact terminal state. Captures commit this
|
||||
line format in their comparison contract, so a capture whose lines were padded
|
||||
to full width does not compare as the same contract.
|
||||
Rendered frame `lines` hold the visible rows after normalization, with trailing
|
||||
U+0020 spaces removed. Blank rows and internal spaces remain. `columns` records
|
||||
terminal cell width; normalization and Unicode can make string lengths differ
|
||||
from that width. `serialized_state` holds terminal serialization after the
|
||||
selected normalization. Original admitted PTY chunks are available through
|
||||
`raw_data ?? data`, as described above. Captures commit this line format in their
|
||||
comparison contract, so a capture whose lines were padded to full width does not
|
||||
compare as the same contract.
|
||||
|
||||
Custom `normalization.patterns` replace literal text with the caller's literal
|
||||
replacement, including dollar sequences such as `$&` and `$$`. An empty pattern
|
||||
also matches the first and last string positions. Captures with custom patterns
|
||||
commit `pattern_normalization_version: "literal-replacements-v1"`. Legacy captures
|
||||
remain readable. Cross-version comparison is rejected; capture both sides with
|
||||
the same replacement semantics. Captures without custom patterns keep their
|
||||
existing comparison contract.
|
||||
|
||||
Port normalization recognizes explicit `port`, `tcp_port`, `udp_port`, and
|
||||
`listen` fields, URL authorities, IP endpoints, and `localhost` endpoints, with
|
||||
|
||||
+199
-2
@@ -88,6 +88,137 @@ entrypoint and include its verifier/fixture files in their cache inputs.
|
||||
|
||||
## End-to-end, integration and golden evidence
|
||||
|
||||
The optional `verify:qwen-client`, `verify:pi-client`, and `verify:hermes-client`
|
||||
lanes require an installed native client. Select its executable with
|
||||
`REA_VERIFY_QWEN_COMMAND`, `REA_VERIFY_PI_COMMAND`, or `REA_VERIFY_HERMES_COMMAND`.
|
||||
They configure disposable profiles through the public REA CLI, preserve caller
|
||||
settings and backups, and exercise real stdio MCP calls and skill loading. The
|
||||
`call` mode checks complete catalog discovery and actual JavaScript Evidence;
|
||||
pass `-- chat` to check plain chat with REA enabled. Qwen also reads the full
|
||||
result that its client offloads, and Pi exercises default codemode execution.
|
||||
Use `REA_VERIFY_RUNTIME_ROOT` to select a production-only installed REA package.
|
||||
Receipts retain client versions, result digests, host coverage and owned-process
|
||||
cleanup. These POSIX lanes use a deterministic loopback model, so they do not
|
||||
prove live model-provider or native Windows compatibility. Qwen and Pi use
|
||||
caller-configured skill directories to isolate the fixture account from the OS
|
||||
home; this does not prove their default home-directory discovery.
|
||||
Set `REA_VERIFY_HERMES_STICKY_PROFILE=1` to exercise native Hermes selection of
|
||||
a named sticky profile with `HERMES_HOME` still pointing to its root.
|
||||
|
||||
`verify:gemini-client` requires an installed Gemini CLI (verified with
|
||||
`@google/gemini-cli@0.63.0`); select it with `REA_VERIFY_GEMINI_COMMAND`.
|
||||
The optional POSIX lane uses native `GEMINI_CLI_HOME` discovery in an isolated
|
||||
Git project, checks setup plans, backups and idempotence, activates the installed
|
||||
personal skill, validates all forwarded input JSON Schemas against their declared
|
||||
dialect, forwards the complete REA catalog and checks full JavaScript
|
||||
Evidence for a Unicode path. Use `-- chat` for ordinary chat with REA enabled,
|
||||
or `REA_VERIFY_RUNTIME_ROOT` for a production-only installed REA package.
|
||||
The local fixture exercises the real Gemini API adapter, including its
|
||||
`untrusted_context` tool-result envelope. Token counts are synthetic; live
|
||||
Google API and native Windows compatibility remain unverified. The lane disables
|
||||
the client's memory-based relaunch to preserve the caller's Node heap budget.
|
||||
|
||||
`verify:opencode-client` requires an installed OpenCode (verified with
|
||||
`opencode-ai@1.18.35`); select it with `REA_VERIFY_OPENCODE_COMMAND`.
|
||||
The optional POSIX lane configures isolated XDG roots and `OPENCODE_CONFIG_DIR`,
|
||||
checks setup plans, backups, preserved JSONC comments and idempotence, activates
|
||||
the installed skill, validates all forwarded input schemas and verifies complete
|
||||
JavaScript Evidence for a Unicode path. JSONC is the default fixture; set
|
||||
`REA_VERIFY_OPENCODE_CONFIG_FORMAT=json` for JSON. Use `-- chat` for ordinary
|
||||
chat or `REA_VERIFY_RUNTIME_ROOT` for a production-only installed package.
|
||||
The native core runs with external plugins disabled through `OPENCODE_PURE`;
|
||||
the local OpenAI-compatible fixture has a caller-declared one-million-token
|
||||
context and synthetic usage. Skills use an explicit isolated `skills.paths`
|
||||
directory, so default OS-home discovery remains unverified. Live model APIs and
|
||||
native Windows are also unverified.
|
||||
|
||||
`verify:claude-client` requires installed Claude Code (verified with
|
||||
`@anthropic-ai/claude-code@2.1.296`); select it with `REA_VERIFY_CLAUDE_COMMAND`.
|
||||
The optional POSIX lane uses native `CLAUDE_CONFIG_DIR` discovery, an independent
|
||||
Git workspace, user settings and default built-in tools. It preserves caller
|
||||
preferences and backups, checks idempotent setup, waits through the native
|
||||
`WaitForMcpServers` tool when discovery is pending, and validates the complete
|
||||
catalog and forwarded input schemas. Call mode loads the personal skill through
|
||||
native `Skill`, checks its full body and verifies named-schema JavaScript Evidence
|
||||
for a Unicode path. Use `-- chat` for ordinary chat with REA enabled, or
|
||||
`REA_VERIFY_RUNTIME_ROOT` for a production-only installed package. The local
|
||||
Anthropic Messages/SSE fixture preserves native resource-hint envelopes in its
|
||||
request artifacts. Usage is synthetic; live Anthropic API, native Windows and
|
||||
bare-mode personal-skill activation remain unverified by this lane.
|
||||
|
||||
`verify:copilot-client` requires installed GitHub Copilot CLI (verified with
|
||||
`@github/copilot@1.0.95`); select it with `REA_VERIFY_COPILOT_COMMAND`.
|
||||
The optional POSIX lane uses native `COPILOT_HOME` discovery, an independent
|
||||
Git workspace, guarded setup plans, preserved registrations, backups and
|
||||
idempotence. Native `skill add` registers the isolated installed skill; call
|
||||
mode loads its full body, validates all forwarded input schemas and checks
|
||||
named-schema JavaScript Evidence for a Unicode path. When Copilot spills a
|
||||
large MCP result, the model requests native `view` with `forceReadLargeFiles`
|
||||
and validates the complete returned Evidence rather than its preview.
|
||||
Use `-- chat` for ordinary
|
||||
chat or `REA_VERIFY_RUNTIME_ROOT` for a production-only installed package.
|
||||
The native offline BYOK adapter uses a loopback OpenAI completions/SSE fixture
|
||||
and `gpt-5.4` model metadata; model inference and token usage are synthetic.
|
||||
Set `REA_VERIFY_COPILOT_MODEL` to examine another model configuration. In the
|
||||
verified client, `gpt-4.1` blocks the complete catalog before HTTP with
|
||||
`compaction_static_context_blocked`, even when requesting a larger BYOK prompt
|
||||
capacity. The effective capacity is unknown; this lane does not establish a
|
||||
fix for that client/model limit. It preserves the complete catalog and schemas.
|
||||
Live model APIs, native Windows and default OS-home skill discovery remain
|
||||
unverified.
|
||||
|
||||
`verify:grok-client` requires installed Grok Build (verified with the official
|
||||
Linux x64 1.0.50 binary); select it with `REA_VERIFY_GROK_COMMAND`. This optional
|
||||
POSIX lane isolates `GROK_HOME`, an independent Git workspace and additional
|
||||
skill roots, disables foreign configuration discovery, guards setup writes,
|
||||
preserves unrelated registrations/backups and checks idempotence. Call mode
|
||||
loads the full skill through native `read_file`, queries all REA names and input
|
||||
schemas through native `search_tool`, and calls analysis through `use_tool`.
|
||||
If discovery reports `partial`, the native agent diagnoses REA registration and
|
||||
retries discovery. No startup-timeout override or fixed readiness delay is used.
|
||||
A larger Unicode-path fixture exercises native result offloading: the full
|
||||
retained Evidence is validated against its named schema, then native terminal
|
||||
queries recover the selected export facts, subject and full artifact digest in
|
||||
the next model request. This verifies useful artifact recovery without claiming
|
||||
that every offloaded graph fact enters model context. Native line-number and
|
||||
truncation envelopes remain in the request artifacts. Use `-- chat` for ordinary
|
||||
chat, or `REA_VERIFY_RUNTIME_ROOT` for a production-only installed package.
|
||||
The loopback OpenAI completions/SSE custom model declares a one-million-token
|
||||
context and synthetic usage. Live xAI APIs, native Windows and default OS-home
|
||||
skill discovery remain unverified.
|
||||
|
||||
`verify:omp-client` requires installed OMP (verified with the official Linux x64
|
||||
18.8.7 binary); select it with `REA_VERIFY_OMP_COMMAND`. This optional POSIX lane
|
||||
isolates the default agent, global config and XDG roots, guards setup targets,
|
||||
preserves unrelated registrations and backups, and checks idempotence. It
|
||||
compares native rejection of an invalid profile with REA's refusal to plan
|
||||
fallback writes. OMP's default `xd://` interface mounts the complete REA catalog
|
||||
as devices: call mode reads the complete installed skill and tool documentation,
|
||||
then dispatches analysis through native `write`. It checks full named-schema
|
||||
JavaScript Evidence with a Unicode path in the next model request. Device
|
||||
metadata and documentation are distinct from forwarding all JSON schemas as
|
||||
model functions. Native print-mode MCP readiness uses its defaults.
|
||||
Use `-- chat` for ordinary chat or `REA_VERIFY_RUNTIME_ROOT` for a production-only
|
||||
installed package. The loopback OpenAI completions/SSE model fixture declares a
|
||||
one-million-token context and synthetic usage. Live model APIs, native Windows,
|
||||
named-profile native execution and default OS-home skill discovery are
|
||||
unverified; skill discovery uses an explicit isolated custom directory.
|
||||
|
||||
`verify:deepseek-client` requires an installed DeepSeek Harness (`dsh`;
|
||||
verified with `@deepseek-ai/dsh@0.2.0-rc.2`) and Git. Set
|
||||
`REA_VERIFY_DEEPSEEK_COMMAND` to select its executable. This optional POSIX lane
|
||||
installs the skill through `rea setup --skill`, writes the native Cordis MCP
|
||||
patch into an isolated `DSH_HOME`, and uses `DSH_AGENTS_HOME` for native personal
|
||||
skill discovery. A separate Git root prevents inherited project skills from
|
||||
masking a missing installation. The default `call` mode checks skill loading,
|
||||
complete catalog discovery, the actual forwarded input schemas and regexes,
|
||||
and full JavaScript Evidence for a Unicode path. Use `-- chat` for ordinary
|
||||
chat with REA enabled, or `REA_VERIFY_RUNTIME_ROOT` for an installed package.
|
||||
The fixture uses Harness's custom OpenAI adapter and the one-million-token
|
||||
context capacity declared by its default DeepSeek Flash model; it does not
|
||||
establish live DeepSeek API or native Windows coverage. Receipts include
|
||||
request artifacts, result digests and owned-process lineage.
|
||||
|
||||
Full E2E tests invoke the production command dispatcher and real providers,
|
||||
without fake launchers, runners or responses. `verify:keyed-archive` writes an
|
||||
actual Foundation binary archive, runs the CLI and a separate stdio MCP
|
||||
@@ -405,6 +536,25 @@ kept under ignored `_reference/`. No Gradle build, Android SDK, emulator or
|
||||
application execution is required. The lane compares real CLI/MCP package,
|
||||
class search, class inventory, method decompilation and incoming references.
|
||||
See [Android analysis](android-analysis.md) for boundaries and resource budgets.
|
||||
|
||||
`verify:jeb` requires a caller-started JEB client serving MCP at
|
||||
`REA_JEB_MCP_URL` (default `http://127.0.0.1:8425/mcp`) with a project
|
||||
already open, and verifies real CLI client inspection, unit listing coverage,
|
||||
and method decompilation. The script records the engine's exact response to
|
||||
`open_jeb_project`; JEB 5.48.0 headless instances do not advertise that tool.
|
||||
REA does not install or launch JEB; see [JEB analysis](jeb-analysis.md) for
|
||||
the bring-your-own boundary. Verified against JEB 5.48.0 serving
|
||||
`jeb-mcp-server` 1.3.0 at four scales: a compiled Java class fixture, a
|
||||
locally built signed probe APK with a launcher activity (manifest, v1/v2/v3
|
||||
certificates, dex bytecode, filtered and paginated listing), the published
|
||||
Signal 8.30.3 universal release APK (109 MB, R8-processed Kotlin/Compose,
|
||||
four signature schemes, native arm64 ELF units) with MainActivity method
|
||||
decompilation through both CLI and MCP, and the official Flutter Gallery
|
||||
2.9.2 release APK (112 MB): per-ABI `libapp.so` Dart AOT snapshot images
|
||||
analyzed as native code (75k methods on arm64) and decompiled through both
|
||||
CLI and MCP with explicit unit selection, plus the thin Java wrapper and
|
||||
decoded manifest. The dedicated Dart snapshot processor and `run_script`
|
||||
remain GUI-only surfaces in JEB 5.48.0 headless.
|
||||
Authenticated IPA and macOS application inventory projection is documented in
|
||||
[Apple application analysis](apple-application-analysis.md).
|
||||
|
||||
@@ -414,6 +564,51 @@ Synthetic producer regressions run independently:
|
||||
npm run test:focused -- tests/boundary/process/jadxIntegration.test.ts tests/boundary/process/androidAnalysisMcp.test.ts
|
||||
```
|
||||
|
||||
## Apktool resource decoding
|
||||
|
||||
`npm run verify:apktool -- --apk PATH` exercises both Apktool operations
|
||||
against a real launcher (`REA_APKTOOL_COMMAND` or PATH) and one APK. No
|
||||
download or build step is involved. Record: apktool 2.7.0-dirty (Debian
|
||||
packaging) on Linux with OpenJDK 25, against a signed aapt2-built probe APK
|
||||
carrying two resource locales — launcher version, on-disk digest agreement
|
||||
with the reported target identity, apktool.yml metadata projection
|
||||
(1.2.3, SDK 24–34), manifest package agreement, and both the default and
|
||||
`de` string tables projected through the locale option. Parser goldens in
|
||||
`src/apktool/ApktoolDecodeOutput.test.ts` come from the same decode. See
|
||||
[Apktool resource analysis](apktool-resource-analysis.md) for boundaries and
|
||||
budgets.
|
||||
|
||||
## ADB device acquisition
|
||||
|
||||
`npm run verify:adb` exercises every ADB operation against a live device or
|
||||
emulator through the caller's adb binary (`REA_ADB_PATH` or PATH). No SDK
|
||||
installation or emulator management is involved; the device-mutating tools
|
||||
run only in the explicit lifecycle section (`--install-apk` drives a
|
||||
lane-owned install → resolve → start → observe → force-stop → uninstall
|
||||
probe). `--pull` acquires a real package and re-digests the pulled files on
|
||||
disk against the returned SHA-256 values; `--serial` selects a device when
|
||||
several are attached.
|
||||
|
||||
Record: adb 34.0.5-debian on Linux against an Android 14 (API 34) x86_64
|
||||
emulator. The lane covered the complete observation surface — 337-process
|
||||
listing, 287 binder services including AIDL `/`-suffixed names, 92 features
|
||||
including hex GL versions, display size/density, window focus (legitimately
|
||||
null on headless devices), `settings get global adb_enabled`, bounded
|
||||
logcat, directory listings — plus a real two-APK split set
|
||||
(`base.apk` plus `split_probe.apk`, built and installed through
|
||||
`install-multiple`) pulled with byte-exact digests, a push/pull roundtrip
|
||||
verified by the device's own sha256sum, a 1.3 MB screen capture with PNG
|
||||
dimensions, `dumpsys package` projection, and the full lifecycle: unique
|
||||
resolution before launch, launcher-activity start through
|
||||
`cmd package resolve-activity` with `am start -W`, the started app visible
|
||||
in the process listing, force-stop, uninstall, and zero matches after
|
||||
removal. System-package pulls whose APKs keep non-`base.apk` names report
|
||||
the `unknown` role with the file-name basis, verified with
|
||||
`com.android.settings` (single 73.9 MB APK). Parser goldens for
|
||||
`adb devices -l`, `getprop`, `pm list packages -f`, and `pm path` come from
|
||||
the same device. See [ADB device analysis](adb-device-analysis.md) for
|
||||
boundaries and budgets.
|
||||
|
||||
## Optional NativeAOT Ghidra analysis
|
||||
|
||||
This lane is separate from the default native lane. `build:fixtures:nativeaot`
|
||||
@@ -714,12 +909,14 @@ Neither optional toolchain is a base-lane prerequisite. See
|
||||
|
||||
Build once with `npm run build:cached`, then run `npm run verify:javascript:recovery`.
|
||||
This focused lane requires Linux x64, util-linux `prlimit`,
|
||||
`REA_WAKARU_COMMAND` pointing to the official Wakaru 1.13.0 Linux x64 binary,
|
||||
`REA_WAKARU_COMMAND` pointing to the official Wakaru 1.14.0 Linux x64 binary,
|
||||
and `REA_JAVASCRIPT_FIXTURE_TOOLS` pointing to an isolated npm prefix containing
|
||||
esbuild 0.25.10 and webpack 5.101.3. No global installation is required.
|
||||
The lane compiles source-owned fixtures, exercises CLI and stdio MCP, verifies
|
||||
published bytes and provenance, feeds recovered modules into existing analysis,
|
||||
and compares a finite set of known fixture results. It does not establish
|
||||
and compares a finite set of known fixture results. The provider also accepts
|
||||
other ^1.13.0 releases and reports them as unverified; this lane proves the
|
||||
audited release. It does not establish
|
||||
arbitrary recovered-application equivalence. CI installs these prerequisites only
|
||||
in `.github/workflows/real-javascript-recovery.yml`; the existing `real-browser`
|
||||
lane uses real Chrome for browser capture and website workflows.
|
||||
|
||||
Generated
+2
-2
@@ -41,6 +41,7 @@
|
||||
"stream-json": "3.7.0",
|
||||
"write-file-atomic": "7.0.0",
|
||||
"ws": "8.21.0",
|
||||
"yaml": "2.9.0",
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"bin": {
|
||||
@@ -64,8 +65,7 @@
|
||||
"turbo": "2.11.7",
|
||||
"typescript": "7.0.2",
|
||||
"vitepress": "1.6.4",
|
||||
"vitest": "5.0.3",
|
||||
"yaml": "2.9.0"
|
||||
"vitest": "5.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.19.0 || ^24.11.0 || >=26.0.0"
|
||||
|
||||
+18
-3
@@ -50,6 +50,7 @@
|
||||
"bridge/ghidra/ReaGhidraBridge.java",
|
||||
"bridge/ghidra/ReaGhidraExtensions.java",
|
||||
"bridge/ghidra/ReaGhidraPrepareCom.java",
|
||||
"bridge/ghidra/ReaGhidraNoReturnFix.java",
|
||||
"bridge/android/ReaJadxBridge.java",
|
||||
"bridge/mitmproxy/capture.py",
|
||||
"scripts/rea.mjs",
|
||||
@@ -146,7 +147,10 @@
|
||||
"verify:macos-bundle": "npm run build:cached && node scripts/verify/apple/macos-bundle.mjs",
|
||||
"verify:legacy-simulator": "npm run build:cached && node scripts/verify/apple/legacy-simulator.mjs",
|
||||
"verify:architecture-guards": "node scripts/verify/architecture-guards.mjs",
|
||||
"verify:apktool": "npm run build:cached && node scripts/verify-apktool.mjs",
|
||||
"verify:adb": "npm run build:cached && node scripts/verify-adb.mjs",
|
||||
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
|
||||
"verify:ghidra:mips": "npm run build:cached && node scripts/verify-real-ghidra-mips.mjs",
|
||||
"verify:ghidra:swift": "npm run build:cached && node scripts/verify-real-ghidra-swift.mjs",
|
||||
"verify:ida": "npm run build:cached && node scripts/verify-real-ida.mjs",
|
||||
"verify:ghidra:switch": "npm run build:cached && node scripts/verify-real-ghidra-switch.mjs",
|
||||
@@ -169,6 +173,7 @@
|
||||
"verify:firmware": "npm run build:cached && node scripts/verify/firmware/analysis.mjs",
|
||||
"fixtures:firmware": "node scripts/fixtures/firmware/generate.mjs",
|
||||
"verify:android": "npm run build:cached && node scripts/verify/android/analysis.mjs",
|
||||
"verify:jeb": "npm run build:cached && node scripts/verify-jeb.mjs",
|
||||
"fixtures:android": "node scripts/fixtures/android/fetch.mjs",
|
||||
"verify:agent": "npm run build:cached && node scripts/verify-agent-experience.mjs",
|
||||
"verify:hopper:linux": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-linux.mjs",
|
||||
@@ -179,6 +184,16 @@
|
||||
"verify:readiness": "npm run build:cached && npm run verify:fixtures && node scripts/verify-reconstruction-readiness.mjs",
|
||||
"verify:package": "npm run build:cached && node scripts/verify-package.mjs",
|
||||
"verify:package:windows": "npm run build:cached && node scripts/verify-windows-package.mjs",
|
||||
"verify:qwen-client": "npm run build:cached && node scripts/verify-qwen-client.mjs",
|
||||
"verify:gemini-client": "npm run build:cached && node scripts/verify-gemini-client.mjs",
|
||||
"verify:opencode-client": "npm run build:cached && node scripts/verify-opencode-client.mjs",
|
||||
"verify:grok-client": "npm run build:cached && node scripts/verify-grok-client.mjs",
|
||||
"verify:omp-client": "npm run build:cached && node scripts/verify-omp-client.mjs",
|
||||
"verify:copilot-client": "npm run build:cached && node scripts/verify-copilot-client.mjs",
|
||||
"verify:claude-client": "npm run build:cached && node scripts/verify-claude-client.mjs",
|
||||
"verify:pi-client": "npm run build:cached && node scripts/verify-native-agent-client.mjs pi",
|
||||
"verify:hermes-client": "npm run build:cached && node scripts/verify-native-agent-client.mjs hermes",
|
||||
"verify:deepseek-client": "npm run build:cached && node scripts/verify-deepseek-client.mjs",
|
||||
"probe:windows-capabilities": "npm run build:cached && node scripts/windows-capabilities.mjs",
|
||||
"verify:published": "node scripts/verify-published-package.mjs",
|
||||
"config:print": "node scripts/print-mcp-config.mjs",
|
||||
@@ -269,7 +284,8 @@
|
||||
"write-file-atomic": "7.0.0",
|
||||
"ws": "8.21.0",
|
||||
"zod": "4.4.3",
|
||||
"evmole": "0.9.3"
|
||||
"evmole": "0.9.3",
|
||||
"yaml": "2.9.0"
|
||||
},
|
||||
"overrides": {
|
||||
"vitepress": {
|
||||
@@ -293,7 +309,6 @@
|
||||
"turbo": "2.11.7",
|
||||
"typescript": "7.0.2",
|
||||
"vitepress": "1.6.4",
|
||||
"vitest": "5.0.3",
|
||||
"yaml": "2.9.0"
|
||||
"vitest": "5.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
"""Optionally audit external website links; network failures stay distinct from missing targets."""
|
||||
|
||||
import argparse
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from functools import lru_cache
|
||||
from html.parser import HTMLParser
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import quote, unquote, urlsplit
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from website import authored_markdown
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def github_api(endpoint):
|
||||
"""Read GitHub evidence through the caller's gh installation and authentication."""
|
||||
result = subprocess.run(["gh", "api", endpoint], capture_output=True, text=True, timeout=20)
|
||||
if result.returncode:
|
||||
raise ValueError(result.stderr.strip())
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def github_target(url):
|
||||
"""Resolve repository URLs, including branch names containing slashes."""
|
||||
parts = [unquote(part) for part in url.path.strip("/").split("/")]
|
||||
if len(parts) < 2:
|
||||
return None
|
||||
repo = f"repos/{parts[0]}/{parts[1]}"
|
||||
if url.hostname == "raw.githubusercontent.com" and len(parts) >= 4:
|
||||
return f"{repo}/contents/{'/'.join(parts[3:])}?ref={quote(parts[2], safe='')}"
|
||||
if len(parts) == 2:
|
||||
return repo
|
||||
kind = parts[2]
|
||||
if kind in ("blob", "tree") and len(parts) >= 4:
|
||||
ref = parts[3]
|
||||
# GitHub uses the longest matching branch name before the source path.
|
||||
if not re.fullmatch(r"[0-9a-f]{40}", ref):
|
||||
references = github_api(f"{repo}/git/matching-refs/heads/{quote(ref, safe='')}")
|
||||
references += github_api(f"{repo}/git/matching-refs/tags/{quote(ref, safe='')}")
|
||||
suffix = "/".join(parts[3:])
|
||||
matches = [item["ref"].split("/", 2)[2] for item in references]
|
||||
matches = [name for name in matches if suffix == name or suffix.startswith(name + "/")]
|
||||
if matches:
|
||||
ref = max(matches, key=len)
|
||||
path = "/".join(parts[3:])[len(ref):].lstrip("/")
|
||||
return f"{repo}/contents/{quote(path, safe='/')}?ref={quote(ref, safe='')}"
|
||||
if kind in ("issues", "pull") and len(parts) == 4 and parts[3].isdigit():
|
||||
return f"{repo}/{'pulls' if kind == 'pull' else 'issues'}/{parts[3]}"
|
||||
if kind == "commit" and len(parts) == 4:
|
||||
return f"{repo}/commits/{parts[3]}"
|
||||
if kind == "releases" and len(parts) >= 5 and parts[3] in ("tag", "download"):
|
||||
return f"{repo}/releases/tags/{quote(parts[4], safe='')}"
|
||||
return None
|
||||
|
||||
|
||||
def audit_url(item):
|
||||
"""Classify confirmed missing links separately from blocked or unsupported probes."""
|
||||
value, sources = item
|
||||
result = {"url": value, "sources": sorted(set(sources))}
|
||||
url = urlsplit(value)
|
||||
try:
|
||||
if url.hostname in ("github.com", "raw.githubusercontent.com"):
|
||||
endpoint = github_target(url)
|
||||
if endpoint is None:
|
||||
return dict(result, status="unknown", reason="GitHub UI route; check in a browser")
|
||||
data = github_api(endpoint)
|
||||
parts = url.path.strip("/").split("/")
|
||||
if len(parts) >= 6 and parts[2:4] == ["releases", "download"]:
|
||||
if unquote(parts[5]) not in [asset["name"] for asset in data.get("assets", [])]:
|
||||
return dict(result, status="broken", reason="Release asset is missing")
|
||||
return dict(result, status="ok", via="gh api", endpoint=endpoint)
|
||||
request = Request(value.split("#", 1)[0], headers={
|
||||
"User-Agent": "REA-Website-Verifier/1.0 (+https://github.com/morluto/rea)",
|
||||
})
|
||||
with urlopen(request, timeout=15) as response:
|
||||
return dict(result, status="ok", http_status=response.status, final_url=response.url)
|
||||
except HTTPError as error:
|
||||
return dict(result, status="broken" if error.code in (404, 410) else "unknown", reason=str(error))
|
||||
except ValueError as error:
|
||||
return dict(result, status="broken" if "HTTP 404" in str(error) else "unknown", reason=str(error))
|
||||
except (OSError, URLError, TimeoutError, subprocess.TimeoutExpired) as error:
|
||||
return dict(result, status="unknown", reason=str(error))
|
||||
|
||||
|
||||
def external_links(root):
|
||||
"""Discover authored external links, excluding locally checked site and preview URLs."""
|
||||
references = {}
|
||||
|
||||
def add(value, source):
|
||||
url = urlsplit(value)
|
||||
if url.scheme in ("http", "https") and url.hostname not in ("rea.tools", "localhost", "127.0.0.1", "::1"):
|
||||
references.setdefault(value, []).append(str(source.relative_to(root)))
|
||||
|
||||
class Links(HTMLParser):
|
||||
def handle_starttag(self, tag, attrs):
|
||||
for key, value in attrs:
|
||||
if key in ("href", "src") and value:
|
||||
add(value, path)
|
||||
|
||||
for path in sorted((root / "website/public").rglob("*.html")):
|
||||
Links().feed(path.read_text(encoding="utf-8"))
|
||||
for path in authored_markdown(root):
|
||||
for value in re.findall(r"(?:\]\(|<)(https?://[^\s<>\)]+)", path.read_text(encoding="utf-8")):
|
||||
add(value, path)
|
||||
return references
|
||||
|
||||
|
||||
def main():
|
||||
"""Run an explicit network audit without making PR checks depend on third-party uptime."""
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--report", type=Path, help="Write the complete JSON report outside public assets")
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
with ThreadPoolExecutor(max_workers=4) as pool:
|
||||
results = list(pool.map(audit_url, external_links(root).items()))
|
||||
if args.report:
|
||||
if args.report.resolve().is_relative_to(root / "website/public"):
|
||||
parser.error("Keep audit reports outside website/public.")
|
||||
args.report.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8")
|
||||
counts = {status: sum(item["status"] == status for item in results) for status in ("ok", "broken", "unknown")}
|
||||
print(f"External links: {counts['ok']} reachable, {counts['broken']} missing, {counts['unknown']} need manual verification.")
|
||||
print("GitHub checks establish target existence; external fragment IDs are not checked.")
|
||||
for item in results:
|
||||
if item["status"] != "ok":
|
||||
print(f"{item['status']}: {item['url']} — {item['reason']}")
|
||||
return 1 if counts["broken"] else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -13,7 +13,7 @@ for (const architecture of ["x86_64", "x86"]) {
|
||||
"fixtures",
|
||||
`rea-ghidra-windows${x86 ? "-x86" : ""}.${dll ? "dll" : "exe"}`,
|
||||
);
|
||||
const bytes = Buffer.alloc(1024);
|
||||
const bytes = Buffer.alloc(dll ? 1024 : 1536);
|
||||
const peOffset = 0x80;
|
||||
const optionalHeader = peOffset + 24;
|
||||
const optionalHeaderSize = x86 ? 0xe0 : 0xf0;
|
||||
@@ -24,7 +24,7 @@ for (const architecture of ["x86_64", "x86"]) {
|
||||
bytes.writeUInt32LE(peOffset, 0x3c);
|
||||
bytes.write("PE\0\0", peOffset, "binary");
|
||||
bytes.writeUInt16LE(x86 ? 0x14c : 0x8664, peOffset + 4);
|
||||
bytes.writeUInt16LE(1, peOffset + 6);
|
||||
bytes.writeUInt16LE(dll ? 1 : 2, peOffset + 6);
|
||||
bytes.writeUInt16LE(optionalHeaderSize, peOffset + 20);
|
||||
bytes.writeUInt16LE(
|
||||
(x86 ? 0x102 : 0x22) | (dll ? 0x2000 : 0),
|
||||
@@ -33,6 +33,7 @@ for (const architecture of ["x86_64", "x86"]) {
|
||||
|
||||
bytes.writeUInt16LE(x86 ? 0x10b : 0x20b, optionalHeader);
|
||||
bytes.writeUInt32LE(0x200, optionalHeader + 4);
|
||||
if (!dll) bytes.writeUInt32LE(0x200, optionalHeader + 8);
|
||||
bytes.writeUInt32LE(dll ? 0 : 0x1000, optionalHeader + 16);
|
||||
bytes.writeUInt32LE(0x1000, optionalHeader + 20);
|
||||
if (x86) bytes.writeUInt32LE(0x400000, optionalHeader + 28);
|
||||
@@ -41,7 +42,7 @@ for (const architecture of ["x86_64", "x86"]) {
|
||||
bytes.writeUInt32LE(0x200, optionalHeader + 36);
|
||||
bytes.writeUInt16LE(6, optionalHeader + 40);
|
||||
bytes.writeUInt16LE(6, optionalHeader + 48);
|
||||
bytes.writeUInt32LE(0x2000, optionalHeader + 56);
|
||||
bytes.writeUInt32LE(dll ? 0x2000 : 0x3000, optionalHeader + 56);
|
||||
bytes.writeUInt32LE(0x200, optionalHeader + 60);
|
||||
bytes.writeUInt16LE(3, optionalHeader + 68);
|
||||
bytes.writeUInt16LE(x86 ? 0x8140 : 0x8160, optionalHeader + 70);
|
||||
@@ -60,7 +61,7 @@ for (const architecture of ["x86_64", "x86"]) {
|
||||
}
|
||||
|
||||
bytes.write(".text", sectionTable, "ascii");
|
||||
bytes.writeUInt32LE(dll ? 0x180 : 0x16, sectionTable + 8);
|
||||
bytes.writeUInt32LE(dll ? 0x180 : 0x50, sectionTable + 8);
|
||||
bytes.writeUInt32LE(0x1000, sectionTable + 12);
|
||||
bytes.writeUInt32LE(0x200, sectionTable + 16);
|
||||
bytes.writeUInt32LE(0x200, sectionTable + 20);
|
||||
@@ -70,11 +71,48 @@ for (const architecture of ["x86_64", "x86"]) {
|
||||
bytes.set(
|
||||
dll
|
||||
? [0xe8, 0x0b, 0, 0, 0, 0x83, 0xc0, 1, 0xc3]
|
||||
: [0xe8, 0x0b, 0, 0, 0, 0x31, 0xc0, 0xc3],
|
||||
: [0xe8, 0x0b, 0, 0, 0, 0xe8, 0x16, 0, 0, 0, 0x31, 0xc0, 0xc3],
|
||||
0x200,
|
||||
);
|
||||
bytes.set([0xb8, 0x2a, 0, 0, 0, 0xc3], 0x210);
|
||||
|
||||
if (!dll) {
|
||||
const dataSection = sectionTable + 40;
|
||||
bytes.write(".data", dataSection, "ascii");
|
||||
bytes.writeUInt32LE(0x20, dataSection + 8);
|
||||
bytes.writeUInt32LE(0x2000, dataSection + 12);
|
||||
bytes.writeUInt32LE(0x200, dataSection + 16);
|
||||
bytes.writeUInt32LE(0x400, dataSection + 20);
|
||||
bytes.writeUInt32LE(0xc000_0040, dataSection + 36);
|
||||
bytes.writeUInt32LE(0x1122_3344, 0x400);
|
||||
const referenceInstructions = x86
|
||||
? [
|
||||
"ba00204000", // MOV EDX, address: DATA only.
|
||||
"a100204000", // MOV EAX, [address]: READ.
|
||||
"a304204000", // MOV [address], EAX: WRITE.
|
||||
"ba10204000", // Indexed base, unknown ECX.
|
||||
"85db", // TEST EBX, EBX: two candidate bases.
|
||||
"7405", // JZ over the alternate base.
|
||||
"ba18204000", // MOV EDX, alternate base.
|
||||
"c6040a01", // MOV byte [EDX + ECX], 1.
|
||||
"ffd7", // CALL EDI: unknown target.
|
||||
"c3",
|
||||
]
|
||||
: [
|
||||
"488d15d90f0000", // LEA RDX, [RIP + displacement]: DATA.
|
||||
"8b05d30f0000", // MOV EAX, [RIP + displacement]: READ.
|
||||
"8905d10f0000", // MOV [RIP + displacement], EAX: WRITE.
|
||||
"488d15d60f0000", // Indexed base, unknown RCX.
|
||||
"85db", // TEST EBX, EBX: two candidate bases.
|
||||
"7407", // JZ over the alternate base.
|
||||
"488d15d30f0000", // LEA RDX, alternate base.
|
||||
"c6040a01", // MOV byte [RDX + RCX], 1.
|
||||
"41ffd0", // CALL R8: unknown target.
|
||||
"c3",
|
||||
];
|
||||
bytes.set(Buffer.from(referenceInstructions.join(""), "hex"), 0x220);
|
||||
}
|
||||
|
||||
if (dll) {
|
||||
// Export both functions; an entry-point-free library is discovered by exports.
|
||||
bytes.writeUInt32LE(0x1040, directories);
|
||||
|
||||
@@ -17,7 +17,7 @@ const lock = JSON.parse(
|
||||
await readFile(join(root, "package-lock.json"), "utf8"),
|
||||
);
|
||||
const skill = await readFile(
|
||||
join(root, "skill-src/reverse-engineer-anything/SKILL.md"),
|
||||
join(root, ".agents/skills/reverse-engineer-anything/SKILL.md"),
|
||||
"utf8",
|
||||
);
|
||||
const skillVersion = /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(skill)?.[1];
|
||||
|
||||
@@ -10,7 +10,7 @@ for (const argument of arguments_)
|
||||
throw new Error(`Unknown skill metadata option: ${argument}`);
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const sourceRoot = join(root, "skill-src/reverse-engineer-anything");
|
||||
const sourceRoot = join(root, ".agents/skills/reverse-engineer-anything");
|
||||
const outputRoot = join(root, "skills/reverse-engineer-anything");
|
||||
const check = arguments_.has("--check");
|
||||
const current = (
|
||||
@@ -25,7 +25,7 @@ const { PRODUCT_IDENTITY } = await import(
|
||||
);
|
||||
if (/^\s{2}(?:tool_count|catalog_digest):/mu.test(current))
|
||||
throw new Error(
|
||||
"Catalog metadata belongs in the generated skill, not skill-src",
|
||||
"Catalog metadata belongs in the generated skill, not the authored source",
|
||||
);
|
||||
const versionLine = /^ {2}version: "[^"\r\n]+"$/mu;
|
||||
if (!versionLine.test(current))
|
||||
|
||||
@@ -47,6 +47,21 @@ export const toolFamilyCatalog = (sources) => {
|
||||
surface: "android-provider",
|
||||
contracts: sources.androidContracts.ANDROID_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
id: "apktool",
|
||||
surface: "apktool-provider",
|
||||
contracts: sources.apktoolContracts.APKTOOL_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
id: "jeb",
|
||||
surface: "jeb-provider",
|
||||
contracts: sources.jebContracts.JEB_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
id: "adb",
|
||||
surface: "adb-provider",
|
||||
contracts: sources.adbContracts.ADB_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
id: "browser",
|
||||
surface: "browser-provider",
|
||||
@@ -153,7 +168,16 @@ export const providerCatalog = (sources) => {
|
||||
{
|
||||
identity: sources.pwntoolsProvider.PWNTOOLS_PROVIDER_IDENTITY,
|
||||
contracts:
|
||||
sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS,
|
||||
sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
|
||||
({ name }) => name !== "inspect_pe_resources",
|
||||
),
|
||||
},
|
||||
{
|
||||
identity: sources.artifactProviders.PE_RESOURCES_PROVIDER,
|
||||
contracts:
|
||||
sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
|
||||
({ name }) => name === "inspect_pe_resources",
|
||||
),
|
||||
},
|
||||
{
|
||||
identity: sources.evmProvider.EVMOLE_PROVIDER_IDENTITY,
|
||||
@@ -188,6 +212,18 @@ export const providerCatalog = (sources) => {
|
||||
identity: sources.androidProvider.JADX_PROVIDER_IDENTITY,
|
||||
contracts: sources.androidContracts.ANDROID_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
identity: sources.apktoolProvider.APKTOOL_PROVIDER_IDENTITY,
|
||||
contracts: sources.apktoolContracts.APKTOOL_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
identity: sources.jebProvider.JEB_PROVIDER_IDENTITY,
|
||||
contracts: sources.jebContracts.JEB_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
identity: sources.adbProvider.ADB_PROVIDER_IDENTITY,
|
||||
contracts: sources.adbContracts.ADB_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
identity: sources.webNetworkCaptureProvider.HAR_CAPTURE_PROVIDER_IDENTITY,
|
||||
contracts:
|
||||
|
||||
@@ -53,6 +53,12 @@ const SOURCE_PATHS = {
|
||||
firmwareProvider: "dist/firmware/FirmwareRelease.js",
|
||||
androidContracts: "dist/contracts/android/androidToolContracts.js",
|
||||
androidProvider: "dist/android/JadxRelease.js",
|
||||
apktoolContracts: "dist/contracts/apktool/apktoolToolContracts.js",
|
||||
apktoolProvider: "dist/apktool/ApktoolProvider.js",
|
||||
jebContracts: "dist/contracts/jeb/jebToolContracts.js",
|
||||
jebProvider: "dist/jeb/JebProvider.js",
|
||||
adbContracts: "dist/contracts/adb/adbToolContracts.js",
|
||||
adbProvider: "dist/adb/AdbProvider.js",
|
||||
managedWorkflowContracts:
|
||||
"dist/contracts/managed/managedWorkflowToolContracts.js",
|
||||
browserContracts: "dist/contracts/browserToolContracts.js",
|
||||
|
||||
@@ -16,6 +16,9 @@ import { verifyGhidraBoundaries } from "./real-ghidra-boundaries.mjs";
|
||||
const defaultEntrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
|
||||
const environment = () => ({
|
||||
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
||||
...(process.env.NODE_OPTIONS === undefined
|
||||
? {}
|
||||
: { NODE_OPTIONS: process.env.NODE_OPTIONS }),
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_ANALYSIS_PROVIDER: "ghidra",
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
||||
|
||||
@@ -40,7 +40,11 @@ export async function verifyGhidraBoundaries(
|
||||
ajv.validateSchema(tool[key]),
|
||||
`${tool.name} ${key}: ${JSON.stringify(ajv.errors)}`,
|
||||
);
|
||||
validators.set(tool.name, ajv.compile(tool.outputSchema));
|
||||
validators.set(
|
||||
tool.name,
|
||||
(tool.outputSchema?.$id && ajv.getSchema(tool.outputSchema.$id)) ||
|
||||
ajv.compile(tool.outputSchema),
|
||||
);
|
||||
}
|
||||
let successfulCalls = 0;
|
||||
let rejectedCalls = 0;
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
/** Verify actual reference facts on the source-owned Windows PE fixture. */
|
||||
export async function verifyGhidraCompactReferences(call, x86, cli) {
|
||||
const base = x86 ? 0x400000n : 0x1_4000_0000n;
|
||||
const address = (offset) => `0x${(base + BigInt(offset)).toString(16)}`;
|
||||
const cases = [
|
||||
["data", 0x1020, 0x2000],
|
||||
["read", x86 ? 0x1025 : 0x1027, 0x2000],
|
||||
["write", x86 ? 0x102a : 0x102d, 0x2004],
|
||||
];
|
||||
const full = await call("procedure_references", {
|
||||
procedure: address(0x1020),
|
||||
});
|
||||
const inspected = {};
|
||||
for (const [kind, offset, target] of cases) {
|
||||
const instruction = await call("inspect_native_instruction", {
|
||||
address: address(offset),
|
||||
});
|
||||
inspected[kind] = instruction;
|
||||
const reference = instruction.references.find(
|
||||
(item) => item.target_address === address(target),
|
||||
);
|
||||
assert.ok(reference, `Missing ${kind} fixture reference`);
|
||||
assertReference(reference, instruction.address, full.references);
|
||||
assert.equal(reference.data, true);
|
||||
assert.equal(reference.read, kind === "read");
|
||||
assert.equal(reference.write, kind === "write");
|
||||
}
|
||||
const indexed = await call("inspect_native_instruction", {
|
||||
address: address(x86 ? 0x103d : 0x1045),
|
||||
});
|
||||
for (const reference of indexed.references)
|
||||
assertReference(reference, indexed.address, full.references);
|
||||
assert.ok(
|
||||
indexed.references.some(
|
||||
(reference) =>
|
||||
reference.primary === false && reference.source === "analysis",
|
||||
),
|
||||
"Indexed fixture lacks a non-primary analyzer reference",
|
||||
);
|
||||
|
||||
const { direct, targets } = await verifyDirectCall(
|
||||
call,
|
||||
address(0x1000),
|
||||
address(0x1010),
|
||||
);
|
||||
|
||||
const indirectAddress = address(x86 ? 0x1041 : 0x1049);
|
||||
const indirect = await call("inspect_native_instruction", {
|
||||
address: indirectAddress,
|
||||
});
|
||||
const unresolved = await call("resolve_native_call_targets", {
|
||||
address: indirectAddress,
|
||||
});
|
||||
assert.equal(indirect.flow.kind, "call");
|
||||
assert.deepEqual(indirect.references, []);
|
||||
assert.equal(unresolved.status, "unresolved");
|
||||
assert.deepEqual(unresolved.targets, []);
|
||||
|
||||
const returned = await verifyEmptyReferences(
|
||||
call,
|
||||
address(x86 ? 0x1043 : 0x104c),
|
||||
);
|
||||
|
||||
if (cli !== undefined) {
|
||||
for (const instruction of [inspected.write, indexed, direct, returned])
|
||||
assert.deepEqual(
|
||||
await cli("inspect-native-instruction", instruction.address),
|
||||
instruction,
|
||||
);
|
||||
assert.deepEqual(
|
||||
await cli("resolve-native-call-targets", direct.address),
|
||||
targets,
|
||||
);
|
||||
assert.deepEqual(
|
||||
await cli("resolve-native-call-targets", indirectAddress),
|
||||
unresolved,
|
||||
);
|
||||
}
|
||||
return {
|
||||
data_read_write: true,
|
||||
source_primary_provenance: true,
|
||||
full_reference_parity: true,
|
||||
direct_call_reference_parity: true,
|
||||
indexed_reference_count: indexed.references.length,
|
||||
indexed_non_primary_count: indexed.references.filter(
|
||||
(reference) => reference.primary === false,
|
||||
).length,
|
||||
indexed_non_primary_analyzer_reference: true,
|
||||
indexed_reference_kinds: indexed.references.map(
|
||||
({ type, read, write, primary, source }) => ({
|
||||
type,
|
||||
read,
|
||||
write,
|
||||
primary,
|
||||
source,
|
||||
}),
|
||||
),
|
||||
targetless_indirect: true,
|
||||
empty_and_unavailable: true,
|
||||
cli_mcp_parity: cli !== undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function verifyDirectCall(call, entry, callee) {
|
||||
const direct = await call("inspect_native_instruction", { address: entry });
|
||||
const targets = await call("resolve_native_call_targets", { address: entry });
|
||||
assert.equal(targets.status, "direct");
|
||||
assert.equal(targets.targets[0].address, callee);
|
||||
assert.deepEqual(
|
||||
targets.targets[0].references,
|
||||
direct.references.filter((item) => item.call),
|
||||
);
|
||||
const full = await call("procedure_references", { procedure: entry });
|
||||
for (const reference of direct.references)
|
||||
assertReference(reference, entry, full.references);
|
||||
return { direct, targets };
|
||||
}
|
||||
|
||||
async function verifyEmptyReferences(call, address) {
|
||||
const returned = await call("inspect_native_instruction", { address });
|
||||
assert.equal(returned.flow.kind, "return");
|
||||
assert.deepEqual(returned.references, []);
|
||||
assert.equal(
|
||||
(await call("resolve_native_call_targets", { address })).status,
|
||||
"not-call",
|
||||
);
|
||||
const outside = await call("inspect_native_instruction", { address: "0x0" });
|
||||
assert.equal(outside.status, "outside-memory");
|
||||
assert.deepEqual(outside.references, []);
|
||||
assert.equal(
|
||||
(await call("resolve_native_call_targets", { address: "0x0" })).status,
|
||||
"unavailable",
|
||||
);
|
||||
return returned;
|
||||
}
|
||||
|
||||
function assertReference(reference, source, full) {
|
||||
assert.equal(reference.source_address, source);
|
||||
assert.equal(reference.provenance, "ghidra-reference-manager");
|
||||
assert.equal(typeof reference.primary, "boolean");
|
||||
assert.equal(typeof reference.source, "string");
|
||||
assert.ok(reference.source.length > 0);
|
||||
const edge = full.find(
|
||||
(item) =>
|
||||
item.source_address === source &&
|
||||
item.target_address === reference.target_address &&
|
||||
item.kind.operand_index === reference.operand_index,
|
||||
);
|
||||
assert.ok(edge, "Compact reference lacks its matching full reference");
|
||||
for (const field of [
|
||||
"type",
|
||||
"call",
|
||||
"jump",
|
||||
"indirect",
|
||||
"computed",
|
||||
"operand_index",
|
||||
"data",
|
||||
"read",
|
||||
"write",
|
||||
"primary",
|
||||
"provenance",
|
||||
])
|
||||
assert.equal(
|
||||
reference[field],
|
||||
edge.kind[field],
|
||||
`Reference ${field} differs from its full representation`,
|
||||
);
|
||||
}
|
||||
@@ -42,7 +42,11 @@ export async function verifyHopperBoundaryContracts(
|
||||
ajv.validateSchema(tool.outputSchema),
|
||||
`${tool.name} output schema`,
|
||||
);
|
||||
validators.set(tool.name, ajv.compile(tool.outputSchema));
|
||||
validators.set(
|
||||
tool.name,
|
||||
(tool.outputSchema?.$id && ajv.getSchema(tool.outputSchema.$id)) ||
|
||||
ajv.compile(tool.outputSchema),
|
||||
);
|
||||
}
|
||||
let successfulCalls = 0;
|
||||
let rejectedCalls = 0;
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
"""Prepare downloads, the sitemap and the sharing image for the static website."""
|
||||
|
||||
from html.parser import HTMLParser
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
from urllib.parse import quote
|
||||
import xml.etree.ElementTree as ET
|
||||
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
||||
|
||||
from website import PageMetadata, page_url
|
||||
|
||||
|
||||
SITE_ORIGIN = "https://rea.tools"
|
||||
EXAMPLE_FILES = (
|
||||
"package.json",
|
||||
"main.js",
|
||||
@@ -21,47 +19,20 @@ EXAMPLE_FILES = (
|
||||
)
|
||||
|
||||
|
||||
class IndexPolicy(HTMLParser):
|
||||
"""Read indexing directives from the authored HTML head."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.in_head = False
|
||||
self.noindex = False
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == "head":
|
||||
self.in_head = True
|
||||
if tag == "meta" and self.in_head:
|
||||
attributes = dict(attrs)
|
||||
if (attributes.get("name") or "").lower() in ("robots", "googlebot"):
|
||||
directives = re.split(
|
||||
r"[\s,]+", (attributes.get("content") or "").lower()
|
||||
)
|
||||
self.noindex |= bool(set(directives) & {"noindex", "none"})
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == "head":
|
||||
self.in_head = False
|
||||
|
||||
|
||||
def prepare_sitemap(site):
|
||||
"""Discover HTML routes automatically, excluding pages marked noindex."""
|
||||
namespace = "http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
ET.register_namespace("", namespace)
|
||||
sitemap = ET.Element(f"{{{namespace}}}urlset")
|
||||
for path in sorted(site.rglob("*.html")):
|
||||
policy = IndexPolicy()
|
||||
policy = PageMetadata()
|
||||
policy.feed(path.read_text(encoding="utf-8"))
|
||||
if policy.noindex:
|
||||
continue
|
||||
route = path.relative_to(site).as_posix()
|
||||
if path.name == "index.html":
|
||||
route = route.removesuffix("index.html")
|
||||
entry = ET.SubElement(sitemap, f"{{{namespace}}}url")
|
||||
ET.SubElement(
|
||||
entry, f"{{{namespace}}}loc"
|
||||
).text = f"{SITE_ORIGIN}/{quote(route, safe='/')}"
|
||||
).text = page_url(site, path)
|
||||
ET.indent(sitemap, space=" ")
|
||||
ET.ElementTree(sitemap).write(
|
||||
site / "sitemap.xml", encoding="utf-8", xml_declaration=True
|
||||
|
||||
@@ -6,7 +6,11 @@ import io
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import tempfile
|
||||
from threading import Thread
|
||||
import unittest
|
||||
from http.server import ThreadingHTTPServer
|
||||
from urllib.error import HTTPError
|
||||
from urllib.request import urlopen
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
@@ -23,6 +27,7 @@ def load_script(name):
|
||||
|
||||
PREPARE = load_script("prepare-website")
|
||||
VERIFY = load_script("verify-website")
|
||||
WEBSITE = load_script("website")
|
||||
|
||||
|
||||
class WebsiteSearchChecks(unittest.TestCase):
|
||||
@@ -126,5 +131,116 @@ class WebsiteSearchChecks(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class WebsiteMaintenanceChecks(unittest.TestCase):
|
||||
"""Exercise contributor edits and the static preview through real consumers."""
|
||||
|
||||
def test_shared_edits_preserve_body_and_catch_drift_before_rewriting(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
for name in ("public", "templates"):
|
||||
shutil.copytree(ROOT / "website" / name, root / "website" / name)
|
||||
page = root / "website/public/blog/touhou-reconstruction/index.html"
|
||||
original = page.read_text()
|
||||
body = original.split('<main id="main"', 1)[1].split("</main>", 1)[0]
|
||||
template = root / "website/templates/header.html.tmpl"
|
||||
template.write_text(template.read_text().replace("Reverse Engineer Anything", "REA test navigation"))
|
||||
with self.assertRaisesRegex(ValueError, "Shared HTML is stale"):
|
||||
WEBSITE.sync_pages(root, check=True)
|
||||
self.assertEqual(page.read_text(), original)
|
||||
WEBSITE.sync_pages(root)
|
||||
updated = page.read_text()
|
||||
self.assertIn("REA test navigation", updated)
|
||||
self.assertEqual(updated.split('<main id="main"', 1)[1].split("</main>", 1)[0], body)
|
||||
self.assertEqual(WEBSITE.sync_pages(root), [])
|
||||
page.write_text(updated.replace(' <!-- website:header:end -->', '', 1))
|
||||
with self.assertRaisesRegex(ValueError, "needs one header region"):
|
||||
WEBSITE.sync_pages(root)
|
||||
|
||||
def test_new_page_escapes_metadata_stays_a_draft_and_joins_sitemap_when_ready(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
for name in ("public", "templates"):
|
||||
shutil.copytree(ROOT / "website" / name, root / "website" / name)
|
||||
site = root / "website/public"
|
||||
page = WEBSITE.new_page(root, "blog/new-example", 'A <test> & "result"', 'Check "quotes" & <markup>.')
|
||||
source = page.read_text()
|
||||
self.assertIn('href="../../blog/" aria-current="page"', source)
|
||||
self.assertIn('href="../../assets/styles.css"', source)
|
||||
self.assertIn("A <test> & "result"", source)
|
||||
with self.assertRaisesRegex(ValueError, "Page already exists"):
|
||||
WEBSITE.new_page(root, "blog/new-example", "Another page", "Description")
|
||||
with self.assertRaises(ValueError):
|
||||
WEBSITE.new_page(root, "../outside", "Title", "Description")
|
||||
WEBSITE.sync_pages(root, check=True)
|
||||
PREPARE.prepare_sitemap(site)
|
||||
self.assertNotIn("https://rea.tools/blog/new-example/", (site / "sitemap.xml").read_text())
|
||||
page.write_text(source.replace(' <meta name="robots" content="noindex" />\n', ''))
|
||||
PREPARE.prepare_sitemap(site)
|
||||
self.assertIn("https://rea.tools/blog/new-example/", (site / "sitemap.xml").read_text())
|
||||
self.assertEqual(VERIFY.check_site(site)[3], [])
|
||||
|
||||
def test_links_in_prompts_fragments_and_copy_controls_are_verified(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
site = Path(directory) / "public"
|
||||
shutil.copytree(ROOT / "website/public", site)
|
||||
page = site / "faq/index.html"
|
||||
original = page.read_text()
|
||||
cases = (
|
||||
('<a href="https://rea.tools/deleted/">Broken</a>', "missing local target"),
|
||||
('<code>https://rea.tools/guides/native/#deleted</code>', "missing HTML fragment"),
|
||||
('<code>https://morluto.github.io/rea/examples/notes-web/</code>', "obsolete website URL"),
|
||||
('<button data-copy="deleted">Copy</button>', "missing copy target"),
|
||||
('<a href="https://[">Malformed</a>', "invalid URL"),
|
||||
)
|
||||
for inserted, message in cases:
|
||||
with self.subTest(message=message):
|
||||
page.write_text(original.replace("</main>", inserted + "</main>", 1))
|
||||
self.assertTrue(any(message in error for error in VERIFY.check_site(site)[3]))
|
||||
|
||||
def test_repository_links_reject_missing_files_and_old_markdown_anchors(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / "website/public").mkdir(parents=True)
|
||||
(root / "docs").mkdir()
|
||||
(root / "docs/guide.md").write_text("# Guide\n## Set up REA\n")
|
||||
page = root / "website/README.md"
|
||||
page.write_text("[Guide](../docs/guide.md#set-up-rea)\n")
|
||||
(root / "website/.venv").mkdir()
|
||||
(root / "website/.venv/vendor.md").write_text("[File](missing.md)")
|
||||
self.assertEqual(VERIFY.check_repository_links(root), [])
|
||||
page.write_text("[Guide](../docs/guide.md#removed)\n[File](../docs/removed.md)\n")
|
||||
errors = VERIFY.check_repository_links(root)
|
||||
self.assertTrue(any("missing Markdown fragment" in error for error in errors))
|
||||
self.assertTrue(any("missing repository target" in error for error in errors))
|
||||
|
||||
def test_preview_preserves_pages_prefix_for_assets_and_directory_redirects(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
site = Path(directory)
|
||||
(site / "guide").mkdir()
|
||||
(site / "guide/index.html").write_text("<h1>Guide</h1>")
|
||||
(site / "asset.css").write_text("body { color: black; }")
|
||||
for prefix in ("/", "/rea/"):
|
||||
with self.subTest(prefix=prefix), ThreadingHTTPServer(("127.0.0.1", 0), WEBSITE.preview_handler(site, prefix)) as server:
|
||||
worker = Thread(target=server.serve_forever, daemon=True)
|
||||
worker.start()
|
||||
try:
|
||||
base = f"http://127.0.0.1:{server.server_port}"
|
||||
with urlopen(base + prefix + "guide?test=1") as response:
|
||||
self.assertEqual(response.url, base + prefix + "guide/?test=1")
|
||||
self.assertEqual(response.read(), b"<h1>Guide</h1>")
|
||||
with urlopen(base + prefix + "asset.css") as response:
|
||||
self.assertIn(b"color: black", response.read())
|
||||
with self.assertRaises(HTTPError) as error:
|
||||
urlopen(base + prefix + "missing")
|
||||
self.assertEqual(error.exception.code, 404)
|
||||
if prefix != "/":
|
||||
with self.assertRaises(HTTPError) as error:
|
||||
urlopen(base + "/guide/")
|
||||
self.assertEqual(error.exception.code, 404)
|
||||
finally:
|
||||
server.shutdown()
|
||||
worker.join()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { AdbDeviceAnalysisService } from "../dist/application/adb/AdbDeviceAnalysisService.js";
|
||||
import { createAdbDeviceAnalysisProvider } from "../dist/composition/adb.js";
|
||||
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
if (options.help) {
|
||||
process.stdout.write(
|
||||
"Usage: npm run verify:adb [-- --serial SERIAL --pull PACKAGE]\n" +
|
||||
"Requires a caller-selected adb binary (REA_ADB_PATH or PATH) and a\n" +
|
||||
"connected device or emulator. Verifies all five operations against\n" +
|
||||
"the live device; --pull acquires a real package and re-digests the\n" +
|
||||
"files on disk against the returned SHA-256 values.\n",
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const provider = createAdbDeviceAnalysisProvider(process.env);
|
||||
const service = new AdbDeviceAnalysisService(provider);
|
||||
let failures = 0;
|
||||
const check = (name, condition, detail = "") => {
|
||||
const mark = condition ? "ok" : "FAIL";
|
||||
process.stdout.write(
|
||||
`[${mark}] ${name}${detail === "" ? "" : ` — ${detail}`}\n`,
|
||||
);
|
||||
if (!condition) failures += 1;
|
||||
};
|
||||
|
||||
const run = async (operation, input) => {
|
||||
const result = await service.execute(operation, input);
|
||||
if (!result.ok) {
|
||||
process.stdout.write(`[FAIL] ${operation} — ${result.error.message}\n`);
|
||||
failures += 1;
|
||||
return null;
|
||||
}
|
||||
return result.value.normalized_result;
|
||||
};
|
||||
|
||||
const client = await run("inspect_adb_client", {});
|
||||
if (client !== null) {
|
||||
check(
|
||||
"adb client reports a version",
|
||||
typeof client.client.version === "string" && client.client.version !== "",
|
||||
`binary=${client.client.binary_path} version=${client.client.version}`,
|
||||
);
|
||||
}
|
||||
|
||||
const listing = await run("list_adb_devices", {});
|
||||
let serial = options.serial ?? null;
|
||||
if (listing !== null) {
|
||||
check(
|
||||
"device listing parsed",
|
||||
Array.isArray(listing.devices),
|
||||
`${listing.devices.length} device(s)`,
|
||||
);
|
||||
const usable = listing.devices.filter((device) => device.state === "device");
|
||||
if (serial === null && usable.length > 0) serial = usable[0].serial;
|
||||
if (serial === null) {
|
||||
process.stdout.write(
|
||||
"[FAIL] no usable device is attached; start an emulator or connect a phone\n",
|
||||
);
|
||||
failures += 1;
|
||||
} else {
|
||||
const selected = listing.devices.find((device) => device.serial === serial);
|
||||
check(
|
||||
"selected device present with a kind inference",
|
||||
selected !== undefined &&
|
||||
["emulator", "physical", "unknown"].includes(selected.kind),
|
||||
`serial=${serial} kind=${selected?.kind} basis=${selected?.kind_basis}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (serial !== null) {
|
||||
const device = await run("inspect_adb_device", { serial });
|
||||
if (device !== null) {
|
||||
check(
|
||||
"device build identity projected",
|
||||
device.properties.some(
|
||||
(property) => property.name === "ro.build.version.sdk",
|
||||
),
|
||||
`sdk=${
|
||||
device.properties.find((p) => p.name === "ro.build.version.sdk")?.value
|
||||
} emulator_observed=${String(device.emulator_observed)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Observation tools: processes, window, display, settings, logcat.
|
||||
const processes = await run("list_adb_processes", { serial });
|
||||
if (processes !== null) {
|
||||
check(
|
||||
"process listing is non-empty with a header",
|
||||
processes.processes.length > 0 && typeof processes.columns === "string",
|
||||
`${processes.processes.length} process(es)`,
|
||||
);
|
||||
}
|
||||
const window = await run("inspect_adb_window", { serial });
|
||||
if (window !== null)
|
||||
check(
|
||||
"window focus observation completed",
|
||||
typeof window.focused_window === "string" ||
|
||||
window.focused_window === null,
|
||||
`focused_window=${String(window.focused_window).slice(0, 60)} (null is legitimate on headless devices)`,
|
||||
);
|
||||
const display = await run("inspect_adb_display", { serial });
|
||||
if (display !== null)
|
||||
check(
|
||||
"display reports a physical size",
|
||||
display.physical_size !== null &&
|
||||
display.physical_size.width > 0 &&
|
||||
display.physical_size.height > 0,
|
||||
`${display.physical_size?.width}x${display.physical_size?.height} @${String(display.physical_density)}`,
|
||||
);
|
||||
const setting = await run("read_adb_setting", {
|
||||
serial,
|
||||
namespace: "global",
|
||||
key: "adb_enabled",
|
||||
});
|
||||
if (setting !== null)
|
||||
check(
|
||||
"settings read returns the device's own value",
|
||||
setting.value === "1",
|
||||
`adb_enabled=${String(setting.value)}`,
|
||||
);
|
||||
const logcat = await run("read_adb_logcat", {
|
||||
serial,
|
||||
count: 20,
|
||||
buffer: "main",
|
||||
});
|
||||
if (logcat !== null)
|
||||
check(
|
||||
"logcat dump returns bounded lines",
|
||||
logcat.lines.length <= 20,
|
||||
`${logcat.lines.length} line(s), coverage=${logcat.coverage}`,
|
||||
);
|
||||
const features = await run("list_adb_features", { serial });
|
||||
if (features !== null)
|
||||
check(
|
||||
"feature inventory is non-empty",
|
||||
features.coverage === "complete" && features.features.length > 0,
|
||||
`${features.features.length} feature(s)`,
|
||||
);
|
||||
const services = await run("list_adb_services", { serial });
|
||||
if (services !== null)
|
||||
check(
|
||||
"service inventory is non-empty",
|
||||
services.coverage === "complete" && services.services.length > 0,
|
||||
`${services.services.length} service(s)`,
|
||||
);
|
||||
const directory = await run("list_adb_directory", {
|
||||
serial,
|
||||
device_path: "/data/local/tmp",
|
||||
});
|
||||
if (directory !== null)
|
||||
check(
|
||||
"directory listing completed",
|
||||
directory.coverage === "complete",
|
||||
`${directory.entries.length} entr(ies)`,
|
||||
);
|
||||
|
||||
const packages = await run("list_adb_packages", {
|
||||
serial,
|
||||
scope: "third_party",
|
||||
});
|
||||
let pullCandidate = options.pull ?? null;
|
||||
if (packages !== null) {
|
||||
// A third-party scope may legitimately be empty (clean emulator images);
|
||||
// completeness is about unparsed lines, not row count.
|
||||
check(
|
||||
"third-party package listing reports complete coverage",
|
||||
packages.coverage === "complete",
|
||||
`${packages.packages.length} package(s)`,
|
||||
);
|
||||
const allPackages = await run("list_adb_packages", {
|
||||
serial,
|
||||
scope: "all",
|
||||
});
|
||||
if (allPackages !== null) {
|
||||
check(
|
||||
"full package inventory is non-empty",
|
||||
allPackages.coverage === "complete" && allPackages.packages.length > 0,
|
||||
`${allPackages.packages.length} package(s)`,
|
||||
);
|
||||
}
|
||||
if (pullCandidate === null && packages.packages.length > 0)
|
||||
pullCandidate = packages.packages[0].package_name;
|
||||
}
|
||||
|
||||
if (pullCandidate !== null) {
|
||||
const directory = await mkdtemp(join(tmpdir(), "rea-adb-verify-"));
|
||||
try {
|
||||
const pull = await run("pull_adb_package", {
|
||||
serial,
|
||||
package: pullCandidate,
|
||||
output_directory: directory,
|
||||
});
|
||||
if (pull !== null) {
|
||||
// Installed /data/app packages name their APKs base.apk and
|
||||
// split_*.apk; system packages keep arbitrary names, so the lane
|
||||
// checks a non-empty artifact set and verifies each file on disk.
|
||||
check(
|
||||
"pull returned a non-empty artifact set",
|
||||
pull.artifacts.length > 0,
|
||||
`${pull.artifacts.length} artifact(s), coverage=${pull.coverage}, roles=${pull.artifacts.map((a) => a.role).join(",")}`,
|
||||
);
|
||||
for (const artifact of pull.artifacts) {
|
||||
const bytes = await stat(artifact.local_path).then(
|
||||
(info) => info.size,
|
||||
);
|
||||
const digest = createHash("sha256")
|
||||
.update(await readFile(artifact.local_path))
|
||||
.digest("hex");
|
||||
check(
|
||||
`artifact digest matches on disk: ${artifact.file_name}`,
|
||||
bytes === artifact.bytes && digest === artifact.sha256,
|
||||
`${bytes} bytes`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
// File transfer: push one lane-owned file, verify both digests, pull it
|
||||
// back, then clean up the device copy through the same fixed argv surface.
|
||||
const transferRoot = await mkdtemp(join(tmpdir(), "rea-adb-transfer-"));
|
||||
try {
|
||||
const payload = join(transferRoot, "rea-verify.txt");
|
||||
const payloadBytes = "rea adb transfer verification\n";
|
||||
await import("node:fs/promises").then((fs) =>
|
||||
fs.writeFile(payload, payloadBytes),
|
||||
);
|
||||
const deviceTarget = `/data/local/tmp/rea-verify-${Date.now()}.txt`;
|
||||
const pushed = await run("push_adb_file", {
|
||||
serial,
|
||||
local_path: payload,
|
||||
device_path: deviceTarget,
|
||||
overwrite: false,
|
||||
});
|
||||
if (pushed !== null) {
|
||||
check(
|
||||
"push reports matching device-side digest",
|
||||
pushed.device_digest_source === "device_sha256sum" &&
|
||||
pushed.device_sha256 === pushed.sha256,
|
||||
`${pushed.bytes} bytes`,
|
||||
);
|
||||
const pulled = await run("pull_adb_file", {
|
||||
serial,
|
||||
device_path: deviceTarget,
|
||||
output_directory: transferRoot,
|
||||
});
|
||||
if (pulled !== null)
|
||||
check(
|
||||
"pulled file digest matches the pushed digest",
|
||||
pulled.sha256 === pushed.sha256,
|
||||
`${pulled.bytes} bytes`,
|
||||
);
|
||||
}
|
||||
const screenRoot = await mkdtemp(join(tmpdir(), "rea-adb-screen-"));
|
||||
try {
|
||||
const screen = await run("capture_adb_screen", {
|
||||
serial,
|
||||
output_directory: screenRoot,
|
||||
});
|
||||
if (screen !== null)
|
||||
check(
|
||||
"screen capture is a sized PNG",
|
||||
screen.width !== null && screen.height !== null && screen.bytes > 0,
|
||||
`${screen.width}x${screen.height}, ${screen.bytes} bytes`,
|
||||
);
|
||||
} finally {
|
||||
await rm(screenRoot, { recursive: true, force: true });
|
||||
}
|
||||
if (options.pull !== null) {
|
||||
const details = await run("inspect_adb_package", {
|
||||
serial,
|
||||
package: options.pull,
|
||||
});
|
||||
if (details !== null)
|
||||
check(
|
||||
"dumpsys package projection completed",
|
||||
details.coverage === "complete",
|
||||
`versionCode=${String(details.version_code)}`,
|
||||
);
|
||||
}
|
||||
if (options.installApk !== null && options.installApk !== undefined) {
|
||||
const probePackage = "com.rea.adb.probe";
|
||||
const installedProbe = await run("install_adb_package", {
|
||||
serial,
|
||||
apk_path: options.installApk,
|
||||
replace: false,
|
||||
});
|
||||
if (installedProbe !== null) {
|
||||
check(
|
||||
"install reports the APK digest",
|
||||
typeof installedProbe.sha256 === "string" &&
|
||||
installedProbe.sha256.length === 64,
|
||||
`${installedProbe.bytes} bytes`,
|
||||
);
|
||||
const resolved = await run("resolve_adb_packages", {
|
||||
serial,
|
||||
query: "rea.adb.probe",
|
||||
});
|
||||
if (resolved !== null)
|
||||
check(
|
||||
"resolve finds the installed probe uniquely",
|
||||
resolved.matches.length === 1 &&
|
||||
resolved.matches[0]?.package_name === probePackage,
|
||||
`${resolved.matches.length} match(es)`,
|
||||
);
|
||||
const started = await run("start_adb_app", {
|
||||
serial,
|
||||
package: probePackage,
|
||||
});
|
||||
if (started !== null)
|
||||
check(
|
||||
"app start resolves and launches the launcher activity",
|
||||
typeof started.component === "string" &&
|
||||
started.component.startsWith(probePackage),
|
||||
String(started.component),
|
||||
);
|
||||
const observedProcesses = await run("list_adb_processes", { serial });
|
||||
if (observedProcesses !== null)
|
||||
check(
|
||||
"started app appears in the process listing",
|
||||
observedProcesses.processes.some(
|
||||
(process) => process.name === probePackage,
|
||||
),
|
||||
);
|
||||
const stopped = await run("stop_adb_app", {
|
||||
serial,
|
||||
package: probePackage,
|
||||
});
|
||||
if (stopped !== null)
|
||||
check(
|
||||
"force-stop completes the dynamic-analysis run",
|
||||
stopped.package_name === probePackage,
|
||||
);
|
||||
const removed = await run("uninstall_adb_package", {
|
||||
serial,
|
||||
package: probePackage,
|
||||
});
|
||||
if (removed !== null)
|
||||
check(
|
||||
"uninstall completes the lifecycle",
|
||||
removed.package_name === probePackage,
|
||||
);
|
||||
const afterRemoval = await run("resolve_adb_packages", {
|
||||
serial,
|
||||
query: "rea.adb.probe",
|
||||
});
|
||||
if (afterRemoval !== null)
|
||||
check(
|
||||
"resolve reports zero matches after removal",
|
||||
afterRemoval.matches.length === 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await rm(transferRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await provider.close();
|
||||
} catch {
|
||||
// The provider owns nothing long-lived; closing is best-effort.
|
||||
}
|
||||
|
||||
if (failures > 0) {
|
||||
process.stdout.write(`\n${failures} check(s) failed\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write("\nall adb device checks passed\n");
|
||||
|
||||
function parseArguments(argv) {
|
||||
const parsed = { help: false, serial: null, pull: null, installApk: null };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const argument = argv[index];
|
||||
if (argument === "--help" || argument === "-h") parsed.help = true;
|
||||
else if (argument === "--serial") {
|
||||
parsed.serial = argv[index + 1] ?? null;
|
||||
index += 1;
|
||||
} else if (argument === "--pull") {
|
||||
parsed.pull = argv[index + 1] ?? null;
|
||||
index += 1;
|
||||
} else if (argument === "--install-apk") {
|
||||
parsed.installApk = argv[index + 1] ?? null;
|
||||
index += 1;
|
||||
} else {
|
||||
process.stderr.write(`unknown argument: ${argument}\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { ApktoolResourceAnalysisService } from "../dist/application/apktool/ApktoolResourceAnalysisService.js";
|
||||
import { createApktoolResourceAnalysisProvider } from "../dist/composition/apktool.js";
|
||||
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
if (options.help) {
|
||||
process.stdout.write(
|
||||
"Usage: npm run verify:apktool [-- --apk PATH]\n" +
|
||||
"Requires a caller-selected apktool launcher (REA_APKTOOL_COMMAND or\n" +
|
||||
"PATH) and a Java runtime. Without --apk, a small signed probe APK is\n" +
|
||||
"expected at the path given by --apk; nothing is downloaded or built.\n",
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const provider = createApktoolResourceAnalysisProvider(process.env);
|
||||
const service = new ApktoolResourceAnalysisService(provider);
|
||||
let failures = 0;
|
||||
const check = (name, condition, detail = "") => {
|
||||
const mark = condition ? "ok" : "FAIL";
|
||||
process.stdout.write(
|
||||
`[${mark}] ${name}${detail === "" ? "" : ` — ${detail}`}\n`,
|
||||
);
|
||||
if (!condition) failures += 1;
|
||||
};
|
||||
|
||||
const run = async (operation, input) => {
|
||||
const result = await service.execute(operation, input);
|
||||
if (!result.ok) {
|
||||
process.stdout.write(`[FAIL] ${operation} — ${result.error.message}\n`);
|
||||
failures += 1;
|
||||
return null;
|
||||
}
|
||||
return result.value.normalized_result;
|
||||
};
|
||||
|
||||
const client = await run("inspect_apktool_client", {});
|
||||
if (client !== null) {
|
||||
check(
|
||||
"apktool launcher reports a version",
|
||||
typeof client.client.apktool_version === "string" &&
|
||||
client.client.apktool_version !== "",
|
||||
`command=${client.client.command} version=${client.client.apktool_version}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (options.apk !== null) {
|
||||
const decoded = await run("decode_android_resources", {
|
||||
path: options.apk,
|
||||
include_strings: true,
|
||||
});
|
||||
if (decoded !== null) {
|
||||
const bytes = await stat(options.apk).then((info) => info.size);
|
||||
const digest = createHash("sha256")
|
||||
.update(await readFile(options.apk))
|
||||
.digest("hex");
|
||||
check(
|
||||
"decoded target identity matches the file on disk",
|
||||
decoded.target.bytes === bytes && decoded.target.sha256 === digest,
|
||||
`${bytes} bytes`,
|
||||
);
|
||||
check(
|
||||
"metadata carries version and SDK facts",
|
||||
typeof decoded.metadata.version_name === "string" &&
|
||||
decoded.metadata.min_sdk_version !== null &&
|
||||
decoded.metadata.target_sdk_version !== null,
|
||||
`version=${String(decoded.metadata.version_name)} sdk=${String(decoded.metadata.min_sdk_version)}..${String(decoded.metadata.target_sdk_version)}`,
|
||||
);
|
||||
check(
|
||||
"decoded manifest names the package",
|
||||
typeof decoded.metadata.package_name === "string" &&
|
||||
decoded.manifest !== null &&
|
||||
decoded.manifest.includes(decoded.metadata.package_name),
|
||||
String(decoded.metadata.package_name),
|
||||
);
|
||||
check(
|
||||
"string resources projected",
|
||||
decoded.strings.length > 0,
|
||||
`${decoded.strings.length} string(s)`,
|
||||
);
|
||||
if (decoded.locales.length > 0) {
|
||||
const locale = await run("decode_android_resources", {
|
||||
path: options.apk,
|
||||
include_strings: true,
|
||||
locale: decoded.locales[0],
|
||||
});
|
||||
if (locale !== null)
|
||||
check(
|
||||
`locale table projected for ${String(decoded.locales[0])}`,
|
||||
Array.isArray(locale.strings),
|
||||
`${locale.strings.length} string(s)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await provider.close();
|
||||
} catch {
|
||||
// The provider owns per-call workspaces only; closing is best-effort.
|
||||
}
|
||||
|
||||
if (failures > 0) {
|
||||
process.stdout.write(`\n${failures} check(s) failed\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write("\nall apktool checks passed\n");
|
||||
|
||||
function parseArguments(argv) {
|
||||
const parsed = { help: false, apk: null };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const argument = argv[index];
|
||||
if (argument === "--help" || argument === "-h") parsed.help = true;
|
||||
else if (argument === "--apk") {
|
||||
parsed.apk = argv[index + 1] ?? null;
|
||||
index += 1;
|
||||
} else {
|
||||
process.stderr.write(`unknown argument: ${argument}\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import {
|
||||
TOOL_CONTRACTS,
|
||||
toolContract,
|
||||
} from "../dist/contracts/toolContracts.js";
|
||||
import { exec } from "./lib/verify-package-core.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { createAnthropicModelFixture } from "./verify/clients/anthropic-model-fixture.mjs";
|
||||
|
||||
assert(
|
||||
process.platform !== "win32",
|
||||
"Native Claude Code verifier requires POSIX; Windows is unverified",
|
||||
);
|
||||
const repo = fileURLToPath(new URL("..", import.meta.url));
|
||||
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
|
||||
const command = process.env.REA_VERIFY_CLAUDE_COMMAND ?? "claude";
|
||||
const mode = process.argv[2] ?? "call";
|
||||
assert(
|
||||
["chat", "call"].includes(mode),
|
||||
"Usage: verify-claude-client.mjs [chat|call]",
|
||||
);
|
||||
const verifier = createVerifierRun();
|
||||
const lab = await mkdtemp(join(tmpdir(), "rea-claude-client-"));
|
||||
const profile = join(lab, "selected profile");
|
||||
const workspace = join(lab, "workspace");
|
||||
const configPath = join(profile, ".claude.json");
|
||||
const skillDirectory = join(profile, "skills", "reverse-engineer-anything");
|
||||
await Promise.all([mkdir(profile), mkdir(workspace)]);
|
||||
await exec("git", ["init", "--quiet", workspace], { timeout: 30_000 });
|
||||
const environment = {
|
||||
...process.env,
|
||||
CLAUDE_CONFIG_DIR: profile,
|
||||
DISABLE_AUTOUPDATER: "1",
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
|
||||
REA_PROCESS_RUN_ID: verifier.run_id,
|
||||
};
|
||||
let clientVersion;
|
||||
try {
|
||||
clientVersion = (
|
||||
await exec(command, ["--version"], { env: environment, timeout: 30_000 })
|
||||
).stdout.trim();
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`Claude Code preflight failed for ${command}; install Claude Code or set REA_VERIFY_CLAUDE_COMMAND. ${String(cause)}`,
|
||||
);
|
||||
}
|
||||
const target = join(lab, "fixture α测试");
|
||||
await mkdir(target);
|
||||
await writeFile(
|
||||
join(target, "app.js"),
|
||||
'export function result() { return { clientCompatibility: "REA_CLIENT_FIXTURE", count: 7 }; }\n',
|
||||
);
|
||||
let skillRequested = false;
|
||||
let skillLoaded = false;
|
||||
let expectedSkillBody;
|
||||
let evidenceSeen = false;
|
||||
let finalSeen = false;
|
||||
let catalogSize = 0;
|
||||
let schemasChecked = 0;
|
||||
let pendingServerWaits = 0;
|
||||
let artifactDigest;
|
||||
const schemaValidator = new Ajv2020({ strict: false, validateFormats: false });
|
||||
const containsText = (value, text) => {
|
||||
if (typeof value === "string") return value.includes(text);
|
||||
if (value && typeof value === "object")
|
||||
return Object.values(value).some((nested) => containsText(nested, text));
|
||||
return false;
|
||||
};
|
||||
const extractEvidence = (value) => {
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
// Native Claude appends this resource hint to tool results. Original bodies remain intact in request artifacts.
|
||||
const json = value.replace(
|
||||
/\n\n<system-reminder>\n<total_tokens>\d+ tokens left<\/total_tokens>\n<\/system-reminder>$/u,
|
||||
"",
|
||||
);
|
||||
return extractEvidence(JSON.parse(json));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
if (
|
||||
typeof value.evidence_id === "string" &&
|
||||
value.normalized_result?.graph?.nodes?.length > 0
|
||||
)
|
||||
return value;
|
||||
for (const nested of Object.values(value)) {
|
||||
const evidence = extractEvidence(nested);
|
||||
if (evidence) return evidence;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const fixture = await createAnthropicModelFixture({
|
||||
directory: lab,
|
||||
onRequest: async ({ body, tools, requestIndex }) => {
|
||||
if (tools.length === 0)
|
||||
return [{ type: "text", text: "REA fixture title" }];
|
||||
assert(requestIndex <= 8, "Local model workflow exceeded its turn budget");
|
||||
const call = (name, input) => [
|
||||
{ type: "tool_use", id: `toolu_rea_${requestIndex}`, name, input },
|
||||
];
|
||||
const analysis = tools.find((t) =>
|
||||
t.name.endsWith("analyze_javascript_application"),
|
||||
);
|
||||
if (!analysis) {
|
||||
// Native automatic discovery can begin the conversation while MCP is still connecting.
|
||||
const wait = tools.find((t) => t.name === "WaitForMcpServers");
|
||||
assert(
|
||||
wait,
|
||||
"Claude must expose REA tools or its native pending-server wait",
|
||||
);
|
||||
pendingServerWaits++;
|
||||
return call(wait.name, { servers: ["rea"] });
|
||||
}
|
||||
const prefix = analysis.name.slice(
|
||||
0,
|
||||
-"analyze_javascript_application".length,
|
||||
);
|
||||
const expected = TOOL_CONTRACTS.map((t) => prefix + t.name).sort();
|
||||
const reaTools = tools.filter((t) => t.name.startsWith(prefix));
|
||||
assert.deepEqual(
|
||||
reaTools.map((t) => t.name).sort(),
|
||||
expected,
|
||||
"Claude must forward the complete REA catalog after connection",
|
||||
);
|
||||
catalogSize = expected.length;
|
||||
if (schemasChecked === 0) {
|
||||
for (const tool of reaTools) {
|
||||
assert(
|
||||
tool.input_schema,
|
||||
`${tool.name}: missing advertised input schema`,
|
||||
);
|
||||
assert(
|
||||
schemaValidator.validateSchema(tool.input_schema),
|
||||
`${tool.name}: invalid input schema: ${JSON.stringify(schemaValidator.errors)}`,
|
||||
);
|
||||
schemasChecked++;
|
||||
}
|
||||
}
|
||||
skillLoaded ||=
|
||||
expectedSkillBody !== undefined &&
|
||||
containsText(body.messages, expectedSkillBody) &&
|
||||
containsText(body.messages, skillDirectory);
|
||||
const observed = extractEvidence(body.messages);
|
||||
if (observed) {
|
||||
const evidence = toolContract(
|
||||
"analyze_javascript_application",
|
||||
).outputSchema.parse(observed);
|
||||
assert.equal(evidence.subject.local_path, target);
|
||||
const shape = evidence.normalized_result.graph.nodes
|
||||
.flatMap((n) => n.observations)
|
||||
.map((o) => o.properties)
|
||||
.find(
|
||||
(p) =>
|
||||
p.semantic_role === "export-return-shapes" &&
|
||||
p.exported_name === "result",
|
||||
);
|
||||
assert(shape?.return_shape_coverage?.projection_complete);
|
||||
const fields = shape.static_return_shapes.flatMap((s) => s.fields);
|
||||
assert(
|
||||
fields.some(
|
||||
(f) =>
|
||||
f.path === "/clientCompatibility" &&
|
||||
f.state === "literal" &&
|
||||
f.value === "REA_CLIENT_FIXTURE",
|
||||
),
|
||||
);
|
||||
assert(
|
||||
fields.some(
|
||||
(f) => f.path === "/count" && f.state === "literal" && f.value === 7,
|
||||
),
|
||||
);
|
||||
const bytes = JSON.stringify(evidence);
|
||||
artifactDigest = createHash("sha256").update(bytes).digest("hex");
|
||||
await writeFile(join(lab, "claude-evidence.json"), bytes);
|
||||
evidenceSeen = true;
|
||||
}
|
||||
if (mode === "chat" || evidenceSeen) {
|
||||
finalSeen = true;
|
||||
return [{ type: "text", text: "REA_CLIENT_COMPATIBILITY_OK" }];
|
||||
}
|
||||
if (!skillRequested) {
|
||||
const skill = tools.find((t) => t.name === "Skill");
|
||||
assert(skill, "Claude must expose its native Skill tool");
|
||||
skillRequested = true;
|
||||
return call(skill.name, { skill: "reverse-engineer-anything" });
|
||||
}
|
||||
assert(
|
||||
skillLoaded,
|
||||
"Claude must load the complete installed personal REA skill",
|
||||
);
|
||||
return call(analysis.name, { input_path: target, format: "directory" });
|
||||
},
|
||||
});
|
||||
const original = JSON.stringify({ theme: "light", mcpServers: {} }, null, 2);
|
||||
await writeFile(configPath, original);
|
||||
let failure;
|
||||
try {
|
||||
for (const args of [
|
||||
["setup", "--client", "claude_code", "--dry-run"],
|
||||
["setup", "--client", "claude_code", "--yes"],
|
||||
["doctor", "--client", "claude_code", "--skill"],
|
||||
]) {
|
||||
const result = await exec(
|
||||
process.execPath,
|
||||
[join(runtimeRoot, "scripts/rea.mjs"), ...args, "--json"],
|
||||
{ env: environment, cwd: workspace, timeout: 60_000 },
|
||||
);
|
||||
await writeFile(
|
||||
join(lab, `${args[0]}${args.includes("--dry-run") ? "-plan" : ""}.json`),
|
||||
result.stdout,
|
||||
);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
if (args[0] === "doctor") assert.equal(parsed.healthy, true);
|
||||
else {
|
||||
assert.equal(
|
||||
parsed.status,
|
||||
args.includes("--dry-run") ? "planned" : "ready",
|
||||
);
|
||||
if (args.includes("--dry-run")) {
|
||||
assert(parsed.plannedActions.some((a) => a.target === configPath));
|
||||
assert(parsed.plannedActions.some((a) => a.target === skillDirectory));
|
||||
assert(
|
||||
parsed.plannedActions.every(
|
||||
(a) =>
|
||||
a.target === configPath || a.target.startsWith(skillDirectory),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
|
||||
const configured = await readFile(configPath, "utf8");
|
||||
assert.equal(JSON.parse(configured).theme, "light");
|
||||
const repeated = await exec(
|
||||
process.execPath,
|
||||
[
|
||||
join(runtimeRoot, "scripts/rea.mjs"),
|
||||
"setup",
|
||||
"--client",
|
||||
"claude_code",
|
||||
"--yes",
|
||||
"--json",
|
||||
],
|
||||
{ env: environment, cwd: workspace, timeout: 60_000 },
|
||||
);
|
||||
assert.deepEqual(JSON.parse(repeated.stdout).appliedActions, []);
|
||||
assert.equal(await readFile(configPath, "utf8"), configured);
|
||||
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
|
||||
const installedSkill = await readFile(
|
||||
join(skillDirectory, "SKILL.md"),
|
||||
"utf8",
|
||||
);
|
||||
assert(installedSkill.includes("name: reverse-engineer-anything"));
|
||||
const lines = installedSkill.split("\n");
|
||||
const headerEnd = lines.indexOf("---", 1);
|
||||
assert(
|
||||
lines[0] === "---" && headerEnd > 0,
|
||||
"Installed skill must have its authored frontmatter",
|
||||
);
|
||||
expectedSkillBody = lines
|
||||
.slice(headerEnd + 1)
|
||||
.join("\n")
|
||||
.trim();
|
||||
assert(expectedSkillBody.length > 0);
|
||||
const execution = exec(
|
||||
command,
|
||||
[
|
||||
"--setting-sources",
|
||||
"user",
|
||||
"--print",
|
||||
"--output-format",
|
||||
"json",
|
||||
"--no-session-persistence",
|
||||
"--model",
|
||||
"claude-sonnet-4-6",
|
||||
"--tools",
|
||||
"default",
|
||||
"--permission-mode",
|
||||
"dontAsk",
|
||||
"--settings",
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
allow: [
|
||||
"Skill",
|
||||
"WaitForMcpServers",
|
||||
"mcp__rea__analyze_javascript_application",
|
||||
],
|
||||
},
|
||||
}),
|
||||
mode === "chat"
|
||||
? "Say hello briefly."
|
||||
: `Use the REA skill and tools to analyze the JavaScript directory ${target}. Report the result.`,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...environment,
|
||||
ANTHROPIC_API_KEY: "local-fixture-key",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
ANTHROPIC_BASE_URL: fixture.baseUrl,
|
||||
},
|
||||
cwd: workspace,
|
||||
timeout: 120_000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
execution.child.stdin.end();
|
||||
const response = await execution;
|
||||
await writeFile(join(lab, `${mode}.stdout`), response.stdout);
|
||||
await writeFile(join(lab, `${mode}.stderr`), response.stderr);
|
||||
assert.equal(fixture.failure, undefined);
|
||||
const parsed = JSON.parse(response.stdout);
|
||||
assert.equal(parsed.is_error, false);
|
||||
assert.equal(parsed.subtype, "success");
|
||||
assert.equal(parsed.result, "REA_CLIENT_COMPATIBILITY_OK");
|
||||
assert.deepEqual(parsed.permission_denials, []);
|
||||
assert(finalSeen);
|
||||
if (mode === "call") assert(evidenceSeen && skillLoaded);
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
if (typeof cause?.stdout === "string")
|
||||
await writeFile(join(lab, `${mode}.stdout`), cause.stdout);
|
||||
if (typeof cause?.stderr === "string")
|
||||
await writeFile(join(lab, `${mode}.stderr`), cause.stderr);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
const receipt = {
|
||||
client: "Claude Code",
|
||||
clientVersion,
|
||||
mode,
|
||||
passed: failure === undefined,
|
||||
artifacts: lab,
|
||||
runtimeRoot,
|
||||
host: {
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
node: process.versions.node,
|
||||
},
|
||||
modelFixture:
|
||||
"loopback Anthropic Messages/SSE; synthetic usage and native resource hints; no live API claim",
|
||||
discovery:
|
||||
"native CLAUDE_CONFIG_DIR user configuration and skills; separate Git root, user settings only, default built-in tools",
|
||||
bareMode:
|
||||
"not used; native bare print mode did not load the personal skill",
|
||||
catalogSize,
|
||||
schemasChecked,
|
||||
pendingServerWaits,
|
||||
evidenceSeen,
|
||||
skillLoaded,
|
||||
artifactDigest,
|
||||
finalSeen,
|
||||
requests: fixture.requests,
|
||||
probes: fixture.probes,
|
||||
fixtureFailure: fixture.failure,
|
||||
verifier: await completeVerifierRun(verifier),
|
||||
};
|
||||
await writeFile(join(lab, "receipt.json"), JSON.stringify(receipt, null, 2));
|
||||
console.log(JSON.stringify(receipt));
|
||||
}
|
||||
if (failure !== undefined) throw failure;
|
||||
@@ -0,0 +1,440 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, isAbsolute, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import {
|
||||
TOOL_CONTRACTS,
|
||||
toolContract,
|
||||
} from "../dist/contracts/toolContracts.js";
|
||||
import { exec } from "./lib/verify-package-core.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs";
|
||||
|
||||
assert(
|
||||
process.platform !== "win32",
|
||||
"Native Copilot CLI verifier requires POSIX; Windows is unverified",
|
||||
);
|
||||
const repo = fileURLToPath(new URL("..", import.meta.url));
|
||||
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
|
||||
const command = process.env.REA_VERIFY_COPILOT_COMMAND ?? "copilot";
|
||||
const mode = process.argv[2] ?? "call";
|
||||
const model = process.env.REA_VERIFY_COPILOT_MODEL ?? "gpt-5.4";
|
||||
assert(
|
||||
["chat", "call"].includes(mode),
|
||||
"Usage: verify-copilot-client.mjs [chat|call]",
|
||||
);
|
||||
const verifier = createVerifierRun();
|
||||
const lab = await mkdtemp(join(tmpdir(), "rea-copilot-client-"));
|
||||
const account = join(lab, "account");
|
||||
const profile = join(lab, "selected profile");
|
||||
const workspace = join(lab, "workspace");
|
||||
const configPath = join(profile, "mcp-config.json");
|
||||
const skillDirectory = join(
|
||||
account,
|
||||
".agents",
|
||||
"skills",
|
||||
"reverse-engineer-anything",
|
||||
);
|
||||
await Promise.all([mkdir(account), mkdir(profile), mkdir(workspace)]);
|
||||
await exec("git", ["init", "--quiet", workspace], { timeout: 30_000 });
|
||||
// Do not inherit the caller's home or credentials into the isolated native profile.
|
||||
const environment = {
|
||||
PATH: process.env.PATH,
|
||||
NODE_OPTIONS: process.env.NODE_OPTIONS,
|
||||
USERPROFILE: account,
|
||||
COPILOT_HOME: profile,
|
||||
COPILOT_OFFLINE: "true",
|
||||
COPILOT_PROVIDER_TYPE: "openai",
|
||||
COPILOT_PROVIDER_WIRE_API: "completions",
|
||||
COPILOT_PROVIDER_API_KEY: "local-fixture-key",
|
||||
COPILOT_PROVIDER_API_KEY_COMMAND: "",
|
||||
COPILOT_PROVIDER_BEARER_TOKEN: "",
|
||||
COPILOT_PROVIDER_HEADERS: "",
|
||||
COPILOT_PROVIDER_MODEL_ID: model,
|
||||
COPILOT_PROVIDER_WIRE_MODEL: model,
|
||||
COPILOT_PROVIDER_MAX_PROMPT_TOKENS: "1000000",
|
||||
COPILOT_PROVIDER_MAX_OUTPUT_TOKENS: "4096",
|
||||
REA_PROCESS_RUN_ID: verifier.run_id,
|
||||
};
|
||||
let clientVersion;
|
||||
try {
|
||||
clientVersion = (
|
||||
await exec(command, ["--version"], { env: environment, timeout: 30_000 })
|
||||
).stdout.trim();
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`Copilot CLI preflight failed for ${command}; install Copilot CLI or set REA_VERIFY_COPILOT_COMMAND. ${String(cause)}`,
|
||||
);
|
||||
}
|
||||
const target = join(lab, "fixture α测试");
|
||||
await mkdir(target);
|
||||
await writeFile(
|
||||
join(target, "app.js"),
|
||||
'export function result() { return { clientCompatibility: "REA_CLIENT_FIXTURE", count: 7 }; }\n',
|
||||
);
|
||||
let skillRequested = false;
|
||||
let skillLoaded = false;
|
||||
let expectedSkillBody;
|
||||
let evidenceSeen = false;
|
||||
let finalSeen = false;
|
||||
let catalogSize = 0;
|
||||
let schemasChecked = 0;
|
||||
let spillReadRequested = false;
|
||||
let artifactDigest;
|
||||
const schemaValidator = new Ajv2020({ strict: false, validateFormats: false });
|
||||
const containsText = (value, text) => {
|
||||
if (typeof value === "string") return value.includes(text);
|
||||
if (value && typeof value === "object")
|
||||
return Object.values(value).some((nested) => containsText(nested, text));
|
||||
return false;
|
||||
};
|
||||
const extractEvidence = (value) => {
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
return extractEvidence(JSON.parse(value));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
if (
|
||||
typeof value.evidence_id === "string" &&
|
||||
value.normalized_result?.graph?.nodes?.length > 0
|
||||
)
|
||||
return value;
|
||||
for (const nested of Object.values(value)) {
|
||||
const evidence = extractEvidence(nested);
|
||||
if (evidence) return evidence;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const fixture = await createOpenAiModelFixture({
|
||||
directory: lab,
|
||||
prefix: "copilot",
|
||||
onRequest: async ({ body, tools, requestIndex }) => {
|
||||
// Auxiliary requests without tools cannot execute the analyst's workflow.
|
||||
if (tools.length === 0)
|
||||
return { role: "assistant", content: "REA fixture title" };
|
||||
const analysis = tools.find((t) =>
|
||||
t.name.endsWith("analyze_javascript_application"),
|
||||
);
|
||||
assert(analysis, "Copilot CLI must expose the REA analysis tool");
|
||||
const prefix = analysis.name.slice(
|
||||
0,
|
||||
-"analyze_javascript_application".length,
|
||||
);
|
||||
const expected = TOOL_CONTRACTS.map((t) => prefix + t.name).sort();
|
||||
const reaTools = tools.filter((t) => expected.includes(t.name));
|
||||
assert.deepEqual(
|
||||
reaTools.map((t) => t.name).sort(),
|
||||
expected,
|
||||
"Copilot CLI must forward the complete REA catalog",
|
||||
);
|
||||
catalogSize = expected.length;
|
||||
if (schemasChecked === 0) {
|
||||
for (const tool of reaTools) {
|
||||
assert(
|
||||
tool.parameters,
|
||||
`${tool.name}: missing advertised input schema`,
|
||||
);
|
||||
assert(
|
||||
schemaValidator.validateSchema(tool.parameters),
|
||||
`${tool.name}: invalid input schema: ${JSON.stringify(schemaValidator.errors)}`,
|
||||
);
|
||||
schemasChecked++;
|
||||
}
|
||||
}
|
||||
const replies = body.messages.filter((m) => m.role === "tool");
|
||||
// Native skill activation injects its context as a user message after the acknowledgement.
|
||||
skillLoaded ||=
|
||||
skillRequested &&
|
||||
expectedSkillBody !== undefined &&
|
||||
containsText(body.messages, expectedSkillBody) &&
|
||||
containsText(body.messages, skillDirectory);
|
||||
const observed = extractEvidence(replies);
|
||||
if (observed) {
|
||||
const evidence = toolContract(
|
||||
"analyze_javascript_application",
|
||||
).outputSchema.parse(observed);
|
||||
assert.equal(evidence.subject.local_path, target);
|
||||
const shape = evidence.normalized_result.graph.nodes
|
||||
.flatMap((n) => n.observations)
|
||||
.map((o) => o.properties)
|
||||
.find(
|
||||
(p) =>
|
||||
p.semantic_role === "export-return-shapes" &&
|
||||
p.exported_name === "result",
|
||||
);
|
||||
assert(shape?.return_shape_coverage?.projection_complete);
|
||||
const fields = shape.static_return_shapes.flatMap((s) => s.fields);
|
||||
assert(
|
||||
fields.some(
|
||||
(f) =>
|
||||
f.path === "/clientCompatibility" &&
|
||||
f.state === "literal" &&
|
||||
f.value === "REA_CLIENT_FIXTURE",
|
||||
),
|
||||
);
|
||||
assert(
|
||||
fields.some(
|
||||
(f) => f.path === "/count" && f.state === "literal" && f.value === 7,
|
||||
),
|
||||
);
|
||||
const bytes = JSON.stringify(evidence);
|
||||
artifactDigest = createHash("sha256").update(bytes).digest("hex");
|
||||
await writeFile(join(lab, "copilot-evidence.json"), bytes);
|
||||
evidenceSeen = true;
|
||||
}
|
||||
if (mode === "chat" || evidenceSeen) {
|
||||
finalSeen = true;
|
||||
return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
|
||||
}
|
||||
assert(requestIndex <= 8, "Local model workflow exceeded its turn budget");
|
||||
const call = (name, args) => ({
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: `call_rea_fixture_${requestIndex}`,
|
||||
type: "function",
|
||||
function: { name, arguments: JSON.stringify(args) },
|
||||
},
|
||||
],
|
||||
});
|
||||
if (!skillRequested) {
|
||||
const skill = tools.find((t) => t.name === "skill");
|
||||
assert(skill, "Copilot CLI must expose its native skill tool");
|
||||
skillRequested = true;
|
||||
return call(skill.name, { skill: "reverse-engineer-anything" });
|
||||
}
|
||||
assert(
|
||||
skillLoaded,
|
||||
"Copilot CLI must load the installed personal REA skill",
|
||||
);
|
||||
const spilled = replies.find(
|
||||
(message) =>
|
||||
typeof message.content === "string" &&
|
||||
message.content.startsWith("Output too large to read at once "),
|
||||
);
|
||||
if (spilled) {
|
||||
assert(
|
||||
!spillReadRequested,
|
||||
"Copilot did not return complete Evidence after its native spill read",
|
||||
);
|
||||
const path = /\. Saved to: ([^\n]+)\n/u.exec(spilled.content)?.[1];
|
||||
assert(
|
||||
path &&
|
||||
isAbsolute(path) &&
|
||||
/^\d+-copilot-tool-output-[a-f\d]+\.txt$/u.test(basename(path)),
|
||||
"Unexpected native spill path",
|
||||
);
|
||||
const view = tools.find((tool) => tool.name === "view");
|
||||
assert(view, "Copilot must expose its native file reader");
|
||||
spillReadRequested = true;
|
||||
return call(view.name, { path, forceReadLargeFiles: true });
|
||||
}
|
||||
return call(analysis.name, { input_path: target, format: "directory" });
|
||||
},
|
||||
});
|
||||
const original = JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
other: {
|
||||
type: "local",
|
||||
command: "unrelated-server",
|
||||
args: [],
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
await writeFile(configPath, original);
|
||||
let failure;
|
||||
try {
|
||||
for (const args of [
|
||||
["setup", "--client", "copilot_cli", "--dry-run"],
|
||||
["setup", "--client", "copilot_cli", "--yes"],
|
||||
["doctor", "--client", "copilot_cli", "--skill"],
|
||||
]) {
|
||||
const result = await exec(
|
||||
process.execPath,
|
||||
[join(runtimeRoot, "scripts/rea.mjs"), ...args, "--json"],
|
||||
{ env: environment, cwd: workspace, timeout: 60_000 },
|
||||
);
|
||||
await writeFile(
|
||||
join(lab, `${args[0]}${args.includes("--dry-run") ? "-plan" : ""}.json`),
|
||||
result.stdout,
|
||||
);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
if (args[0] === "doctor") assert.equal(parsed.healthy, true);
|
||||
else {
|
||||
assert.equal(
|
||||
parsed.status,
|
||||
args.includes("--dry-run") ? "planned" : "ready",
|
||||
);
|
||||
if (args.includes("--dry-run")) {
|
||||
assert(parsed.plannedActions.some((a) => a.target === configPath));
|
||||
assert(parsed.plannedActions.some((a) => a.target === skillDirectory));
|
||||
assert(
|
||||
parsed.plannedActions.every(
|
||||
(a) =>
|
||||
a.target === configPath ||
|
||||
a.target === skillDirectory ||
|
||||
a.target.startsWith(skillDirectory + "/"),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
|
||||
const configured = await readFile(configPath, "utf8");
|
||||
assert(configured.includes("unrelated-server"));
|
||||
|
||||
const repeated = await exec(
|
||||
process.execPath,
|
||||
[
|
||||
join(runtimeRoot, "scripts/rea.mjs"),
|
||||
"setup",
|
||||
"--client",
|
||||
"copilot_cli",
|
||||
"--yes",
|
||||
"--json",
|
||||
],
|
||||
{ env: environment, cwd: workspace, timeout: 60_000 },
|
||||
);
|
||||
assert.deepEqual(JSON.parse(repeated.stdout).appliedActions, []);
|
||||
assert.equal(await readFile(configPath, "utf8"), configured);
|
||||
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
|
||||
const installedSkill = await readFile(
|
||||
join(skillDirectory, "SKILL.md"),
|
||||
"utf8",
|
||||
);
|
||||
assert(installedSkill.includes("name: reverse-engineer-anything"));
|
||||
const skillLines = installedSkill.split("\n");
|
||||
const headerEnd = skillLines.indexOf("---", 1);
|
||||
assert(
|
||||
skillLines[0] === "---" && headerEnd > 0,
|
||||
"Installed skill must have its authored frontmatter",
|
||||
);
|
||||
expectedSkillBody = skillLines
|
||||
.slice(headerEnd + 1)
|
||||
.join("\n")
|
||||
.trim();
|
||||
assert(expectedSkillBody.length > 0);
|
||||
for (const [name, args] of [
|
||||
["mcp-list", ["mcp", "list"]],
|
||||
["skill-add", ["skill", "add", skillDirectory]],
|
||||
["skill-list", ["skill", "list"]],
|
||||
]) {
|
||||
const result = await exec(command, args, {
|
||||
env: environment,
|
||||
cwd: workspace,
|
||||
timeout: 30_000,
|
||||
});
|
||||
await writeFile(join(lab, `${name}.stdout`), result.stdout);
|
||||
await writeFile(join(lab, `${name}.stderr`), result.stderr);
|
||||
if (name === "mcp-list") assert(result.stdout.includes("rea"));
|
||||
if (name === "skill-list")
|
||||
assert(result.stdout.includes("reverse-engineer-anything"));
|
||||
}
|
||||
const execution = exec(
|
||||
command,
|
||||
[
|
||||
"--disable-builtin-mcps",
|
||||
"--disable-mcp-server",
|
||||
"other",
|
||||
"--no-ask-user",
|
||||
"--output-format",
|
||||
"json",
|
||||
"--model",
|
||||
model,
|
||||
"--allow-tool=skill",
|
||||
"--allow-tool=read",
|
||||
"--allow-tool=rea(analyze_javascript_application)",
|
||||
"--add-dir",
|
||||
target,
|
||||
"--prompt",
|
||||
mode === "chat"
|
||||
? "Say hello briefly."
|
||||
: `Use the REA skill and tools to analyze the JavaScript directory ${target}. Report the result.`,
|
||||
],
|
||||
{
|
||||
env: { ...environment, COPILOT_PROVIDER_BASE_URL: fixture.baseUrl },
|
||||
cwd: workspace,
|
||||
timeout: 120_000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
execution.child.stdin.end();
|
||||
const response = await execution;
|
||||
await writeFile(join(lab, `${mode}.stdout`), response.stdout);
|
||||
await writeFile(join(lab, `${mode}.stderr`), response.stderr);
|
||||
assert.equal(fixture.failure, undefined);
|
||||
const events = response.stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
assert(
|
||||
!events.some((event) => event.type === "session.error"),
|
||||
"Copilot emitted a session error",
|
||||
);
|
||||
assert(finalSeen);
|
||||
assert(
|
||||
events.some(
|
||||
(event) =>
|
||||
event.type === "assistant.message" &&
|
||||
event.data.content === "REA_CLIENT_COMPATIBILITY_OK",
|
||||
),
|
||||
"Copilot must finish with the expected native assistant message",
|
||||
);
|
||||
if (mode === "call") assert(evidenceSeen && skillLoaded);
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
if (typeof cause?.stdout === "string")
|
||||
await writeFile(join(lab, `${mode}.stdout`), cause.stdout);
|
||||
if (typeof cause?.stderr === "string")
|
||||
await writeFile(join(lab, `${mode}.stderr`), cause.stderr);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
const lineage = await completeVerifierRun(verifier);
|
||||
const receipt = {
|
||||
client: "Copilot CLI",
|
||||
clientVersion,
|
||||
mode,
|
||||
model,
|
||||
passed: failure === undefined,
|
||||
artifacts: lab,
|
||||
runtimeRoot,
|
||||
host: {
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
node: process.versions.node,
|
||||
},
|
||||
modelFixture:
|
||||
"loopback OpenAI completions/SSE, synthetic usage, requested one-million-token BYOK prompt capacity; effective capacity unknown; no live provider claim",
|
||||
skillDiscovery:
|
||||
"native COPILOT_HOME MCP configuration and caller-added isolated skill directory; default OS-home discovery unverified",
|
||||
capacityBoundary:
|
||||
"gpt-4.1 model configuration blocks the full catalog before HTTP even with a ten-million-token requested prompt override; gpt-5.4 is the verified local fixture configuration",
|
||||
catalogSize,
|
||||
schemasChecked,
|
||||
spillReadRequested,
|
||||
evidenceSeen,
|
||||
skillLoaded,
|
||||
artifactDigest,
|
||||
finalSeen,
|
||||
requests: fixture.requests,
|
||||
probes: fixture.probes,
|
||||
fixtureFailure: fixture.failure,
|
||||
verifier: lineage,
|
||||
};
|
||||
await writeFile(join(lab, "receipt.json"), JSON.stringify(receipt, null, 2));
|
||||
console.log(JSON.stringify(receipt));
|
||||
}
|
||||
if (failure !== undefined) throw failure;
|
||||
@@ -0,0 +1,346 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import {
|
||||
TOOL_CONTRACTS,
|
||||
toolContract,
|
||||
} from "../dist/contracts/toolContracts.js";
|
||||
import { exec } from "./lib/verify-package-core.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs";
|
||||
|
||||
const mode = process.argv[2] ?? "call";
|
||||
assert(["chat", "call"].includes(mode), "Select chat or call.");
|
||||
assert(process.platform !== "win32", "Native Windows coverage is unverified.");
|
||||
const repo = fileURLToPath(new URL("..", import.meta.url));
|
||||
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
|
||||
const command = process.env.REA_VERIFY_DEEPSEEK_COMMAND ?? "dsh";
|
||||
const lab = await mkdtemp(join(tmpdir(), "rea-deepseek-client-"));
|
||||
const account = join(lab, "account");
|
||||
const profile = join(lab, "selected profile");
|
||||
const workspace = join(lab, "workspace");
|
||||
const target = join(workspace, "fixture α测试");
|
||||
const skillPath = join(
|
||||
account,
|
||||
".agents",
|
||||
"skills",
|
||||
"reverse-engineer-anything",
|
||||
"SKILL.md",
|
||||
);
|
||||
const cli = join(runtimeRoot, "scripts", "rea.mjs");
|
||||
const verifier = createVerifierRun();
|
||||
await Promise.all([
|
||||
mkdir(account),
|
||||
mkdir(profile),
|
||||
mkdir(target, { recursive: true }),
|
||||
]);
|
||||
const environment = {
|
||||
PATH: process.env.PATH ?? "",
|
||||
USERPROFILE: account,
|
||||
DSH_HOME: profile,
|
||||
DSH_AGENTS_HOME: join(account, ".agents"),
|
||||
OPENAI_API_KEY: "local-fixture-key",
|
||||
REA_PROCESS_RUN_ID: verifier.run_id,
|
||||
};
|
||||
// A temporary directory under the checkout must not inherit its project skills or AGENTS.md.
|
||||
await exec(
|
||||
"git",
|
||||
["-c", "init.defaultBranch=main", "init", "--quiet", workspace],
|
||||
{
|
||||
env: environment,
|
||||
timeout: 30_000,
|
||||
},
|
||||
);
|
||||
await writeFile(
|
||||
join(target, "app.js"),
|
||||
'export function result() { return { clientCompatibility: "REA_CLIENT_FIXTURE", count: 7 }; }\n',
|
||||
);
|
||||
const clientVersion = (
|
||||
await exec(command, ["--version"], {
|
||||
env: environment,
|
||||
cwd: workspace,
|
||||
timeout: 30_000,
|
||||
})
|
||||
).stdout.trim();
|
||||
for (const args of [
|
||||
["setup", "--skill", "--dry-run"],
|
||||
["setup", "--skill", "--yes"],
|
||||
]) {
|
||||
const result = await exec(process.execPath, [cli, ...args, "--json"], {
|
||||
env: environment,
|
||||
cwd: workspace,
|
||||
timeout: 60_000,
|
||||
});
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
assert.equal(parsed.status, args.includes("--dry-run") ? "planned" : "ready");
|
||||
if (args.includes("--dry-run")) {
|
||||
assert.equal(parsed.plannedActions.length, 1);
|
||||
assert.equal(parsed.plannedActions[0].kind, "install_skill");
|
||||
assert.equal(parsed.plannedActions[0].target, dirname(skillPath));
|
||||
}
|
||||
await writeFile(
|
||||
join(lab, args.includes("--dry-run") ? "setup-plan.json" : "setup.json"),
|
||||
result.stdout,
|
||||
);
|
||||
}
|
||||
assert(
|
||||
(await readFile(skillPath, "utf8")).includes(
|
||||
"name: reverse-engineer-anything",
|
||||
),
|
||||
);
|
||||
|
||||
let catalog;
|
||||
let patternCount = 0;
|
||||
let skillRequested = false;
|
||||
let skillLoaded = false;
|
||||
let evidenceSeen = false;
|
||||
let finalSeen = false;
|
||||
let artifactDigest;
|
||||
let mainRequests = 0;
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
const patterns = new Set();
|
||||
const validatePatterns = (value) => {
|
||||
if (!value || typeof value !== "object") return;
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
if (key === "pattern" && typeof child === "string") {
|
||||
// Check the representation the native client actually forwards to its model adapter.
|
||||
for (const flags of ["", "u", "v"]) new RegExp(child, flags);
|
||||
patterns.add(child);
|
||||
patternCount++;
|
||||
} else validatePatterns(child);
|
||||
}
|
||||
};
|
||||
const extractEvidence = (value) => {
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
return extractEvidence(JSON.parse(value));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
if (
|
||||
typeof value.evidence_id === "string" &&
|
||||
value.normalized_result?.graph?.nodes
|
||||
)
|
||||
return value;
|
||||
for (const child of Object.values(value)) {
|
||||
const found = extractEvidence(child);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
};
|
||||
const fixture = await createOpenAiModelFixture({
|
||||
directory: lab,
|
||||
prefix: `deepseek-${mode}`,
|
||||
onRequest: async ({ body, tools, requestIndex }) => {
|
||||
// Harness generates a session title in a separate, tool-free model request.
|
||||
if (tools.length === 0)
|
||||
return { role: "assistant", content: "REA validation" };
|
||||
assert(
|
||||
++mainRequests <= 4,
|
||||
"Harness did not complete the bounded fixture workflow.",
|
||||
);
|
||||
const rea = tools.filter((tool) => tool.name.startsWith("mcp__rea__"));
|
||||
const observedCatalog = rea
|
||||
.map((tool) => tool.name.slice("mcp__rea__".length))
|
||||
.sort();
|
||||
assert.deepEqual(
|
||||
observedCatalog,
|
||||
TOOL_CONTRACTS.map((tool) => tool.name).sort(),
|
||||
);
|
||||
if (catalog === undefined) {
|
||||
catalog = observedCatalog;
|
||||
for (const tool of rea) {
|
||||
assert(
|
||||
ajv.validateSchema(tool.parameters),
|
||||
`${tool.name}: ${ajv.errorsText(ajv.errors)}`,
|
||||
);
|
||||
validatePatterns(tool.parameters);
|
||||
}
|
||||
assert(
|
||||
patternCount > 0,
|
||||
"The advertised regex constraints must reach the model.",
|
||||
);
|
||||
}
|
||||
const toolMessages = (body.messages ?? []).filter(
|
||||
(message) => message.role === "tool",
|
||||
);
|
||||
const serialized = JSON.stringify(toolMessages);
|
||||
skillLoaded ||=
|
||||
serialized.includes(
|
||||
'<skill_content name=\\"reverse-engineer-anything\\">',
|
||||
) && serialized.includes(dirname(skillPath));
|
||||
for (const message of toolMessages) {
|
||||
const found = extractEvidence(message.content);
|
||||
if (found === undefined) continue;
|
||||
const evidence = toolContract(
|
||||
"analyze_javascript_application",
|
||||
).outputSchema.parse(found);
|
||||
assert.equal(evidence.subject.local_path, target);
|
||||
const shape = evidence.normalized_result.graph.nodes
|
||||
.flatMap((node) => node.observations)
|
||||
.map((observation) => observation.properties)
|
||||
.find(
|
||||
(properties) =>
|
||||
properties.semantic_role === "export-return-shapes" &&
|
||||
properties.exported_name === "result",
|
||||
);
|
||||
assert(shape?.return_shape_coverage?.projection_complete);
|
||||
const fields = shape.static_return_shapes.flatMap(
|
||||
(entry) => entry.fields,
|
||||
);
|
||||
assert(
|
||||
fields.some(
|
||||
(field) =>
|
||||
field.path === "/clientCompatibility" &&
|
||||
field.state === "literal" &&
|
||||
field.value === "REA_CLIENT_FIXTURE",
|
||||
),
|
||||
);
|
||||
assert(
|
||||
fields.some(
|
||||
(field) =>
|
||||
field.path === "/count" &&
|
||||
field.state === "literal" &&
|
||||
field.value === 7,
|
||||
),
|
||||
);
|
||||
const text = JSON.stringify(evidence);
|
||||
await writeFile(join(lab, "evidence.json"), text);
|
||||
artifactDigest = createHash("sha256").update(text).digest("hex");
|
||||
evidenceSeen = true;
|
||||
}
|
||||
if (mode === "chat" || evidenceSeen) {
|
||||
finalSeen = true;
|
||||
return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
|
||||
}
|
||||
const call = (name, args) => ({
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: `call_rea_${requestIndex}`,
|
||||
type: "function",
|
||||
function: { name, arguments: JSON.stringify(args) },
|
||||
},
|
||||
],
|
||||
});
|
||||
if (!skillRequested) {
|
||||
assert(tools.some((tool) => tool.name === "skill"));
|
||||
skillRequested = true;
|
||||
return call("skill", { name: "reverse-engineer-anything" });
|
||||
}
|
||||
assert(
|
||||
skillLoaded,
|
||||
"Harness must load the installed skill before analysis.",
|
||||
);
|
||||
return call("mcp__rea__analyze_javascript_application", {
|
||||
input_path: target,
|
||||
format: "directory",
|
||||
});
|
||||
},
|
||||
});
|
||||
let failure;
|
||||
try {
|
||||
// Use the client's native Cordis patch DSL; generic `rea setup --client` does not edit it.
|
||||
await writeFile(
|
||||
join(profile, "cordis.patch.yml"),
|
||||
`
|
||||
- id: llm-pi-ai
|
||||
config:
|
||||
providers:
|
||||
rea-fixture:
|
||||
apiKeyEnv: OPENAI_API_KEY
|
||||
api: openai-completions
|
||||
baseURL: ${fixture.baseUrl}
|
||||
defaultMaxTokens: 4096
|
||||
retryPolicy:
|
||||
mode: normal
|
||||
maxRetries: 0
|
||||
models:
|
||||
- id: rea-client-fixture
|
||||
contextWindow: 1000000
|
||||
- id: agent-default-model
|
||||
config:
|
||||
provider: rea-fixture
|
||||
model: rea-client-fixture
|
||||
- insert:
|
||||
- id: rea-mcp
|
||||
name: '@deepseek-ai/dsh-mcp-client'
|
||||
config:
|
||||
serverName: rea
|
||||
transport: stdio
|
||||
command: ${JSON.stringify(process.execPath)}
|
||||
args: ${JSON.stringify([cli, "mcp"])}
|
||||
cwd: !!js process.cwd()
|
||||
env:
|
||||
REA_PROCESS_RUN_ID: ${JSON.stringify(verifier.run_id)}
|
||||
`,
|
||||
);
|
||||
const execution = exec(
|
||||
command,
|
||||
[
|
||||
"--profile",
|
||||
"headless",
|
||||
"--json",
|
||||
mode === "chat" ? "Say hello briefly." : `Use REA to analyze ${target}.`,
|
||||
],
|
||||
{
|
||||
env: environment,
|
||||
cwd: workspace,
|
||||
timeout: 120_000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
execution.child.stdin.end();
|
||||
const result = await execution;
|
||||
await writeFile(join(lab, "client.stdout"), result.stdout);
|
||||
await writeFile(join(lab, "client.stderr"), result.stderr);
|
||||
assert.equal(fixture.failure, undefined);
|
||||
assert(finalSeen && result.stdout.includes("REA_CLIENT_COMPATIBILITY_OK"));
|
||||
if (mode === "call") assert(evidenceSeen && skillLoaded);
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
if (typeof cause?.stdout === "string")
|
||||
await writeFile(join(lab, "client.stdout"), cause.stdout);
|
||||
if (typeof cause?.stderr === "string")
|
||||
await writeFile(join(lab, "client.stderr"), cause.stderr);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
const receipt = {
|
||||
client: "deepseek-harness",
|
||||
clientVersion,
|
||||
mode,
|
||||
passed: failure === undefined,
|
||||
artifacts: lab,
|
||||
runtimeRoot,
|
||||
host: {
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
node: process.versions.node,
|
||||
},
|
||||
modelFixture:
|
||||
"loopback custom OpenAI-compatible adapter; no live DeepSeek provider claim",
|
||||
modelContextWindow: 1000000,
|
||||
skillDiscovery:
|
||||
"native DSH_AGENTS_HOME personal directory; isolated project root",
|
||||
catalogSize: catalog?.length,
|
||||
patternCount,
|
||||
uniquePatterns: patterns.size,
|
||||
skillLoaded,
|
||||
evidenceSeen,
|
||||
artifactDigest,
|
||||
finalSeen,
|
||||
requests: fixture.requests,
|
||||
fixtureFailure: fixture.failure,
|
||||
verifier: await completeVerifierRun(verifier),
|
||||
};
|
||||
await writeFile(join(lab, "receipt.json"), JSON.stringify(receipt, null, 2));
|
||||
console.log(JSON.stringify(receipt));
|
||||
}
|
||||
if (failure !== undefined) throw failure;
|
||||
@@ -0,0 +1,378 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import {
|
||||
TOOL_CONTRACTS,
|
||||
toolContract,
|
||||
} from "../dist/contracts/toolContracts.js";
|
||||
import { exec } from "./lib/verify-package-core.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { createGeminiModelFixture } from "./verify/clients/gemini-model-fixture.mjs";
|
||||
|
||||
assert(
|
||||
process.platform !== "win32",
|
||||
"Native Gemini verifier requires POSIX; Windows is unverified",
|
||||
);
|
||||
const repo = fileURLToPath(new URL("..", import.meta.url));
|
||||
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
|
||||
const command = process.env.REA_VERIFY_GEMINI_COMMAND ?? "gemini";
|
||||
const mode = process.argv[2] ?? "call";
|
||||
assert(
|
||||
["chat", "call"].includes(mode),
|
||||
"Usage: verify-gemini-client.mjs [chat|call]",
|
||||
);
|
||||
const verifier = createVerifierRun();
|
||||
const lab = await mkdtemp(join(tmpdir(), "rea-gemini-client-"));
|
||||
const account = join(lab, "account");
|
||||
const profile = join(lab, "selected profile");
|
||||
const workspace = join(lab, "workspace");
|
||||
const configPath = join(profile, ".gemini", "settings.json");
|
||||
const skillPath = join(
|
||||
profile,
|
||||
".agents",
|
||||
"skills",
|
||||
"reverse-engineer-anything",
|
||||
"SKILL.md",
|
||||
);
|
||||
await Promise.all([
|
||||
mkdir(account),
|
||||
mkdir(workspace),
|
||||
mkdir(join(profile, ".gemini"), { recursive: true }),
|
||||
]);
|
||||
await exec("git", ["init", "--quiet", workspace], { timeout: 30_000 });
|
||||
await writeFile(join(lab, "system-settings.json"), "{}\n");
|
||||
await writeFile(join(lab, "system-defaults.json"), "{}\n");
|
||||
const environment = {
|
||||
PATH: process.env.PATH ?? "",
|
||||
USERPROFILE: account,
|
||||
GEMINI_CLI_HOME: profile,
|
||||
GEMINI_CLI_SYSTEM_SETTINGS_PATH: join(lab, "system-settings.json"),
|
||||
GEMINI_CLI_SYSTEM_DEFAULTS_PATH: join(lab, "system-defaults.json"),
|
||||
// Preserve the caller's bounded Node heap instead of the launcher's host-memory auto-sizing.
|
||||
GEMINI_CLI_NO_RELAUNCH: "1",
|
||||
GEMINI_CLI_TRUST_WORKSPACE: "true",
|
||||
REA_PROCESS_RUN_ID: verifier.run_id,
|
||||
};
|
||||
let clientVersion;
|
||||
try {
|
||||
clientVersion = (
|
||||
await exec(command, ["--version"], { env: environment, timeout: 30_000 })
|
||||
).stdout.trim();
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`Gemini preflight failed for ${command}; install Gemini CLI or set REA_VERIFY_GEMINI_COMMAND. ${String(cause)}`,
|
||||
);
|
||||
}
|
||||
const original = JSON.stringify(
|
||||
{
|
||||
security: {
|
||||
auth: { selectedType: "gemini-api-key" },
|
||||
folderTrust: { enabled: false },
|
||||
},
|
||||
telemetry: { enabled: false },
|
||||
advanced: { autoConfigureMemory: false },
|
||||
model: { name: "gemini-2.5-flash", maxSessionTurns: 8 },
|
||||
mcpServers: { other: { command: "unrelated-server" } },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
await writeFile(configPath, original);
|
||||
for (const args of [
|
||||
["setup", "--client", "gemini_cli", "--dry-run"],
|
||||
["setup", "--client", "gemini_cli", "--yes"],
|
||||
["doctor", "--client", "gemini_cli", "--skill"],
|
||||
]) {
|
||||
const result = await exec(
|
||||
process.execPath,
|
||||
[join(runtimeRoot, "scripts/rea.mjs"), ...args, "--json"],
|
||||
{ env: environment, cwd: workspace, timeout: 60_000 },
|
||||
);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
if (args[0] === "doctor") assert.equal(parsed.healthy, true);
|
||||
else {
|
||||
assert.equal(
|
||||
parsed.status,
|
||||
args.includes("--dry-run") ? "planned" : "ready",
|
||||
);
|
||||
if (args.includes("--dry-run")) {
|
||||
assert(parsed.plannedActions.some((a) => a.target === configPath));
|
||||
assert(
|
||||
parsed.plannedActions.some((a) =>
|
||||
a.target.startsWith(join(profile, ".agents", "skills") + "/"),
|
||||
),
|
||||
);
|
||||
assert(
|
||||
parsed.plannedActions.every((a) => a.target.startsWith(profile + "/")),
|
||||
);
|
||||
}
|
||||
}
|
||||
await writeFile(
|
||||
join(lab, `${args[0]}${args.includes("--dry-run") ? "-plan" : ""}.json`),
|
||||
result.stdout,
|
||||
);
|
||||
}
|
||||
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
|
||||
const configured = await readFile(configPath, "utf8");
|
||||
assert.deepEqual(JSON.parse(configured).mcpServers.other, {
|
||||
command: "unrelated-server",
|
||||
});
|
||||
const repeated = await exec(
|
||||
process.execPath,
|
||||
[
|
||||
join(runtimeRoot, "scripts/rea.mjs"),
|
||||
"setup",
|
||||
"--client",
|
||||
"gemini_cli",
|
||||
"--yes",
|
||||
"--json",
|
||||
],
|
||||
{ env: environment, cwd: workspace, timeout: 60_000 },
|
||||
);
|
||||
assert.deepEqual(JSON.parse(repeated.stdout).appliedActions, []);
|
||||
assert.equal(await readFile(configPath, "utf8"), configured);
|
||||
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
|
||||
assert(
|
||||
(await readFile(skillPath, "utf8")).includes(
|
||||
"name: reverse-engineer-anything",
|
||||
),
|
||||
);
|
||||
const target = join(lab, "fixture α测试");
|
||||
await mkdir(target);
|
||||
await writeFile(
|
||||
join(target, "app.js"),
|
||||
'export function result() { return { clientCompatibility: "REA_CLIENT_FIXTURE", count: 7 }; }\n',
|
||||
);
|
||||
let evidenceSeen = false;
|
||||
let skillLoaded = false;
|
||||
let skillRequested = false;
|
||||
let finalSeen = false;
|
||||
let catalogSize = 0;
|
||||
let schemasChecked = 0;
|
||||
let artifactDigest;
|
||||
const schemaValidator = new Ajv2020({ strict: false, validateFormats: false });
|
||||
const extractEvidence = (value) => {
|
||||
if (typeof value === "string") {
|
||||
if (
|
||||
value.startsWith("<untrusted_context>\n") &&
|
||||
value.endsWith("\n</untrusted_context>")
|
||||
) {
|
||||
return extractEvidence(
|
||||
value.slice(
|
||||
"<untrusted_context>\n".length,
|
||||
-"\n</untrusted_context>".length,
|
||||
),
|
||||
);
|
||||
}
|
||||
try {
|
||||
return extractEvidence(JSON.parse(value));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
if (
|
||||
typeof value.evidence_id === "string" &&
|
||||
value.normalized_result?.graph?.nodes?.length > 0
|
||||
)
|
||||
return value;
|
||||
for (const nested of Object.values(value)) {
|
||||
const evidence = extractEvidence(nested);
|
||||
if (evidence) return evidence;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const fixture = await createGeminiModelFixture({
|
||||
directory: lab,
|
||||
onRequest: async ({ body, tools }) => {
|
||||
const analysis = tools.find((t) =>
|
||||
t.name.endsWith("analyze_javascript_application"),
|
||||
);
|
||||
assert(analysis, "Gemini must expose the REA analysis tool");
|
||||
const prefix = analysis.name.slice(
|
||||
0,
|
||||
-"analyze_javascript_application".length,
|
||||
);
|
||||
const expected = TOOL_CONTRACTS.map((t) => prefix + t.name).sort();
|
||||
assert.deepEqual(
|
||||
tools
|
||||
.filter((t) => expected.includes(t.name))
|
||||
.map((t) => t.name)
|
||||
.sort(),
|
||||
expected,
|
||||
"Gemini must forward the complete REA tool catalog",
|
||||
);
|
||||
catalogSize = expected.length;
|
||||
if (schemasChecked === 0) {
|
||||
for (const tool of tools.filter((t) => expected.includes(t.name))) {
|
||||
assert(
|
||||
tool.parametersJsonSchema,
|
||||
`${tool.name}: missing advertised JSON Schema`,
|
||||
);
|
||||
assert(
|
||||
schemaValidator.validateSchema(tool.parametersJsonSchema),
|
||||
`${tool.name}: invalid advertised JSON Schema: ${JSON.stringify(schemaValidator.errors)}`,
|
||||
);
|
||||
schemasChecked++;
|
||||
}
|
||||
}
|
||||
const responses = body.contents
|
||||
.flatMap((c) => c.parts ?? [])
|
||||
.filter((p) => p.functionResponse)
|
||||
.map((p) => p.functionResponse);
|
||||
skillLoaded ||= responses.some(
|
||||
(r) =>
|
||||
r.name === "activate_skill" &&
|
||||
r.response?.output?.includes(
|
||||
`<activated_skill name="reverse-engineer-anything">`,
|
||||
) &&
|
||||
r.response.output.includes(
|
||||
join(profile, ".agents", "skills", "reverse-engineer-anything"),
|
||||
),
|
||||
);
|
||||
const observed = extractEvidence(responses);
|
||||
if (observed) {
|
||||
const evidence = toolContract(
|
||||
"analyze_javascript_application",
|
||||
).outputSchema.parse(observed);
|
||||
assert.equal(evidence.subject.local_path, target);
|
||||
const shape = evidence.normalized_result.graph.nodes
|
||||
.flatMap((n) => n.observations)
|
||||
.map((o) => o.properties)
|
||||
.find(
|
||||
(p) =>
|
||||
p.semantic_role === "export-return-shapes" &&
|
||||
p.exported_name === "result",
|
||||
);
|
||||
assert(shape?.return_shape_coverage?.projection_complete);
|
||||
const fields = shape.static_return_shapes.flatMap((s) => s.fields);
|
||||
assert(
|
||||
fields.some(
|
||||
(f) =>
|
||||
f.path === "/clientCompatibility" &&
|
||||
f.state === "literal" &&
|
||||
f.value === "REA_CLIENT_FIXTURE",
|
||||
),
|
||||
);
|
||||
assert(
|
||||
fields.some(
|
||||
(f) => f.path === "/count" && f.state === "literal" && f.value === 7,
|
||||
),
|
||||
);
|
||||
const bytes = JSON.stringify(evidence);
|
||||
artifactDigest = createHash("sha256").update(bytes).digest("hex");
|
||||
await writeFile(join(lab, "gemini-evidence.json"), bytes);
|
||||
evidenceSeen = true;
|
||||
}
|
||||
if (mode === "chat" || evidenceSeen) {
|
||||
finalSeen = true;
|
||||
return [{ text: "REA_CLIENT_COMPATIBILITY_OK" }];
|
||||
}
|
||||
if (!skillRequested) {
|
||||
const skill = tools.find((t) => t.name === "activate_skill");
|
||||
assert(skill, "Gemini must expose its native skill activation tool");
|
||||
skillRequested = true;
|
||||
return [
|
||||
{
|
||||
functionCall: {
|
||||
name: skill.name,
|
||||
args: { name: "reverse-engineer-anything" },
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
assert(skillLoaded, "Gemini must load the installed personal REA skill");
|
||||
return [
|
||||
{
|
||||
functionCall: {
|
||||
name: analysis.name,
|
||||
args: { input_path: target, format: "directory" },
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
let failure;
|
||||
try {
|
||||
const response = await exec(
|
||||
command,
|
||||
[
|
||||
"--model",
|
||||
"gemini-2.5-flash",
|
||||
"--approval-mode",
|
||||
mode === "chat" ? "default" : "yolo",
|
||||
"--allowed-mcp-server-names",
|
||||
"rea",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"-p",
|
||||
mode === "chat"
|
||||
? "Say hello briefly."
|
||||
: `Use REA to analyze the JavaScript directory ${target}. Report the result.`,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...environment,
|
||||
GEMINI_API_KEY: "local-fixture-key",
|
||||
GOOGLE_GEMINI_BASE_URL: fixture.baseUrl,
|
||||
},
|
||||
cwd: workspace,
|
||||
timeout: 120_000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
await writeFile(join(lab, `${mode}.stdout`), response.stdout);
|
||||
await writeFile(join(lab, `${mode}.stderr`), response.stderr);
|
||||
const events = response.stdout
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line));
|
||||
assert.equal(events.findLast((e) => e.type === "result")?.status, "success");
|
||||
assert.equal(fixture.failure, undefined);
|
||||
assert(finalSeen && response.stdout.includes("REA_CLIENT_COMPATIBILITY_OK"));
|
||||
if (mode === "call") assert(evidenceSeen && skillLoaded);
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
if (typeof cause?.stdout === "string")
|
||||
await writeFile(join(lab, `${mode}.stdout`), cause.stdout);
|
||||
if (typeof cause?.stderr === "string")
|
||||
await writeFile(join(lab, `${mode}.stderr`), cause.stderr);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
const lineage = await completeVerifierRun(verifier);
|
||||
const receipt = {
|
||||
client: "Gemini CLI",
|
||||
clientVersion,
|
||||
mode,
|
||||
passed: failure === undefined,
|
||||
artifacts: lab,
|
||||
runtimeRoot,
|
||||
host: {
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
node: process.versions.node,
|
||||
},
|
||||
modelFixture:
|
||||
"loopback native Gemini API adapter; token counts synthetic; no live Google API claim",
|
||||
skillDiscovery:
|
||||
"native GEMINI_CLI_HOME personal shared directory; isolated Git project",
|
||||
catalogSize,
|
||||
schemasChecked,
|
||||
evidenceSeen,
|
||||
skillLoaded,
|
||||
artifactDigest,
|
||||
finalSeen,
|
||||
requests: fixture.requests,
|
||||
probes: fixture.probes,
|
||||
fixtureFailure: fixture.failure,
|
||||
verifier: lineage,
|
||||
};
|
||||
await writeFile(join(lab, "receipt.json"), JSON.stringify(receipt, null, 2));
|
||||
console.log(JSON.stringify(receipt));
|
||||
}
|
||||
if (failure !== undefined) throw failure;
|
||||
@@ -0,0 +1,539 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import { isAbsolute, join, relative } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import {
|
||||
TOOL_CONTRACTS,
|
||||
toolContract,
|
||||
} from "../dist/contracts/toolContracts.js";
|
||||
import { exec } from "./lib/verify-package-core.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs";
|
||||
|
||||
assert(
|
||||
process.platform !== "win32",
|
||||
"Native Grok verifier requires POSIX; Windows is unverified",
|
||||
);
|
||||
const repo = fileURLToPath(new URL("..", import.meta.url));
|
||||
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
|
||||
const command = process.env.REA_VERIFY_GROK_COMMAND ?? "grok";
|
||||
const mode = process.argv[2] ?? "call";
|
||||
assert(
|
||||
["chat", "call"].includes(mode),
|
||||
"Usage: verify-grok-client.mjs [chat|call]",
|
||||
);
|
||||
const verifier = createVerifierRun();
|
||||
const lab = await mkdtemp(join(tmpdir(), "rea-grok-client-"));
|
||||
const account = join(lab, "account");
|
||||
const profile = join(lab, "selected profile");
|
||||
const workspace = join(lab, "workspace");
|
||||
const configPath = join(profile, "config.toml");
|
||||
const skillDirectory = join(
|
||||
account,
|
||||
".agents",
|
||||
"skills",
|
||||
"reverse-engineer-anything",
|
||||
);
|
||||
await Promise.all([mkdir(account), mkdir(profile), mkdir(workspace)]);
|
||||
await exec("git", ["init", "--quiet", workspace], { timeout: 30_000 });
|
||||
// Native compatibility discovery is independent of GROK_HOME. Disable those
|
||||
// sources and ignore the real shared skill root instead of changing HOME.
|
||||
const environment = {
|
||||
PATH: process.env.PATH,
|
||||
NODE_OPTIONS: process.env.NODE_OPTIONS,
|
||||
USERPROFILE: account,
|
||||
GROK_HOME: profile,
|
||||
GROK_DISABLE_AUTOUPDATER: "1",
|
||||
...Object.fromEntries(
|
||||
["CLAUDE", "CURSOR"].flatMap((vendor) =>
|
||||
["SKILLS", "RULES", "AGENTS", "MCPS", "HOOKS", "SESSIONS"].map((cell) => [
|
||||
`GROK_${vendor}_${cell}_ENABLED`,
|
||||
"false",
|
||||
]),
|
||||
),
|
||||
),
|
||||
XDG_DATA_HOME: join(lab, "data"),
|
||||
XDG_CONFIG_HOME: join(lab, "config"),
|
||||
XDG_CACHE_HOME: join(lab, "cache"),
|
||||
REA_PROCESS_RUN_ID: verifier.run_id,
|
||||
};
|
||||
const target = join(lab, "fixture α测试");
|
||||
await mkdir(target);
|
||||
await writeFile(
|
||||
join(target, "app.js"),
|
||||
'export function result() { return { clientCompatibility: "REA_CLIENT_FIXTURE", count: 7 }; }\n' +
|
||||
// A real analysis result large enough to exercise native MCP offloading.
|
||||
Array.from(
|
||||
{ length: 60 },
|
||||
(_, index) =>
|
||||
`export function extra${index}() { return { index: ${index}, tag: "GROK_LARGE_FIXTURE" }; }`,
|
||||
).join("\n"),
|
||||
);
|
||||
const expectedNames = TOOL_CONTRACTS.map((tool) => "rea__" + tool.name).sort();
|
||||
assert(
|
||||
expectedNames.length <= 255,
|
||||
"Native search_tool limit is uint8; this full-catalog query cannot cover more than 255 tools",
|
||||
);
|
||||
const schemaValidator = new Ajv2020({ strict: false, validateFormats: false });
|
||||
const shellQuote = (value) => "'" + value.replaceAll("'", "'\"'\"'") + "'";
|
||||
const containsText = (value, text) => {
|
||||
if (typeof value === "string") return value.includes(text);
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === "object" &&
|
||||
Object.values(value).some((item) => containsText(item, text))
|
||||
);
|
||||
};
|
||||
let clientVersion;
|
||||
let expectedSkill;
|
||||
let turn = 0;
|
||||
let skillRequested = false;
|
||||
let skillLoaded = false;
|
||||
let diagnosticsRequested = false;
|
||||
let diagnosticsVerified = false;
|
||||
let lastTool;
|
||||
let catalogChecked = false;
|
||||
let schemasChecked = 0;
|
||||
let analysisRequested = false;
|
||||
let evidenceSeen = false;
|
||||
let spillQueryRequested = false;
|
||||
let projectionSeen = false;
|
||||
let artifactDigest;
|
||||
let artifactBytes;
|
||||
let retainedArtifact;
|
||||
let expectedProjection;
|
||||
let finalSeen = false;
|
||||
let failure;
|
||||
const call = (name, args) => {
|
||||
lastTool = name;
|
||||
return {
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: `call_grok_${turn}`,
|
||||
type: "function",
|
||||
function: { name, arguments: JSON.stringify(args) },
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
const verifyEvidence = (bytes) => {
|
||||
const evidence = toolContract(
|
||||
"analyze_javascript_application",
|
||||
).outputSchema.parse(JSON.parse(bytes.toString("utf8")));
|
||||
assert.equal(evidence.subject.local_path, target);
|
||||
const shapes = evidence.normalized_result.graph.nodes
|
||||
.flatMap((node) => node.observations)
|
||||
.map((observation) => observation.properties)
|
||||
.filter(
|
||||
(properties) =>
|
||||
properties.semantic_role === "export-return-shapes" &&
|
||||
properties.exported_name === "result",
|
||||
);
|
||||
assert(
|
||||
shapes.length > 0 &&
|
||||
shapes.every((shape) => shape.return_shape_coverage?.projection_complete),
|
||||
);
|
||||
const fields = shapes.flatMap((shape) =>
|
||||
shape.static_return_shapes.flatMap((item) => item.fields),
|
||||
);
|
||||
assert(
|
||||
fields.some(
|
||||
(field) =>
|
||||
field.path === "/clientCompatibility" &&
|
||||
field.state === "literal" &&
|
||||
field.value === "REA_CLIENT_FIXTURE",
|
||||
),
|
||||
);
|
||||
assert(
|
||||
fields.some(
|
||||
(field) =>
|
||||
field.path === "/count" &&
|
||||
field.state === "literal" &&
|
||||
field.value === 7,
|
||||
),
|
||||
);
|
||||
artifactDigest = createHash("sha256").update(bytes).digest("hex");
|
||||
artifactBytes = bytes.length;
|
||||
evidenceSeen = true;
|
||||
return {
|
||||
evidence_id: evidence.evidence_id,
|
||||
subject: evidence.subject,
|
||||
operation: evidence.operation,
|
||||
artifact_sha256: artifactDigest,
|
||||
byte_length: artifactBytes,
|
||||
shapes,
|
||||
};
|
||||
};
|
||||
const diagnosticFile = join(lab, "native-doctor.json");
|
||||
const diagnosticCode =
|
||||
"const fs=require('node:fs');const d=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));console.log(JSON.stringify({rea_client_diagnostic:{healthy:d.healthy,registrations:d.identity.registrations.filter(r=>r.client==='grok_build'),skill:d.identity.skill.state}}));";
|
||||
// Query the saved result through a native agent command. Only the facts needed
|
||||
// for this analyst question enter context; the complete named-schema Evidence
|
||||
// and its digest remain available in the native artifact.
|
||||
const recoveryCode =
|
||||
"const fs=require('node:fs'),crypto=require('node:crypto');const bytes=fs.readFileSync(process.argv[1]),e=JSON.parse(bytes);const shapes=e.normalized_result.graph.nodes.flatMap(n=>n.observations).map(o=>o.properties).filter(p=>p.semantic_role==='export-return-shapes'&&p.exported_name==='result');console.log(JSON.stringify({rea_recovered:{evidence_id:e.evidence_id,subject:e.subject,operation:e.operation,artifact_sha256:crypto.createHash('sha256').update(bytes).digest('hex'),byte_length:bytes.length,shapes}}));";
|
||||
const fixture = await createOpenAiModelFixture({
|
||||
directory: lab,
|
||||
prefix: "grok",
|
||||
onRequest: async ({ body, tools }) => {
|
||||
// Native title/summary requests cannot execute the analyst workflow.
|
||||
if (!tools.some((tool) => tool.name === "search_tool"))
|
||||
return { role: "assistant", content: "REA fixture auxiliary response" };
|
||||
turn++;
|
||||
assert(
|
||||
containsText(body.messages, skillDirectory),
|
||||
"Grok must discover the isolated installed skill",
|
||||
);
|
||||
assert(
|
||||
!containsText(body.messages, join(homedir(), ".agents", "skills")),
|
||||
"Do not inherit real personal skills",
|
||||
);
|
||||
if (mode === "chat") {
|
||||
finalSeen = true;
|
||||
return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
|
||||
}
|
||||
const replies = body.messages.filter((message) => message.role === "tool");
|
||||
if (!skillRequested) {
|
||||
skillRequested = true;
|
||||
return call("read_file", {
|
||||
target_file: join(skillDirectory, "SKILL.md"),
|
||||
});
|
||||
}
|
||||
assert(
|
||||
expectedSkill !== undefined && containsText(replies, expectedSkill),
|
||||
"Native read_file must return the full skill with its documented line-number rendering",
|
||||
);
|
||||
skillLoaded = true;
|
||||
const last = replies.at(-1)?.content;
|
||||
assert.equal(typeof last, "string");
|
||||
if (!catalogChecked) {
|
||||
const discovery =
|
||||
lastTool === "search_tool" ? JSON.parse(last) : undefined;
|
||||
if (discovery?.status === "partial" && !diagnosticsRequested) {
|
||||
diagnosticsRequested = true;
|
||||
const diagnostic = [
|
||||
process.execPath,
|
||||
join(runtimeRoot, "scripts/rea.mjs"),
|
||||
"doctor",
|
||||
"--client",
|
||||
"grok_build",
|
||||
"--skill",
|
||||
"--json",
|
||||
]
|
||||
.map(shellQuote)
|
||||
.join(" ");
|
||||
const projection = [
|
||||
process.execPath,
|
||||
"-e",
|
||||
diagnosticCode,
|
||||
diagnosticFile,
|
||||
]
|
||||
.map(shellQuote)
|
||||
.join(" ");
|
||||
return call("run_terminal_command", {
|
||||
command: `${diagnostic} > ${shellQuote(diagnosticFile)} && ${projection}`,
|
||||
description:
|
||||
"Diagnose REA registration while native MCP discovery is pending",
|
||||
});
|
||||
}
|
||||
if (lastTool === "run_terminal_command") {
|
||||
const line = last
|
||||
.split("\n")
|
||||
.find((item) => item.startsWith('{"rea_client_diagnostic":'));
|
||||
assert(
|
||||
line,
|
||||
"Native REA diagnostics failed or did not return the selected status",
|
||||
);
|
||||
const diagnostic = JSON.parse(line).rea_client_diagnostic;
|
||||
assert.equal(diagnostic.healthy, true);
|
||||
assert.equal(diagnostic.skill, "aligned");
|
||||
assert(
|
||||
diagnostic.registrations.some(
|
||||
(registration) =>
|
||||
registration.state === "aligned" &&
|
||||
registration.config_path === configPath,
|
||||
),
|
||||
);
|
||||
diagnosticsVerified = true;
|
||||
}
|
||||
if (discovery?.status !== "ready")
|
||||
return call("search_tool", { query: "rea", limit: 255 });
|
||||
const inventory = discovery.results
|
||||
.filter((result) => result.server === "rea")
|
||||
.flatMap((result) => result.tools);
|
||||
assert.deepEqual(
|
||||
inventory.map((tool) => tool.tool_name).sort(),
|
||||
expectedNames,
|
||||
"Native search_tool must discover the complete REA catalog",
|
||||
);
|
||||
for (const tool of inventory) {
|
||||
assert(tool.input_schema, `${tool.tool_name}: missing input schema`);
|
||||
assert(
|
||||
schemaValidator.validateSchema(tool.input_schema),
|
||||
`${tool.tool_name}: invalid advertised schema: ${JSON.stringify(schemaValidator.errors)}`,
|
||||
);
|
||||
schemasChecked++;
|
||||
}
|
||||
await writeFile(join(lab, "native-discovery.json"), last);
|
||||
catalogChecked = true;
|
||||
analysisRequested = true;
|
||||
return call("use_tool", {
|
||||
tool_name: "rea__analyze_javascript_application",
|
||||
tool_input: { input_path: target, format: "directory" },
|
||||
});
|
||||
}
|
||||
if (!spillQueryRequested) {
|
||||
const marker = "Full output written to: ";
|
||||
const start = last.indexOf(marker);
|
||||
if (start < 0) {
|
||||
verifyEvidence(Buffer.from(last, "utf8"));
|
||||
finalSeen = true;
|
||||
return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
|
||||
}
|
||||
// The native hint explicitly calls for a query rather than read_file on
|
||||
// the saved JSON's very long line. Preserve the original hint in requests.
|
||||
const file = last
|
||||
.slice(start + marker.length)
|
||||
.split(". The full output ")[0];
|
||||
assert(isAbsolute(file));
|
||||
const childPath = relative(join(profile, "sessions"), file);
|
||||
assert(
|
||||
childPath !== "" &&
|
||||
!childPath.startsWith("..") &&
|
||||
!isAbsolute(childPath) &&
|
||||
file.endsWith(".json"),
|
||||
"Unexpected native MCP spill path",
|
||||
);
|
||||
retainedArtifact = file;
|
||||
expectedProjection = verifyEvidence(await readFile(file));
|
||||
spillQueryRequested = true;
|
||||
return call("run_terminal_command", {
|
||||
command: [process.execPath, "-e", recoveryCode, file]
|
||||
.map(shellQuote)
|
||||
.join(" "),
|
||||
description:
|
||||
"Query full saved REA Evidence while preserving artifact identity",
|
||||
});
|
||||
}
|
||||
const line = last
|
||||
.split("\n")
|
||||
.find((item) => item.startsWith('{"rea_recovered":'));
|
||||
assert(
|
||||
line,
|
||||
"The next model request must contain native artifact query output",
|
||||
);
|
||||
const projection = JSON.parse(line).rea_recovered;
|
||||
assert.deepEqual(
|
||||
projection,
|
||||
expectedProjection,
|
||||
"Native query must recover the observed facts and complete artifact digest",
|
||||
);
|
||||
projectionSeen = true;
|
||||
finalSeen = true;
|
||||
return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
|
||||
},
|
||||
});
|
||||
const q = JSON.stringify;
|
||||
const original = `# Keep unrelated settings and server state.
|
||||
disabled_mcp_servers = ["rea", "other"]
|
||||
[cli]
|
||||
auto_update = false
|
||||
[features]
|
||||
telemetry = "off"
|
||||
[skills]
|
||||
paths = [${q(join(account, ".agents", "skills"))}]
|
||||
ignore = [${q(join(homedir(), ".agents", "skills"))}]
|
||||
[model.rea-client-fixture]
|
||||
model = "rea-client-fixture"
|
||||
base_url = ${q(fixture.baseUrl)}
|
||||
api_key = "local-fixture-key"
|
||||
api_backend = "chat_completions"
|
||||
context_window = 1000000
|
||||
max_completion_tokens = 4096
|
||||
[mcp_servers.other]
|
||||
command = "unrelated-server"
|
||||
enabled = false
|
||||
`;
|
||||
await writeFile(configPath, original);
|
||||
const rea = async (args) =>
|
||||
exec(
|
||||
process.execPath,
|
||||
[join(runtimeRoot, "scripts/rea.mjs"), ...args, "--json"],
|
||||
{ env: environment, cwd: workspace, timeout: 60_000 },
|
||||
);
|
||||
try {
|
||||
clientVersion = (
|
||||
await exec(command, ["--version"], { env: environment, timeout: 30_000 })
|
||||
).stdout.trim();
|
||||
for (const args of [
|
||||
["setup", "--client", "grok_build", "--dry-run"],
|
||||
["setup", "--client", "grok_build", "--yes"],
|
||||
["doctor", "--client", "grok_build", "--skill"],
|
||||
]) {
|
||||
const result = await rea(args);
|
||||
await writeFile(
|
||||
join(lab, `${args[0]}${args.includes("--dry-run") ? "-plan" : ""}.json`),
|
||||
result.stdout,
|
||||
);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
if (args[0] === "doctor") assert.equal(parsed.healthy, true);
|
||||
else {
|
||||
assert.equal(
|
||||
parsed.status,
|
||||
args.includes("--dry-run") ? "planned" : "ready",
|
||||
);
|
||||
if (args.includes("--dry-run")) {
|
||||
assert(
|
||||
parsed.plannedActions.some((action) => action.target === configPath),
|
||||
);
|
||||
assert(
|
||||
parsed.plannedActions.some(
|
||||
(action) => action.target === skillDirectory,
|
||||
),
|
||||
);
|
||||
assert(
|
||||
parsed.plannedActions.every(
|
||||
(action) =>
|
||||
action.target === configPath ||
|
||||
action.target === skillDirectory ||
|
||||
action.target.startsWith(skillDirectory + "/"),
|
||||
),
|
||||
"Refuse writes outside the owned profile/skill",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
|
||||
const configured = await readFile(configPath, "utf8");
|
||||
assert(
|
||||
configured.includes("unrelated-server") &&
|
||||
configured.includes("# Keep unrelated settings"),
|
||||
);
|
||||
const repeated = JSON.parse(
|
||||
(await rea(["setup", "--client", "grok_build", "--yes"])).stdout,
|
||||
);
|
||||
assert.deepEqual(repeated.appliedActions, []);
|
||||
assert.equal(await readFile(configPath, "utf8"), configured);
|
||||
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
|
||||
const skill = await readFile(join(skillDirectory, "SKILL.md"), "utf8");
|
||||
expectedSkill = skill
|
||||
.trimEnd()
|
||||
.split("\n")
|
||||
.map((line, index) =>
|
||||
index === 0 || (index + 1) % 10 === 0 ? `${index + 1}→${line}` : line,
|
||||
)
|
||||
.join("\n");
|
||||
const execution = exec(
|
||||
command,
|
||||
[
|
||||
"-p",
|
||||
mode === "chat"
|
||||
? "Say hello briefly."
|
||||
: `Use the REA skill to analyze ${target}.`,
|
||||
"-m",
|
||||
"rea-client-fixture",
|
||||
"--output-format",
|
||||
"streaming-messages-json",
|
||||
"--always-approve",
|
||||
"--no-subagents",
|
||||
],
|
||||
{
|
||||
env: environment,
|
||||
cwd: workspace,
|
||||
timeout: 120_000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
execution.child.stdin.end();
|
||||
const response = await execution;
|
||||
await writeFile(join(lab, `${mode}.stdout`), response.stdout);
|
||||
await writeFile(join(lab, `${mode}.stderr`), response.stderr);
|
||||
assert.equal(fixture.failure, undefined);
|
||||
const events = response.stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
const init = events.find(
|
||||
(event) => event.type === "system" && event.subtype === "init",
|
||||
);
|
||||
assert(init?.mcp_servers?.some((server) => server.name === "rea"));
|
||||
assert(
|
||||
init.mcp_servers.every((server) => ["rea", "other"].includes(server.name)),
|
||||
"Do not inherit foreign MCP configuration",
|
||||
);
|
||||
assert(
|
||||
events.some(
|
||||
(event) =>
|
||||
event.type === "result" &&
|
||||
event.subtype === "success" &&
|
||||
event.is_error === false &&
|
||||
event.result === "REA_CLIENT_COMPATIBILITY_OK",
|
||||
),
|
||||
);
|
||||
assert(finalSeen);
|
||||
if (mode === "call")
|
||||
assert(
|
||||
skillLoaded &&
|
||||
catalogChecked &&
|
||||
analysisRequested &&
|
||||
evidenceSeen &&
|
||||
(!spillQueryRequested || projectionSeen) &&
|
||||
(!diagnosticsRequested || diagnosticsVerified),
|
||||
);
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
if (typeof cause?.stdout === "string")
|
||||
await writeFile(join(lab, `${mode}.stdout`), cause.stdout);
|
||||
if (typeof cause?.stderr === "string")
|
||||
await writeFile(join(lab, `${mode}.stderr`), cause.stderr);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
const lineage = await completeVerifierRun(verifier);
|
||||
const receipt = {
|
||||
client: "Grok Build",
|
||||
clientVersion,
|
||||
mode,
|
||||
passed: failure === undefined,
|
||||
artifacts: lab,
|
||||
runtimeRoot,
|
||||
host: {
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
node: process.versions.node,
|
||||
},
|
||||
modelFixture:
|
||||
"loopback OpenAI completions/SSE, declared one-million-token custom model and synthetic usage; no live model claim",
|
||||
discovery:
|
||||
"native GROK_HOME, isolated additional skill directory, disabled foreign compatibility sources; default OS-home skill discovery and Windows unverified",
|
||||
interface:
|
||||
"default native search_tool/use_tool with a complete explicit inventory query; asynchronous partial discovery may trigger native REA diagnosis; large MCP outputs are queried via native terminal",
|
||||
catalogChecked,
|
||||
catalogSize: catalogChecked ? expectedNames.length : 0,
|
||||
schemasChecked,
|
||||
skillLoaded,
|
||||
diagnosticsRequested,
|
||||
diagnosticsVerified,
|
||||
analysisRequested,
|
||||
evidenceSeen,
|
||||
spillQueryRequested,
|
||||
projectionSeen,
|
||||
retainedArtifact,
|
||||
artifactBytes,
|
||||
artifactDigest,
|
||||
finalSeen,
|
||||
evidenceScope:
|
||||
"complete saved Evidence validated with its named schema; the model receives the selected export facts, subject and full artifact digest, not every offloaded graph fact",
|
||||
requests: fixture.requests,
|
||||
probes: fixture.probes,
|
||||
fixtureFailure: fixture.failure,
|
||||
verifier: lineage,
|
||||
};
|
||||
await writeFile(join(lab, "receipt.json"), JSON.stringify(receipt, null, 2));
|
||||
console.log(JSON.stringify(receipt));
|
||||
}
|
||||
if (failure !== undefined) throw failure;
|
||||
@@ -0,0 +1,153 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { JebAnalysisService } from "../dist/application/jeb/JebAnalysisService.js";
|
||||
import { createJebAnalysisProvider } from "../dist/composition/jeb.js";
|
||||
|
||||
const run = promisify(execFile);
|
||||
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
if (options.help) {
|
||||
process.stdout.write(
|
||||
"Usage: npm run verify:jeb [-- --item-address ADDRESS --marker TEXT]\n" +
|
||||
"Requires a running JEB client serving MCP at REA_JEB_MCP_URL\n" +
|
||||
"(default http://127.0.0.1:8425/mcp) with a project already open.\n" +
|
||||
"JEB 5.48 headless instances open their target at start (--infile or\n" +
|
||||
"the documented programmatic launcher); open_jeb_project is verified\n" +
|
||||
"only when the engine advertises it. Without --item-address, a tiny\n" +
|
||||
"Java fixture is compiled and its known method is decompiled.\n",
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const provider = createJebAnalysisProvider(process.env);
|
||||
const service = new JebAnalysisService(provider);
|
||||
const fixtureDirectory = await mkdtemp(join(tmpdir(), "rea-jeb-"));
|
||||
try {
|
||||
const client = await service.execute("inspect_jeb_client", {});
|
||||
if (!client.ok) throw new Error(client.error.message);
|
||||
const engine = client.value.normalized_result.engine;
|
||||
if (engine.version === null)
|
||||
throw new Error("The JEB client did not report its version");
|
||||
|
||||
// JEB 5.48 headless clients do not advertise open_project; a project opened
|
||||
// at engine start is the supported target-selection route there. Attempt the
|
||||
// tool and record the engine's exact refusal when it is unavailable.
|
||||
let openRefusal = null;
|
||||
const target = await resolveTarget(options, fixtureDirectory);
|
||||
const opened = await service.execute("open_jeb_project", {
|
||||
path: target.path,
|
||||
});
|
||||
if (opened.ok) {
|
||||
const artifact = opened.value.normalized_result.input_files.find(
|
||||
(file) => typeof file === "object" && file !== null,
|
||||
);
|
||||
if (
|
||||
artifact === undefined ||
|
||||
!/^[0-9a-f]{64}$/u.test(artifact.contents_sha256_hash)
|
||||
)
|
||||
throw new Error("The engine did not report an input artifact digest");
|
||||
} else {
|
||||
openRefusal = opened.error.message;
|
||||
}
|
||||
|
||||
const units = await service.execute("list_jeb_units", { count: 100 });
|
||||
if (!units.ok) throw new Error(units.error.message);
|
||||
if (units.value.normalized_result.units.length === 0)
|
||||
throw new Error("The engine reported no project units");
|
||||
|
||||
const decompiled = await service.execute("decompile_jeb_item", {
|
||||
item_address: target.itemAddress,
|
||||
item_kind: "method",
|
||||
});
|
||||
if (!decompiled.ok) throw new Error(decompiled.error.message);
|
||||
const text = decompiled.value.normalized_result.text;
|
||||
if (!text.includes(target.marker))
|
||||
throw new Error(
|
||||
"Decompiled pseudo-code did not contain the fixture marker",
|
||||
);
|
||||
|
||||
const report = {
|
||||
ok: true,
|
||||
provider: "jeb",
|
||||
host_platform: process.platform,
|
||||
engine_version: engine.version,
|
||||
engine_gui: engine.gui_client,
|
||||
endpoint: engine.endpoint,
|
||||
open_project_observed: openRefusal === null,
|
||||
...(openRefusal === null ? {} : { open_project_refusal: openRefusal }),
|
||||
unit_count: units.value.normalized_result.units.length,
|
||||
decompiled_marker_observed: true,
|
||||
};
|
||||
process.stdout.write(`${JSON.stringify(report)}\n`);
|
||||
} catch (cause) {
|
||||
process.stderr.write(
|
||||
`JEB real-provider verification failed: ${cause instanceof Error ? cause.message : String(cause)}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
await provider.close().catch(() => undefined);
|
||||
await rm(fixtureDirectory, { recursive: true, force: true }).catch(
|
||||
() => undefined,
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveTarget(selected, directory) {
|
||||
if (selected.itemAddress !== undefined)
|
||||
return {
|
||||
path: "/engine-side/target",
|
||||
itemAddress: selected.itemAddress,
|
||||
marker: selected.marker ?? "",
|
||||
};
|
||||
await writeFile(
|
||||
join(directory, "Hello.java"),
|
||||
[
|
||||
"public class Hello {",
|
||||
" public static String greet(String name) {",
|
||||
' return "Hello, " + name + "!";',
|
||||
" }",
|
||||
"}",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
try {
|
||||
await run("javac", [join(directory, "Hello.java")]);
|
||||
} catch {
|
||||
throw new Error(
|
||||
"javac is required to build the default verification fixture",
|
||||
);
|
||||
}
|
||||
return {
|
||||
path: join(directory, "Hello.class"),
|
||||
itemAddress: "LHello;->greet(Ljava/lang/String;)Ljava/lang/String;",
|
||||
marker: "Hello, ",
|
||||
};
|
||||
}
|
||||
|
||||
function parseArguments(args) {
|
||||
if (args.length === 1 && args[0] === "--help") return { help: true };
|
||||
const values = new Map();
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const flag = args[index];
|
||||
const value = args[index + 1];
|
||||
if (
|
||||
(flag !== "--item-address" && flag !== "--marker") ||
|
||||
value === undefined
|
||||
)
|
||||
throw new Error(
|
||||
"Expected no arguments, or --item-address ADDRESS [--marker TEXT]",
|
||||
);
|
||||
values.set(flag, value);
|
||||
index += 1;
|
||||
}
|
||||
return {
|
||||
help: false,
|
||||
...(values.has("--item-address")
|
||||
? { itemAddress: values.get("--item-address") }
|
||||
: {}),
|
||||
...(values.has("--marker") ? { marker: values.get("--marker") } : {}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
TOOL_CONTRACTS,
|
||||
toolContract,
|
||||
} from "../dist/contracts/toolContracts.js";
|
||||
import { exec } from "./lib/verify-package-core.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs";
|
||||
|
||||
const client = process.argv[2];
|
||||
const mode = process.argv[3] ?? "call";
|
||||
assert(["pi", "hermes"].includes(client), "Select pi or hermes.");
|
||||
assert(["chat", "call"].includes(mode), "Select chat or call.");
|
||||
assert(
|
||||
process.platform !== "win32",
|
||||
"This real-client lane requires POSIX; native Windows is unverified.",
|
||||
);
|
||||
const repo = fileURLToPath(new URL("..", import.meta.url));
|
||||
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
|
||||
const command =
|
||||
process.env[`REA_VERIFY_${client.toUpperCase()}_COMMAND`] ?? client;
|
||||
const lab = await mkdtemp(join(tmpdir(), `rea-${client}-client-`));
|
||||
const account = join(lab, "account");
|
||||
const profile = join(lab, "selected profile");
|
||||
const stickyHermes =
|
||||
client === "hermes" && process.env.REA_VERIFY_HERMES_STICKY_PROFILE === "1";
|
||||
const selectedProfile = stickyHermes
|
||||
? join(profile, "profiles", "coder")
|
||||
: profile;
|
||||
const workspace = join(lab, "workspace");
|
||||
const target = join(workspace, "fixture α测试");
|
||||
const artifactPath = join(workspace, "evidence.json");
|
||||
const skillPath = join(
|
||||
client === "pi" ? join(account, ".agents") : selectedProfile,
|
||||
"skills",
|
||||
"reverse-engineer-anything",
|
||||
"SKILL.md",
|
||||
);
|
||||
const verifier = createVerifierRun();
|
||||
await Promise.all([
|
||||
mkdir(account),
|
||||
mkdir(profile, { recursive: true }),
|
||||
...(stickyHermes ? [mkdir(selectedProfile, { recursive: true })] : []),
|
||||
mkdir(target, { recursive: true }),
|
||||
]);
|
||||
await writeFile(
|
||||
join(target, "app.js"),
|
||||
'export function result() { return { clientCompatibility: "REA_CLIENT_FIXTURE", count: 7 }; }\n',
|
||||
);
|
||||
const environment = {
|
||||
PATH: process.env.PATH ?? "",
|
||||
USERPROFILE: account,
|
||||
[client === "pi" ? "PI_CODING_AGENT_DIR" : "HERMES_HOME"]: profile,
|
||||
PI_OFFLINE: "1",
|
||||
REA_PROCESS_RUN_ID: verifier.run_id,
|
||||
};
|
||||
const configPath = join(
|
||||
selectedProfile,
|
||||
client === "pi" ? "mcp.json" : "config.yaml",
|
||||
);
|
||||
if (stickyHermes) await writeFile(join(profile, "active_profile"), "coder\n");
|
||||
const original =
|
||||
client === "pi"
|
||||
? JSON.stringify({
|
||||
mcpServers: { other: { command: "unrelated-server", enabled: false } },
|
||||
})
|
||||
: "# Keep caller settings.\nmcp_servers:\n other:\n command: unrelated-server\n enabled: false\n";
|
||||
await writeFile(configPath, original);
|
||||
if (client === "pi") {
|
||||
// The CLI fixture account and native os.homedir() differ; use Pi's caller-owned skill setting.
|
||||
await writeFile(
|
||||
join(profile, "settings.json"),
|
||||
JSON.stringify({ skills: [join(account, ".agents", "skills")] }),
|
||||
);
|
||||
}
|
||||
const clientVersion = (
|
||||
await exec(command, ["--version"], { env: environment, timeout: 30_000 })
|
||||
).stdout.trim();
|
||||
for (const args of [
|
||||
["setup", "--client", client, "--dry-run"],
|
||||
["setup", "--client", client, "--yes"],
|
||||
["doctor", "--client", client, "--skill"],
|
||||
]) {
|
||||
const result = await exec(
|
||||
process.execPath,
|
||||
[join(runtimeRoot, "scripts/rea.mjs"), ...args, "--json"],
|
||||
{ env: environment, cwd: workspace, timeout: 60_000 },
|
||||
);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
if (args[0] === "doctor") assert.equal(parsed.healthy, true);
|
||||
else
|
||||
assert.equal(
|
||||
parsed.status,
|
||||
args.includes("--dry-run") ? "planned" : "ready",
|
||||
);
|
||||
await writeFile(
|
||||
join(lab, `${args[0]}${args.includes("--dry-run") ? "-plan" : ""}.json`),
|
||||
result.stdout,
|
||||
);
|
||||
}
|
||||
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
|
||||
const installedConfiguration = await readFile(configPath, "utf8");
|
||||
assert(installedConfiguration.includes("unrelated-server"));
|
||||
if (client === "hermes")
|
||||
assert(installedConfiguration.includes("# Keep caller settings."));
|
||||
const repeated = await exec(
|
||||
process.execPath,
|
||||
[
|
||||
join(runtimeRoot, "scripts/rea.mjs"),
|
||||
"setup",
|
||||
"--client",
|
||||
client,
|
||||
"--yes",
|
||||
"--json",
|
||||
],
|
||||
{ env: environment, cwd: workspace, timeout: 60_000 },
|
||||
);
|
||||
assert.deepEqual(JSON.parse(repeated.stdout).appliedActions, []);
|
||||
assert.equal(await readFile(configPath, "utf8"), installedConfiguration);
|
||||
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
|
||||
assert(
|
||||
(await readFile(skillPath, "utf8")).includes(
|
||||
"name: reverse-engineer-anything",
|
||||
),
|
||||
);
|
||||
let evidenceSeen = false;
|
||||
let skillLoaded = false;
|
||||
let skillRequested = false;
|
||||
let callRequested = false;
|
||||
let searchRequested = false;
|
||||
let describeRequested = false;
|
||||
let finalSeen = false;
|
||||
let catalog;
|
||||
let artifactDigest;
|
||||
const extractEvidence = (value) => {
|
||||
if (typeof value === "string") {
|
||||
if (
|
||||
value.startsWith("<untrusted_tool_result ") &&
|
||||
value.endsWith("</untrusted_tool_result>")
|
||||
) {
|
||||
// Hermes wraps external MCP content with its own instruction banner.
|
||||
// Decode only that observed envelope, retaining the original request artifact.
|
||||
const start = value.indexOf("\n\n");
|
||||
if (start >= 0)
|
||||
return extractEvidence(
|
||||
value
|
||||
.slice(start + 2, value.lastIndexOf("</untrusted_tool_result>"))
|
||||
.trim(),
|
||||
);
|
||||
}
|
||||
try {
|
||||
return extractEvidence(JSON.parse(value));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
if (
|
||||
typeof value.evidence_id === "string" &&
|
||||
value.normalized_result?.graph?.nodes
|
||||
)
|
||||
return value;
|
||||
for (const child of Object.values(value)) {
|
||||
const found = extractEvidence(child);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
};
|
||||
const validateEvidence = async (value) => {
|
||||
const evidence = toolContract(
|
||||
"analyze_javascript_application",
|
||||
).outputSchema.parse(value);
|
||||
assert.equal(evidence.subject.local_path, target);
|
||||
const shape = evidence.normalized_result.graph.nodes
|
||||
.flatMap((n) => n.observations)
|
||||
.map((o) => o.properties)
|
||||
.find(
|
||||
(p) =>
|
||||
p.semantic_role === "export-return-shapes" &&
|
||||
p.exported_name === "result",
|
||||
);
|
||||
assert(shape?.return_shape_coverage?.projection_complete);
|
||||
const fields = shape.static_return_shapes.flatMap((s) => s.fields);
|
||||
assert(
|
||||
fields.some(
|
||||
(f) =>
|
||||
f.path === "/clientCompatibility" &&
|
||||
f.state === "literal" &&
|
||||
f.value === "REA_CLIENT_FIXTURE",
|
||||
),
|
||||
);
|
||||
assert(
|
||||
fields.some(
|
||||
(f) => f.path === "/count" && f.state === "literal" && f.value === 7,
|
||||
),
|
||||
);
|
||||
const serialized = JSON.stringify(evidence);
|
||||
await writeFile(join(lab, "evidence.json"), serialized);
|
||||
artifactDigest = createHash("sha256").update(serialized).digest("hex");
|
||||
evidenceSeen = true;
|
||||
};
|
||||
const fixture = await createOpenAiModelFixture({
|
||||
directory: lab,
|
||||
prefix: `${client}-${mode}`,
|
||||
onRequest: async ({ body, tools, requestIndex }) => {
|
||||
assert(
|
||||
requestIndex <= 6,
|
||||
"Client did not complete the bounded fixture workflow.",
|
||||
);
|
||||
const messages = body.messages ?? [];
|
||||
const toolMessages = messages.filter((m) => m.role === "tool");
|
||||
const serializedTools = JSON.stringify(toolMessages);
|
||||
const call = (name, args) => ({
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: `call_rea_${requestIndex}`,
|
||||
type: "function",
|
||||
function: { name, arguments: JSON.stringify(args) },
|
||||
},
|
||||
],
|
||||
});
|
||||
if (
|
||||
client === "pi" &&
|
||||
callRequested &&
|
||||
serializedTools.includes("REA_CLIENT_FIXTURE")
|
||||
) {
|
||||
const fact = toolMessages
|
||||
.flatMap((m) => {
|
||||
const text =
|
||||
typeof m.content === "string"
|
||||
? m.content
|
||||
: JSON.stringify(m.content);
|
||||
const marker = text.indexOf('{"REA_CLIENT_FIXTURE":true');
|
||||
if (marker < 0) return [];
|
||||
const end = text.indexOf("}\n", marker);
|
||||
try {
|
||||
return [
|
||||
JSON.parse(text.slice(marker, end < 0 ? undefined : end + 1)),
|
||||
];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
.find((v) => Array.isArray(v.catalog));
|
||||
assert(
|
||||
fact,
|
||||
"Pi codemode must return its actual catalog and skill-read receipt.",
|
||||
);
|
||||
catalog = fact.catalog;
|
||||
skillLoaded = fact.skill_loaded === true;
|
||||
await validateEvidence(JSON.parse(await readFile(artifactPath, "utf8")));
|
||||
} else if (client === "hermes") {
|
||||
const search = tools.find((t) => t.name === "tool_search");
|
||||
const exposed = tools
|
||||
.filter((t) => t.name.startsWith("mcp__rea__"))
|
||||
.map((t) => t.name.slice(10));
|
||||
const deferred = [
|
||||
...(search?.description ?? "").matchAll(/^- mcp__rea__(\w+):/gmu),
|
||||
].map((m) => m[1]);
|
||||
const native = [...new Set([...exposed, ...deferred])];
|
||||
// Hermes advertises MCP prompt utilities beside server tools.
|
||||
if (native.length)
|
||||
catalog = native.filter(
|
||||
(name) => !["get_prompt", "list_prompts"].includes(name),
|
||||
);
|
||||
skillLoaded ||=
|
||||
serializedTools.includes(skillPath) &&
|
||||
serializedTools.includes("reverse-engineer-anything");
|
||||
for (const message of toolMessages) {
|
||||
const evidence = extractEvidence(message.content);
|
||||
if (evidence) await validateEvidence(evidence);
|
||||
}
|
||||
}
|
||||
if (mode === "chat" || evidenceSeen) {
|
||||
finalSeen = true;
|
||||
return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
|
||||
}
|
||||
if (client === "pi") {
|
||||
const tool = tools.find((t) => t.name === "codemode");
|
||||
assert(
|
||||
tool,
|
||||
`Native Pi codemode unavailable: ${tools.map((t) => t.name).join(", ")}`,
|
||||
);
|
||||
assert(
|
||||
JSON.stringify(messages).includes(skillPath),
|
||||
"Pi must advertise the installed REA skill.",
|
||||
);
|
||||
callRequested = true;
|
||||
return call(tool.name, {
|
||||
code: `await describeNamespace("rea"); const skill = await tools.read({path:${JSON.stringify(skillPath)}}); const result = await tools.mcp__rea__analyze_javascript_application({input_path:${JSON.stringify(target)},format:"directory"}); if(result.isError) throw new Error(JSON.stringify(result)); const evidence = result.structuredContent ?? JSON.parse(result.content.find(c=>c.type==="text").text); await tools.write({path:${JSON.stringify(artifactPath)},content:JSON.stringify(evidence)}); text({REA_CLIENT_FIXTURE:true,skill_loaded:typeof skill==="string"&&skill.includes("name: reverse-engineer-anything"),catalog:ALL_TOOLS.filter(t=>t.name.startsWith("mcp__rea__")).map(t=>t.name.slice(10))});`,
|
||||
});
|
||||
}
|
||||
if (!skillRequested) {
|
||||
const tool = tools.find((t) => t.name === "skill_view");
|
||||
assert(tool, "Hermes skill_view must be available.");
|
||||
skillRequested = true;
|
||||
return call(tool.name, { name: "reverse-engineer-anything" });
|
||||
}
|
||||
if (!searchRequested) {
|
||||
const tool = tools.find((t) => t.name === "tool_search");
|
||||
assert(tool, "Hermes native deferred tool_search must be available.");
|
||||
searchRequested = true;
|
||||
return call(tool.name, { queries: ["analyze_javascript_application"] });
|
||||
}
|
||||
assert(
|
||||
serializedTools.includes("mcp__rea__analyze_javascript_application"),
|
||||
"Hermes must discover the analysis tool.",
|
||||
);
|
||||
if (!describeRequested) {
|
||||
const tool = tools.find((t) => t.name === "tool_describe");
|
||||
assert(tool, "Hermes must load the actual parameter schema.");
|
||||
describeRequested = true;
|
||||
return call(tool.name, {
|
||||
names: ["mcp__rea__analyze_javascript_application"],
|
||||
});
|
||||
}
|
||||
const tool = tools.find((t) => t.name === "tool_call");
|
||||
assert(tool, "Hermes native deferred tool_call must be available.");
|
||||
callRequested = true;
|
||||
return call(tool.name, {
|
||||
calls: [
|
||||
{
|
||||
name: "mcp__rea__analyze_javascript_application",
|
||||
arguments: { input_path: target, format: "directory" },
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
if (client === "pi") {
|
||||
await writeFile(
|
||||
join(profile, "models.json"),
|
||||
JSON.stringify({
|
||||
providers: {
|
||||
"rea-fixture": {
|
||||
baseUrl: fixture.baseUrl,
|
||||
api: "openai-completions",
|
||||
apiKey: "local-fixture-key",
|
||||
models: [
|
||||
{
|
||||
id: "rea-client-fixture",
|
||||
reasoning: false,
|
||||
contextWindow: 200000,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const configured = await readFile(configPath, "utf8");
|
||||
await writeFile(
|
||||
configPath,
|
||||
configured +
|
||||
`\nmodel:\n default: rea-client-fixture\n provider: custom\n base_url: ${fixture.baseUrl}\nagent:\n max_turns: 6\n`,
|
||||
);
|
||||
}
|
||||
const args =
|
||||
client === "pi"
|
||||
? [
|
||||
"--offline",
|
||||
"--provider",
|
||||
"rea-fixture",
|
||||
"--model",
|
||||
"rea-client-fixture",
|
||||
"--no-session",
|
||||
"--no-context-files",
|
||||
"--mode",
|
||||
"json",
|
||||
"-p",
|
||||
mode === "chat"
|
||||
? "Say hello briefly."
|
||||
: `Use REA to analyze ${target}.`,
|
||||
]
|
||||
: [
|
||||
"--oneshot",
|
||||
mode === "chat"
|
||||
? "Say hello briefly."
|
||||
: `Use REA to analyze ${target}.`,
|
||||
"--provider",
|
||||
"custom",
|
||||
"--model",
|
||||
"rea-client-fixture",
|
||||
"--reasoning",
|
||||
"none",
|
||||
];
|
||||
let failure;
|
||||
try {
|
||||
const execution = exec(command, args, {
|
||||
env: {
|
||||
...environment,
|
||||
OPENAI_API_KEY: "local-fixture-key",
|
||||
OPENAI_BASE_URL: fixture.baseUrl,
|
||||
},
|
||||
cwd: workspace,
|
||||
timeout: 120_000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
// Pi consumes piped input before its first prompt; an unused open pipe is not EOF.
|
||||
execution.child.stdin.end();
|
||||
const result = await execution;
|
||||
await writeFile(join(lab, "client.stdout"), result.stdout);
|
||||
await writeFile(join(lab, "client.stderr"), result.stderr);
|
||||
assert.equal(fixture.failure, undefined);
|
||||
assert(finalSeen);
|
||||
assert(result.stdout.includes("REA_CLIENT_COMPATIBILITY_OK"));
|
||||
if (mode === "call") {
|
||||
assert(evidenceSeen);
|
||||
assert(
|
||||
skillLoaded,
|
||||
"Native client must actually read the installed skill.",
|
||||
);
|
||||
assert.deepEqual(
|
||||
catalog?.sort(),
|
||||
TOOL_CONTRACTS.map((t) => t.name).sort(),
|
||||
"The complete REA catalog must survive native client discovery.",
|
||||
);
|
||||
}
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
if (typeof cause?.stdout === "string")
|
||||
await writeFile(join(lab, "client.stdout"), cause.stdout);
|
||||
if (typeof cause?.stderr === "string")
|
||||
await writeFile(join(lab, "client.stderr"), cause.stderr);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
const receipt = {
|
||||
client,
|
||||
clientVersion,
|
||||
mode,
|
||||
passed: failure === undefined,
|
||||
artifacts: lab,
|
||||
runtimeRoot,
|
||||
host: {
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
node: process.versions.node,
|
||||
},
|
||||
modelFixture:
|
||||
"loopback deterministic OpenAI-compatible endpoint; no live provider claim",
|
||||
skillDiscovery:
|
||||
client === "pi"
|
||||
? "caller-configured isolated shared directory; default OS home discovery unverified"
|
||||
: "native HERMES_HOME personal directory",
|
||||
stickyHermes,
|
||||
catalogSize: catalog?.length,
|
||||
evidenceSeen,
|
||||
skillLoaded,
|
||||
artifactDigest,
|
||||
finalSeen,
|
||||
requests: fixture.requests,
|
||||
probes: fixture.probes,
|
||||
fixtureFailure: fixture.failure,
|
||||
verifier: await completeVerifierRun(verifier),
|
||||
};
|
||||
await writeFile(join(lab, "receipt.json"), JSON.stringify(receipt, null, 2));
|
||||
console.log(JSON.stringify(receipt));
|
||||
}
|
||||
if (failure !== undefined) throw failure;
|
||||
@@ -0,0 +1,441 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import { join, relative } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
TOOL_CONTRACTS,
|
||||
toolContract,
|
||||
} from "../dist/contracts/toolContracts.js";
|
||||
import { exec } from "./lib/verify-package-core.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs";
|
||||
|
||||
assert(
|
||||
process.platform !== "win32",
|
||||
"Native OMP verifier requires POSIX; Windows is unverified",
|
||||
);
|
||||
const repo = fileURLToPath(new URL("..", import.meta.url));
|
||||
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
|
||||
const command = process.env.REA_VERIFY_OMP_COMMAND ?? "omp";
|
||||
const mode = process.argv[2] ?? "call";
|
||||
assert(
|
||||
["chat", "call"].includes(mode),
|
||||
"Usage: verify-omp-client.mjs [chat|call]",
|
||||
);
|
||||
const verifier = createVerifierRun();
|
||||
const lab = await mkdtemp(join(tmpdir(), "rea-omp-client-"));
|
||||
const account = join(lab, "account");
|
||||
const agent = join(lab, "selected agent");
|
||||
const workspace = join(lab, "workspace");
|
||||
const configPath = join(agent, "mcp.json");
|
||||
const skillDirectory = join(
|
||||
account,
|
||||
".agents",
|
||||
"skills",
|
||||
"reverse-engineer-anything",
|
||||
);
|
||||
await Promise.all([
|
||||
mkdir(account),
|
||||
mkdir(agent),
|
||||
mkdir(workspace),
|
||||
...["data", "state", "cache"].map((name) =>
|
||||
mkdir(join(lab, name, "omp"), { recursive: true }),
|
||||
),
|
||||
]);
|
||||
await exec("git", ["init", "--quiet", workspace], { timeout: 30_000 });
|
||||
// OMP's global config root uses native homedir independently of the agent override.
|
||||
// A documented relative PI_CONFIG_DIR redirects it without changing HOME.
|
||||
const environment = {
|
||||
PATH: process.env.PATH,
|
||||
NODE_OPTIONS: process.env.NODE_OPTIONS,
|
||||
USERPROFILE: account,
|
||||
PI_CODING_AGENT_DIR: agent,
|
||||
PI_CONFIG_DIR: relative(homedir(), join(lab, "native-config")),
|
||||
OMP_PROFILE: "",
|
||||
XDG_DATA_HOME: join(lab, "data"),
|
||||
XDG_STATE_HOME: join(lab, "state"),
|
||||
XDG_CACHE_HOME: join(lab, "cache"),
|
||||
REA_PROCESS_RUN_ID: verifier.run_id,
|
||||
};
|
||||
const target = join(lab, "fixture α测试");
|
||||
await mkdir(target);
|
||||
await writeFile(
|
||||
join(target, "app.js"),
|
||||
'export function result() { return { clientCompatibility: "REA_CLIENT_FIXTURE", count: 7 }; }\n',
|
||||
);
|
||||
const expectedDevices = TOOL_CONTRACTS.map(
|
||||
(tool) => "mcp__rea_" + tool.name,
|
||||
).sort();
|
||||
const analysisDevice = "xd://mcp__rea_analyze_javascript_application";
|
||||
let clientVersion;
|
||||
let expectedSkillBody;
|
||||
let skillLoaded = false;
|
||||
let contractRead = false;
|
||||
let evidenceSeen = false;
|
||||
let artifactDigest;
|
||||
let finalSeen = false;
|
||||
let catalogMounted = false;
|
||||
let invalidProfileRejected = false;
|
||||
let failure;
|
||||
const containsText = (value, text) => {
|
||||
if (typeof value === "string") return value.includes(text);
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === "object" &&
|
||||
Object.values(value).some((item) => containsText(item, text))
|
||||
);
|
||||
};
|
||||
const call = (index, name, args) => ({
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: `call_omp_${index}`,
|
||||
type: "function",
|
||||
function: { name, arguments: JSON.stringify(args) },
|
||||
},
|
||||
],
|
||||
});
|
||||
const fixture = await createOpenAiModelFixture({
|
||||
directory: lab,
|
||||
prefix: "omp",
|
||||
onRequest: async ({ body, tools, requestIndex }) => {
|
||||
assert(tools.some((tool) => tool.name === "read"));
|
||||
assert(tools.some((tool) => tool.name === "write"));
|
||||
const instructions = body.messages.filter(
|
||||
(message) => message.role === "system",
|
||||
);
|
||||
for (const device of expectedDevices)
|
||||
assert(
|
||||
containsText(instructions, device),
|
||||
`OMP must advertise mounted device ${device}`,
|
||||
);
|
||||
if (mode === "chat") {
|
||||
finalSeen = true;
|
||||
return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
|
||||
}
|
||||
const replies = body.messages.filter((message) => message.role === "tool");
|
||||
if (requestIndex === 1)
|
||||
return call(requestIndex, "read", {
|
||||
i: "Load REA skill",
|
||||
path: "skill://reverse-engineer-anything:raw",
|
||||
});
|
||||
assert(
|
||||
expectedSkillBody !== undefined &&
|
||||
containsText(replies, expectedSkillBody),
|
||||
"OMP must return the complete installed skill body",
|
||||
);
|
||||
skillLoaded = true;
|
||||
if (requestIndex === 2)
|
||||
return call(requestIndex, "read", {
|
||||
i: "Inspect REA native tool contract",
|
||||
path: analysisDevice,
|
||||
});
|
||||
assert(containsText(replies, "# mcp__rea_analyze_javascript_application"));
|
||||
assert(
|
||||
containsText(replies, "input_path") &&
|
||||
containsText(replies, '"directory"'),
|
||||
);
|
||||
contractRead = true;
|
||||
if (requestIndex === 3)
|
||||
return call(requestIndex, "write", {
|
||||
path: analysisDevice,
|
||||
content: JSON.stringify({ input_path: target, format: "directory" }),
|
||||
});
|
||||
const evidenceReply = replies.find(
|
||||
(reply) =>
|
||||
typeof reply.content === "string" &&
|
||||
reply.content.startsWith('{"evidence_id":'),
|
||||
);
|
||||
assert(
|
||||
evidenceReply,
|
||||
"OMP must deliver the full analysis result to the next model turn",
|
||||
);
|
||||
const evidence = toolContract(
|
||||
"analyze_javascript_application",
|
||||
).outputSchema.parse(JSON.parse(evidenceReply.content));
|
||||
assert.equal(evidence.subject.local_path, target);
|
||||
const shape = evidence.normalized_result.graph.nodes
|
||||
.flatMap((node) => node.observations)
|
||||
.map((observation) => observation.properties)
|
||||
.find(
|
||||
(properties) =>
|
||||
properties.semantic_role === "export-return-shapes" &&
|
||||
properties.exported_name === "result",
|
||||
);
|
||||
assert(shape?.return_shape_coverage?.projection_complete);
|
||||
const fields = shape.static_return_shapes.flatMap((item) => item.fields);
|
||||
assert(
|
||||
fields.some(
|
||||
(field) =>
|
||||
field.path === "/clientCompatibility" &&
|
||||
field.state === "literal" &&
|
||||
field.value === "REA_CLIENT_FIXTURE",
|
||||
),
|
||||
);
|
||||
assert(
|
||||
fields.some(
|
||||
(field) =>
|
||||
field.path === "/count" &&
|
||||
field.state === "literal" &&
|
||||
field.value === 7,
|
||||
),
|
||||
);
|
||||
const bytes = JSON.stringify(evidence);
|
||||
artifactDigest = createHash("sha256").update(bytes).digest("hex");
|
||||
await writeFile(join(lab, "omp-evidence.json"), bytes);
|
||||
evidenceSeen = true;
|
||||
finalSeen = true;
|
||||
return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
|
||||
},
|
||||
});
|
||||
const original = JSON.stringify(
|
||||
{
|
||||
mcpServers: { other: { command: "unrelated-server", enabled: false } },
|
||||
disabledServers: ["rea", "other"],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
await writeFile(configPath, original);
|
||||
await writeFile(
|
||||
join(agent, "config.yml"),
|
||||
JSON.stringify({
|
||||
skills: { customDirectories: [join(account, ".agents", "skills")] },
|
||||
retry: { fallbackChains: { default: [] } },
|
||||
}),
|
||||
);
|
||||
await writeFile(
|
||||
join(agent, "models.yml"),
|
||||
JSON.stringify({
|
||||
providers: {
|
||||
"rea-fixture": {
|
||||
baseUrl: fixture.baseUrl,
|
||||
apiKey: "local-fixture-key",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "rea-client-fixture",
|
||||
name: "REA local fixture",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const rea = async (args, env = environment) =>
|
||||
exec(
|
||||
process.execPath,
|
||||
[join(runtimeRoot, "scripts/rea.mjs"), ...args, "--json"],
|
||||
{ env, cwd: workspace, timeout: 60_000 },
|
||||
);
|
||||
try {
|
||||
clientVersion = (
|
||||
await exec(command, ["--version"], { env: environment, timeout: 30_000 })
|
||||
).stdout.trim();
|
||||
const invalidEnvironment = { ...environment, OMP_PROFILE: "Bad Name" };
|
||||
const nativeRejected = await exec(
|
||||
command,
|
||||
["--print", "--no-session", "--no-title", "Hello"],
|
||||
{ env: invalidEnvironment, cwd: workspace, timeout: 30_000 },
|
||||
).then(
|
||||
() => undefined,
|
||||
(cause) => cause,
|
||||
);
|
||||
assert(
|
||||
nativeRejected?.code === 1 &&
|
||||
nativeRejected.stderr.includes('Invalid OMP profile "Bad Name"'),
|
||||
);
|
||||
await writeFile(
|
||||
join(lab, "invalid-profile-native.stderr"),
|
||||
nativeRejected.stderr,
|
||||
);
|
||||
const setupRejected = await rea(
|
||||
["setup", "--client", "omp", "--dry-run"],
|
||||
invalidEnvironment,
|
||||
).then(
|
||||
() => undefined,
|
||||
(cause) => cause,
|
||||
);
|
||||
assert(typeof setupRejected?.code === "number" && setupRejected.code !== 0);
|
||||
const rejected = JSON.parse(setupRejected.stdout);
|
||||
assert.equal(rejected.status, "needs_human");
|
||||
assert.deepEqual(rejected.plannedActions, []);
|
||||
assert.deepEqual(rejected.appliedActions, []);
|
||||
assert(rejected.remediation.includes('Invalid OMP profile "Bad Name"'));
|
||||
assert.equal(await readFile(configPath, "utf8"), original);
|
||||
await writeFile(
|
||||
join(lab, "invalid-profile-setup.json"),
|
||||
setupRejected.stdout,
|
||||
);
|
||||
invalidProfileRejected = true;
|
||||
for (const args of [
|
||||
["setup", "--client", "omp", "--dry-run"],
|
||||
["setup", "--client", "omp", "--yes"],
|
||||
["doctor", "--client", "omp", "--skill"],
|
||||
]) {
|
||||
const result = await rea(args);
|
||||
await writeFile(
|
||||
join(lab, `${args[0]}${args.includes("--dry-run") ? "-plan" : ""}.json`),
|
||||
result.stdout,
|
||||
);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
if (args[0] === "doctor") assert.equal(parsed.healthy, true);
|
||||
else {
|
||||
assert.equal(
|
||||
parsed.status,
|
||||
args.includes("--dry-run") ? "planned" : "ready",
|
||||
);
|
||||
if (args.includes("--dry-run")) {
|
||||
assert(
|
||||
parsed.plannedActions.some((action) => action.target === configPath),
|
||||
);
|
||||
assert(
|
||||
parsed.plannedActions.some(
|
||||
(action) => action.target === skillDirectory,
|
||||
),
|
||||
);
|
||||
assert(
|
||||
parsed.plannedActions.every(
|
||||
(action) =>
|
||||
action.target === configPath ||
|
||||
action.target === skillDirectory ||
|
||||
action.target.startsWith(skillDirectory + "/"),
|
||||
),
|
||||
"Refuse setup writes outside the owned config/skill",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
|
||||
const configured = await readFile(configPath, "utf8");
|
||||
const registration = JSON.parse(configured);
|
||||
assert.equal(registration.mcpServers.other.command, "unrelated-server");
|
||||
assert.deepEqual(registration.disabledServers, ["other"]);
|
||||
const repeated = JSON.parse(
|
||||
(await rea(["setup", "--client", "omp", "--yes"])).stdout,
|
||||
);
|
||||
assert.deepEqual(repeated.appliedActions, []);
|
||||
assert.equal(await readFile(configPath, "utf8"), configured);
|
||||
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
|
||||
expectedSkillBody = await readFile(join(skillDirectory, "SKILL.md"), "utf8");
|
||||
const execution = exec(
|
||||
command,
|
||||
[
|
||||
"--mode",
|
||||
"json",
|
||||
"--print",
|
||||
"--no-session",
|
||||
"--no-title",
|
||||
"--no-lsp",
|
||||
"--no-pty",
|
||||
"--approval-mode",
|
||||
"yolo",
|
||||
"--model",
|
||||
"rea-fixture/rea-client-fixture",
|
||||
mode === "chat"
|
||||
? "Say hello briefly."
|
||||
: `Use the REA skill to analyze ${target}.`,
|
||||
],
|
||||
{
|
||||
env: environment,
|
||||
cwd: workspace,
|
||||
timeout: 120_000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
execution.child.stdin.end();
|
||||
const response = await execution;
|
||||
await writeFile(join(lab, `${mode}.stdout`), response.stdout);
|
||||
await writeFile(join(lab, `${mode}.stderr`), response.stderr);
|
||||
assert.equal(fixture.failure, undefined);
|
||||
const events = response.stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
const mounted = events
|
||||
.filter(
|
||||
(event) =>
|
||||
event.type === "notice" && event.message?.startsWith("xd://: mounted "),
|
||||
)
|
||||
.flatMap((event) =>
|
||||
event.message.slice("xd://: mounted ".length).split(", "),
|
||||
)
|
||||
.filter((name) => name.startsWith("mcp__rea_"));
|
||||
assert.deepEqual(
|
||||
mounted.sort(),
|
||||
expectedDevices,
|
||||
"Native OMP must mount the complete REA catalog",
|
||||
);
|
||||
catalogMounted = true;
|
||||
assert(
|
||||
!events.some(
|
||||
(event) => event.type === "tool_execution_end" && event.isError,
|
||||
),
|
||||
);
|
||||
assert(
|
||||
events.some(
|
||||
(event) =>
|
||||
event.type === "message_end" &&
|
||||
event.message?.role === "assistant" &&
|
||||
event.message.stopReason === "stop" &&
|
||||
containsText(event.message.content, "REA_CLIENT_COMPATIBILITY_OK"),
|
||||
),
|
||||
"OMP must finish with the expected native assistant message",
|
||||
);
|
||||
assert(
|
||||
events.some((event) => event.type === "agent_end" && event.isTerminal),
|
||||
);
|
||||
assert(finalSeen);
|
||||
if (mode === "call") assert(skillLoaded && contractRead && evidenceSeen);
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
if (typeof cause?.stdout === "string")
|
||||
await writeFile(join(lab, `${mode}.stdout`), cause.stdout);
|
||||
if (typeof cause?.stderr === "string")
|
||||
await writeFile(join(lab, `${mode}.stderr`), cause.stderr);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
const lineage = await completeVerifierRun(verifier);
|
||||
const receipt = {
|
||||
client: "OMP",
|
||||
clientVersion,
|
||||
mode,
|
||||
passed: failure === undefined,
|
||||
artifacts: lab,
|
||||
runtimeRoot,
|
||||
host: {
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
node: process.versions.node,
|
||||
},
|
||||
modelFixture:
|
||||
"loopback OpenAI completions/SSE, caller-declared one-million-token model and synthetic usage; no live model claim",
|
||||
discovery:
|
||||
"explicit isolated default agent/global config/XDG roots and custom skill directory; named profiles, default OS-home skill discovery and Windows unverified",
|
||||
interface:
|
||||
"default native xd:// device metadata with read/write dispatch; complete JSON schemas are not forwarded as model functions",
|
||||
catalogMounted,
|
||||
catalogSize: expectedDevices.length,
|
||||
invalidProfileRejected,
|
||||
skillLoaded,
|
||||
contractRead,
|
||||
evidenceSeen,
|
||||
artifactDigest,
|
||||
finalSeen,
|
||||
requests: fixture.requests,
|
||||
probes: fixture.probes,
|
||||
fixtureFailure: fixture.failure,
|
||||
verifier: lineage,
|
||||
};
|
||||
await writeFile(join(lab, "receipt.json"), JSON.stringify(receipt, null, 2));
|
||||
console.log(JSON.stringify(receipt));
|
||||
}
|
||||
if (failure !== undefined) throw failure;
|
||||
@@ -0,0 +1,377 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import {
|
||||
TOOL_CONTRACTS,
|
||||
toolContract,
|
||||
} from "../dist/contracts/toolContracts.js";
|
||||
import { exec } from "./lib/verify-package-core.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs";
|
||||
|
||||
assert(
|
||||
process.platform !== "win32",
|
||||
"Native OpenCode verifier requires POSIX; Windows is unverified",
|
||||
);
|
||||
const repo = fileURLToPath(new URL("..", import.meta.url));
|
||||
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
|
||||
const command = process.env.REA_VERIFY_OPENCODE_COMMAND ?? "opencode";
|
||||
const mode = process.argv[2] ?? "call";
|
||||
const configFormat = process.env.REA_VERIFY_OPENCODE_CONFIG_FORMAT ?? "jsonc";
|
||||
assert(
|
||||
["chat", "call"].includes(mode),
|
||||
"Usage: verify-opencode-client.mjs [chat|call]",
|
||||
);
|
||||
assert(
|
||||
["json", "jsonc"].includes(configFormat),
|
||||
"REA_VERIFY_OPENCODE_CONFIG_FORMAT must be json or jsonc",
|
||||
);
|
||||
const verifier = createVerifierRun();
|
||||
const lab = await mkdtemp(join(tmpdir(), "rea-opencode-client-"));
|
||||
const account = join(lab, "account");
|
||||
const profile = join(lab, "selected profile");
|
||||
const workspace = join(lab, "workspace");
|
||||
const configPath = join(profile, `opencode.${configFormat}`);
|
||||
const skillDirectory = join(
|
||||
account,
|
||||
".agents",
|
||||
"skills",
|
||||
"reverse-engineer-anything",
|
||||
);
|
||||
await Promise.all([mkdir(account), mkdir(profile), mkdir(workspace)]);
|
||||
await exec("git", ["init", "--quiet", workspace], { timeout: 30_000 });
|
||||
const environment = {
|
||||
USERPROFILE: account,
|
||||
OPENCODE_CONFIG_DIR: profile,
|
||||
XDG_CONFIG_HOME: join(lab, "config"),
|
||||
XDG_CACHE_HOME: join(lab, "cache"),
|
||||
XDG_DATA_HOME: join(lab, "data"),
|
||||
XDG_STATE_HOME: join(lab, "state"),
|
||||
OPENCODE_DISABLE_AUTOUPDATE: "true",
|
||||
OPENCODE_DISABLE_MODELS_FETCH: "true",
|
||||
// Use the native core without external account plugins.
|
||||
OPENCODE_PURE: "true",
|
||||
REA_PROCESS_RUN_ID: verifier.run_id,
|
||||
};
|
||||
let clientVersion;
|
||||
try {
|
||||
clientVersion = (
|
||||
await exec(command, ["--version"], { env: environment, timeout: 30_000 })
|
||||
).stdout.trim();
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`OpenCode preflight failed for ${command}; install OpenCode or set REA_VERIFY_OPENCODE_COMMAND. ${String(cause)}`,
|
||||
);
|
||||
}
|
||||
const target = join(lab, "fixture α测试");
|
||||
await mkdir(target);
|
||||
await writeFile(
|
||||
join(target, "app.js"),
|
||||
'export function result() { return { clientCompatibility: "REA_CLIENT_FIXTURE", count: 7 }; }\n',
|
||||
);
|
||||
let skillRequested = false;
|
||||
let skillLoaded = false;
|
||||
let expectedSkillBody;
|
||||
let evidenceSeen = false;
|
||||
let finalSeen = false;
|
||||
let catalogSize = 0;
|
||||
let schemasChecked = 0;
|
||||
let artifactDigest;
|
||||
const schemaValidator = new Ajv2020({ strict: false, validateFormats: false });
|
||||
const extractEvidence = (value) => {
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
return extractEvidence(JSON.parse(value));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
if (
|
||||
typeof value.evidence_id === "string" &&
|
||||
value.normalized_result?.graph?.nodes?.length > 0
|
||||
)
|
||||
return value;
|
||||
for (const nested of Object.values(value)) {
|
||||
const evidence = extractEvidence(nested);
|
||||
if (evidence) return evidence;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const fixture = await createOpenAiModelFixture({
|
||||
directory: lab,
|
||||
prefix: "opencode",
|
||||
onRequest: async ({ body, tools, requestIndex }) => {
|
||||
// Native session-title requests do not execute the analyst's workflow.
|
||||
if (tools.length === 0)
|
||||
return { role: "assistant", content: "REA fixture title" };
|
||||
const analysis = tools.find((t) =>
|
||||
t.name.endsWith("analyze_javascript_application"),
|
||||
);
|
||||
assert(analysis, "OpenCode must expose the REA analysis tool");
|
||||
const prefix = analysis.name.slice(
|
||||
0,
|
||||
-"analyze_javascript_application".length,
|
||||
);
|
||||
const expected = TOOL_CONTRACTS.map((t) => prefix + t.name).sort();
|
||||
const reaTools = tools.filter((t) => expected.includes(t.name));
|
||||
assert.deepEqual(
|
||||
reaTools.map((t) => t.name).sort(),
|
||||
expected,
|
||||
"OpenCode must forward the complete REA catalog",
|
||||
);
|
||||
catalogSize = expected.length;
|
||||
if (schemasChecked === 0) {
|
||||
for (const tool of reaTools) {
|
||||
assert(
|
||||
tool.parameters,
|
||||
`${tool.name}: missing advertised input schema`,
|
||||
);
|
||||
assert(
|
||||
schemaValidator.validateSchema(tool.parameters),
|
||||
`${tool.name}: invalid input schema: ${JSON.stringify(schemaValidator.errors)}`,
|
||||
);
|
||||
schemasChecked++;
|
||||
}
|
||||
}
|
||||
const replies = body.messages.filter((m) => m.role === "tool");
|
||||
skillLoaded ||= replies.some(
|
||||
(m) =>
|
||||
typeof m.content === "string" &&
|
||||
expectedSkillBody !== undefined &&
|
||||
m.content.includes(skillDirectory) &&
|
||||
m.content.includes(expectedSkillBody),
|
||||
);
|
||||
const observed = extractEvidence(replies);
|
||||
if (observed) {
|
||||
const evidence = toolContract(
|
||||
"analyze_javascript_application",
|
||||
).outputSchema.parse(observed);
|
||||
assert.equal(evidence.subject.local_path, target);
|
||||
const shape = evidence.normalized_result.graph.nodes
|
||||
.flatMap((n) => n.observations)
|
||||
.map((o) => o.properties)
|
||||
.find(
|
||||
(p) =>
|
||||
p.semantic_role === "export-return-shapes" &&
|
||||
p.exported_name === "result",
|
||||
);
|
||||
assert(shape?.return_shape_coverage?.projection_complete);
|
||||
const fields = shape.static_return_shapes.flatMap((s) => s.fields);
|
||||
assert(
|
||||
fields.some(
|
||||
(f) =>
|
||||
f.path === "/clientCompatibility" &&
|
||||
f.state === "literal" &&
|
||||
f.value === "REA_CLIENT_FIXTURE",
|
||||
),
|
||||
);
|
||||
assert(
|
||||
fields.some(
|
||||
(f) => f.path === "/count" && f.state === "literal" && f.value === 7,
|
||||
),
|
||||
);
|
||||
const bytes = JSON.stringify(evidence);
|
||||
artifactDigest = createHash("sha256").update(bytes).digest("hex");
|
||||
await writeFile(join(lab, "opencode-evidence.json"), bytes);
|
||||
evidenceSeen = true;
|
||||
}
|
||||
if (mode === "chat" || evidenceSeen) {
|
||||
finalSeen = true;
|
||||
return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
|
||||
}
|
||||
assert(requestIndex <= 8, "Local model workflow exceeded its turn budget");
|
||||
const call = (name, args) => ({
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: `call_rea_fixture_${requestIndex}`,
|
||||
type: "function",
|
||||
function: { name, arguments: JSON.stringify(args) },
|
||||
},
|
||||
],
|
||||
});
|
||||
if (!skillRequested) {
|
||||
const skill = tools.find((t) => t.name === "skill");
|
||||
assert(skill, "OpenCode must expose its native skill tool");
|
||||
skillRequested = true;
|
||||
return call(skill.name, { name: "reverse-engineer-anything" });
|
||||
}
|
||||
assert(skillLoaded, "OpenCode must load the installed personal REA skill");
|
||||
return call(analysis.name, { input_path: target, format: "directory" });
|
||||
},
|
||||
});
|
||||
const original =
|
||||
(configFormat === "jsonc" ? "// Keep caller configuration.\n" : "") +
|
||||
JSON.stringify(
|
||||
{
|
||||
autoupdate: false,
|
||||
skills: { paths: [join(account, ".agents", "skills")] },
|
||||
permission: { "*": "allow" },
|
||||
provider: {
|
||||
"rea-fixture": {
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
name: "REA local model fixture",
|
||||
options: { baseURL: fixture.baseUrl, apiKey: "local-fixture-key" },
|
||||
models: {
|
||||
"rea-client-fixture": {
|
||||
name: "REA fixture",
|
||||
tool_call: true,
|
||||
limit: { context: 1_000_000, output: 4096 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mcp: {
|
||||
other: { type: "local", command: ["unrelated-server"], enabled: false },
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
await writeFile(configPath, original);
|
||||
let failure;
|
||||
try {
|
||||
for (const args of [
|
||||
["setup", "--client", "opencode", "--dry-run"],
|
||||
["setup", "--client", "opencode", "--yes"],
|
||||
["doctor", "--client", "opencode", "--skill"],
|
||||
]) {
|
||||
const result = await exec(
|
||||
process.execPath,
|
||||
[join(runtimeRoot, "scripts/rea.mjs"), ...args, "--json"],
|
||||
{ env: environment, cwd: workspace, timeout: 60_000 },
|
||||
);
|
||||
await writeFile(
|
||||
join(lab, `${args[0]}${args.includes("--dry-run") ? "-plan" : ""}.json`),
|
||||
result.stdout,
|
||||
);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
if (args[0] === "doctor") assert.equal(parsed.healthy, true);
|
||||
else {
|
||||
assert.equal(
|
||||
parsed.status,
|
||||
args.includes("--dry-run") ? "planned" : "ready",
|
||||
);
|
||||
if (args.includes("--dry-run")) {
|
||||
assert(parsed.plannedActions.some((a) => a.target === configPath));
|
||||
assert(parsed.plannedActions.some((a) => a.target === skillDirectory));
|
||||
assert(
|
||||
parsed.plannedActions.every(
|
||||
(a) =>
|
||||
a.target === configPath || a.target.startsWith(skillDirectory),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
|
||||
const configured = await readFile(configPath, "utf8");
|
||||
assert(configured.includes("unrelated-server"));
|
||||
if (configFormat === "jsonc")
|
||||
assert(configured.includes("// Keep caller configuration."));
|
||||
const repeated = await exec(
|
||||
process.execPath,
|
||||
[
|
||||
join(runtimeRoot, "scripts/rea.mjs"),
|
||||
"setup",
|
||||
"--client",
|
||||
"opencode",
|
||||
"--yes",
|
||||
"--json",
|
||||
],
|
||||
{ env: environment, cwd: workspace, timeout: 60_000 },
|
||||
);
|
||||
assert.deepEqual(JSON.parse(repeated.stdout).appliedActions, []);
|
||||
assert.equal(await readFile(configPath, "utf8"), configured);
|
||||
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
|
||||
const installedSkill = await readFile(
|
||||
join(skillDirectory, "SKILL.md"),
|
||||
"utf8",
|
||||
);
|
||||
assert(installedSkill.includes("name: reverse-engineer-anything"));
|
||||
const skillLines = installedSkill.split("\n");
|
||||
const headerEnd = skillLines.indexOf("---", 1);
|
||||
assert(
|
||||
skillLines[0] === "---" && headerEnd > 0,
|
||||
"Installed skill must have its authored frontmatter",
|
||||
);
|
||||
expectedSkillBody = skillLines
|
||||
.slice(headerEnd + 1)
|
||||
.join("\n")
|
||||
.trim();
|
||||
assert(expectedSkillBody.length > 0);
|
||||
const execution = exec(
|
||||
command,
|
||||
[
|
||||
"run",
|
||||
"--format",
|
||||
"json",
|
||||
"--model",
|
||||
"rea-fixture/rea-client-fixture",
|
||||
mode === "chat"
|
||||
? "Say hello briefly."
|
||||
: `Use REA to analyze the JavaScript directory ${target}. Report the result.`,
|
||||
],
|
||||
{
|
||||
env: environment,
|
||||
cwd: workspace,
|
||||
timeout: 120_000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
execution.child.stdin.end();
|
||||
const response = await execution;
|
||||
await writeFile(join(lab, `${mode}.stdout`), response.stdout);
|
||||
await writeFile(join(lab, `${mode}.stderr`), response.stderr);
|
||||
assert.equal(fixture.failure, undefined);
|
||||
assert(finalSeen && response.stdout.includes("REA_CLIENT_COMPATIBILITY_OK"));
|
||||
if (mode === "call") assert(evidenceSeen && skillLoaded);
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
if (typeof cause?.stdout === "string")
|
||||
await writeFile(join(lab, `${mode}.stdout`), cause.stdout);
|
||||
if (typeof cause?.stderr === "string")
|
||||
await writeFile(join(lab, `${mode}.stderr`), cause.stderr);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
const lineage = await completeVerifierRun(verifier);
|
||||
const receipt = {
|
||||
client: "OpenCode",
|
||||
clientVersion,
|
||||
mode,
|
||||
configFormat,
|
||||
passed: failure === undefined,
|
||||
artifacts: lab,
|
||||
runtimeRoot,
|
||||
host: {
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
node: process.versions.node,
|
||||
},
|
||||
modelFixture:
|
||||
"loopback custom OpenAI-compatible provider; one-million-token fixture capacity and synthetic usage; no live provider claim",
|
||||
skillDiscovery:
|
||||
"caller-configured isolated shared directory; default OS home discovery unverified",
|
||||
externalPlugins: "disabled through native OPENCODE_PURE",
|
||||
catalogSize,
|
||||
schemasChecked,
|
||||
evidenceSeen,
|
||||
skillLoaded,
|
||||
artifactDigest,
|
||||
finalSeen,
|
||||
requests: fixture.requests,
|
||||
probes: fixture.probes,
|
||||
fixtureFailure: fixture.failure,
|
||||
verifier: lineage,
|
||||
};
|
||||
await writeFile(join(lab, "receipt.json"), JSON.stringify(receipt, null, 2));
|
||||
console.log(JSON.stringify(receipt));
|
||||
}
|
||||
if (failure !== undefined) throw failure;
|
||||
@@ -85,7 +85,12 @@ export async function verifyPackageEnvironment({
|
||||
PI_CODING_AGENT_DIR: undefined,
|
||||
PI_CONFIG_DIR: undefined,
|
||||
PI_PROFILE: undefined,
|
||||
QWEN_HOME: undefined,
|
||||
OPENCODE_CONFIG: undefined,
|
||||
OPENCODE_CONFIG_DIR: undefined,
|
||||
GEMINI_CLI_HOME: undefined,
|
||||
GEMINI_CLI_SYSTEM_SETTINGS_PATH: undefined,
|
||||
GEMINI_CLI_SYSTEM_DEFAULTS_PATH: undefined,
|
||||
XDG_CONFIG_HOME: join(home, ".config"),
|
||||
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
|
||||
REA_ANALYSIS_PROVIDER: "auto",
|
||||
|
||||
@@ -13,7 +13,16 @@ export async function verifyPackageInstall({
|
||||
}) {
|
||||
await exec(
|
||||
"npm",
|
||||
["install", "--global", "--ignore-scripts", "--prefix", prefix, tarball],
|
||||
[
|
||||
"install",
|
||||
"--global",
|
||||
"--ignore-scripts",
|
||||
"--omit=dev",
|
||||
"--install-strategy=nested",
|
||||
"--prefix",
|
||||
prefix,
|
||||
tarball,
|
||||
],
|
||||
{ env: environment },
|
||||
);
|
||||
const cli = join(prefix, "bin", "rea");
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function verifyPackagePack({ root, workspace }) {
|
||||
throw new Error("package did not expose both rea command entry points");
|
||||
if (packedManifest.mcpName !== "io.github.morluto/rea")
|
||||
throw new Error("package did not retain its MCP Registry ownership marker");
|
||||
if (packedFiles.some((path) => path.startsWith("package/skill-src/")))
|
||||
if (packedFiles.some((path) => path.startsWith("package/.agents/")))
|
||||
throw new Error(
|
||||
"package included authored skill sources instead of only the generated bundle",
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ import { verifyPackageMcp } from "./verify-package-mcp.mjs";
|
||||
import { verifyPackagePack } from "./verify-package-pack.mjs";
|
||||
import { verifyPackagePlatform } from "./verify-package-platform.mjs";
|
||||
import { verifyPackageSetup } from "./verify-package-setup.mjs";
|
||||
import { verifyRepositorySkill } from "./verify-repository-skill.mjs";
|
||||
import { verifyManaged } from "./verify-package-managed.mjs";
|
||||
import { verifyUnknownProvider } from "./verify-package-unknown-provider.mjs";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
@@ -34,9 +35,11 @@ let tarball;
|
||||
let mcpStartup;
|
||||
let mcpModuleLoading;
|
||||
let update;
|
||||
let repositorySkill;
|
||||
let report;
|
||||
|
||||
try {
|
||||
repositorySkill = await verifyRepositorySkill({ root, workspace });
|
||||
({ tarball } = await verifyPackagePack({ root, workspace }));
|
||||
const environmentData = await verifyPackageEnvironment({
|
||||
root,
|
||||
@@ -156,6 +159,7 @@ try {
|
||||
configSymlinkLifecycle: supportedSetupHost,
|
||||
skill: supportedSetupHost,
|
||||
skillReferences: supportedSetupHost,
|
||||
repositorySkill,
|
||||
mcpTools: TOOL_CONTRACTS.length,
|
||||
mcpPrompts: prompts.names.length,
|
||||
promptCompletion: true,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
|
||||
import { verifyGhidraCompactReferences } from "./lib/real-ghidra-compact-references.mjs";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
@@ -171,7 +172,11 @@ const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
const runtimeParent = join(workspace, "runtime with spaces");
|
||||
await mkdir(runtimeParent);
|
||||
const callerDirectory = join(workspace, "caller cwd with spaces");
|
||||
const scriptCollisions = ["ReaGhidraBridge.java", "ReaGhidraPrepareCom.java"];
|
||||
const scriptCollisions = [
|
||||
"ReaGhidraBridge.java",
|
||||
"ReaGhidraPrepareCom.java",
|
||||
"ReaGhidraNoReturnFix.java",
|
||||
];
|
||||
await mkdir(callerDirectory);
|
||||
for (const name of scriptCollisions) await mkdir(join(callerDirectory, name));
|
||||
const environment = {
|
||||
@@ -358,6 +363,36 @@ try {
|
||||
await call("inspect_native_data_type", { type: "/undefined8" });
|
||||
await call("inspect_native_instruction", { address: procedure.address });
|
||||
await call("resolve_native_call_targets", { address: procedure.address });
|
||||
// Explicit caller-selected targets need not contain our fixture instructions.
|
||||
const controlledTargets = [
|
||||
"rea-ghidra-windows.exe",
|
||||
"rea-ghidra-windows-x86.exe",
|
||||
].map((name) => resolve("build", "fixtures", name));
|
||||
if (controlledTargets.includes(target))
|
||||
report.compactReferences = await verifyGhidraCompactReferences(
|
||||
call,
|
||||
target === controlledTargets[1],
|
||||
async (command, address) => {
|
||||
const result = await exec(
|
||||
process.execPath,
|
||||
[entry, command, target, address, "--provider", "ghidra", "--json"],
|
||||
{
|
||||
env: environment,
|
||||
cwd: callerDirectory,
|
||||
timeout: 360_000,
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
const evidence = JSON.parse(result.stdout);
|
||||
assert.ok(evidence.error === undefined, `CLI ${command} failed`);
|
||||
const contract = TOOL_CONTRACTS.find(
|
||||
({ name }) => name === command.replaceAll("-", "_"),
|
||||
);
|
||||
assert.ok(contract, `Missing CLI ${command} contract`);
|
||||
contract.outputSchema.parse(evidence);
|
||||
return evidence.normalized_result;
|
||||
},
|
||||
);
|
||||
const expected = (
|
||||
await import(
|
||||
pathToFileURL(join(packageRoot, "dist/ghidra/GhidraSessionValues.js"))
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs";
|
||||
import {
|
||||
mkdir,
|
||||
writeFile,
|
||||
readFile,
|
||||
mkdtemp,
|
||||
realpath,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createHash } from "node:crypto";
|
||||
import { exec } from "./lib/verify-package-core.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import {
|
||||
TOOL_CONTRACTS,
|
||||
toolContract,
|
||||
} from "../dist/contracts/toolContracts.js";
|
||||
|
||||
assert(
|
||||
process.platform !== "win32",
|
||||
"The Qwen real-client verifier currently requires a POSIX host; native Windows is unverified.",
|
||||
);
|
||||
const repo = fileURLToPath(new URL("..", import.meta.url));
|
||||
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
|
||||
const lab = await mkdtemp(join(tmpdir(), "rea-qwen-client-"));
|
||||
const client = "qwen";
|
||||
const mode = process.argv[2] ?? "call";
|
||||
assert(
|
||||
["chat", "call"].includes(mode),
|
||||
"Usage: node scripts/verify-qwen-client.mjs [chat|call]",
|
||||
);
|
||||
const command = process.env.REA_VERIFY_QWEN_COMMAND ?? "qwen";
|
||||
const verifier = createVerifierRun();
|
||||
const profile = join(lab, "selected profile");
|
||||
const account = join(lab, "account");
|
||||
const runtime = join(profile, "runtime");
|
||||
const workspace = join(lab, "workspace");
|
||||
const sharedSkills = join(account, ".agents", "skills");
|
||||
await Promise.all([
|
||||
mkdir(profile, { recursive: true }),
|
||||
mkdir(account),
|
||||
mkdir(workspace),
|
||||
]);
|
||||
const baseEnvironment = {
|
||||
PATH: process.env.PATH ?? "",
|
||||
USERPROFILE: account,
|
||||
QWEN_HOME: profile,
|
||||
QWEN_RUNTIME_DIR: runtime,
|
||||
REA_PROCESS_RUN_ID: verifier.run_id,
|
||||
};
|
||||
let clientVersion;
|
||||
try {
|
||||
clientVersion = (
|
||||
await exec(command, ["--version"], {
|
||||
env: baseEnvironment,
|
||||
timeout: 30_000,
|
||||
})
|
||||
).stdout.trim();
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`Qwen client preflight failed for ${command}; install Qwen Code or set REA_VERIFY_QWEN_COMMAND. ${String(cause)}`,
|
||||
);
|
||||
}
|
||||
const original = JSON.stringify(
|
||||
{
|
||||
security: {
|
||||
auth: { selectedType: "openai" },
|
||||
folderTrust: { enabled: false },
|
||||
},
|
||||
telemetry: { enabled: false },
|
||||
model: { name: "rea-client-fixture" },
|
||||
// Qwen uses os.homedir(), while REA's isolated CLI account uses USERPROFILE.
|
||||
// This caller-owned setting lets the real client load the REA-installed bundle.
|
||||
skills: { directories: [sharedSkills] },
|
||||
mcpServers: { other: { command: "unrelated-server", disabled: true } },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
await writeFile(join(profile, "settings.json"), original);
|
||||
for (const step of [
|
||||
["setup", "--client", "qwen_code", "--dry-run"],
|
||||
["setup", "--client", "qwen_code", "--yes"],
|
||||
["doctor", "--client", "qwen_code", "--skill"],
|
||||
]) {
|
||||
const response = await exec(
|
||||
process.execPath,
|
||||
[join(runtimeRoot, "scripts/rea.mjs"), ...step, "--json"],
|
||||
{ env: baseEnvironment, cwd: repo, timeout: 60_000 },
|
||||
);
|
||||
const result = JSON.parse(response.stdout);
|
||||
if (step.includes("--dry-run"))
|
||||
assert(
|
||||
result.plannedActions.every(
|
||||
(a) =>
|
||||
a.target.startsWith(profile + "/") ||
|
||||
a.target.startsWith(sharedSkills + "/"),
|
||||
),
|
||||
);
|
||||
await writeFile(
|
||||
join(lab, `${step[0]}${step.includes("--dry-run") ? "-plan" : ""}.json`),
|
||||
response.stdout,
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
await readFile(join(profile, "settings.json.rea.backup"), "utf8"),
|
||||
original,
|
||||
);
|
||||
const configured = JSON.parse(
|
||||
await readFile(join(profile, "settings.json"), "utf8"),
|
||||
);
|
||||
assert.deepEqual(configured.model, { name: "rea-client-fixture" });
|
||||
assert.deepEqual(configured.skills, { directories: [sharedSkills] });
|
||||
assert.deepEqual(configured.mcpServers.other, {
|
||||
command: "unrelated-server",
|
||||
disabled: true,
|
||||
});
|
||||
const target = join(lab, "fixture α测试");
|
||||
await mkdir(target, { recursive: true });
|
||||
await writeFile(
|
||||
`${target}/app.js`,
|
||||
'export function result() { return { clientCompatibility: "REA_CLIENT_FIXTURE", count: 7 }; }\n',
|
||||
);
|
||||
let evidenceSeen = false;
|
||||
let skillSeen = false;
|
||||
let finalSeen = false;
|
||||
let skillRequested = false;
|
||||
let searchRequested = false;
|
||||
let artifactReadByAgent = false;
|
||||
let artifactPath;
|
||||
let selectedSkillLoaded = false;
|
||||
let artifactDigest;
|
||||
const extractEvidence = (value) => {
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
return extractEvidence(JSON.parse(value));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
const result = extractEvidence(item);
|
||||
if (result) return result;
|
||||
}
|
||||
} else if (value && typeof value === "object") {
|
||||
if (
|
||||
typeof value.evidence_id === "string" &&
|
||||
value.normalized_result?.graph?.nodes?.length > 0
|
||||
)
|
||||
return value;
|
||||
for (const nested of Object.values(value)) {
|
||||
const result = extractEvidence(nested);
|
||||
if (result) return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const fixture = await createOpenAiModelFixture({
|
||||
directory: lab,
|
||||
prefix: `${client}-${mode}`,
|
||||
onRequest: async ({ body, tools, requestIndex }) => {
|
||||
skillSeen ||= JSON.stringify(body.messages ?? []).includes(
|
||||
"reverse-engineer-anything",
|
||||
);
|
||||
selectedSkillLoaded ||= JSON.stringify(body.messages ?? []).includes(
|
||||
join(sharedSkills, "reverse-engineer-anything"),
|
||||
);
|
||||
const toolText = JSON.stringify(
|
||||
(body.messages ?? []).filter((m) => m.role === "tool"),
|
||||
);
|
||||
if (
|
||||
toolText.includes("REA_CLIENT_FIXTURE") &&
|
||||
/(?:\\?"REA_CLIENT_FIXTURE\\?"\s*:\s*true)/u.test(toolText)
|
||||
) {
|
||||
artifactReadByAgent = true;
|
||||
assert(artifactPath);
|
||||
const artifact = await readFile(artifactPath);
|
||||
artifactDigest = createHash("sha256").update(artifact).digest("hex");
|
||||
const evidence = toolContract(
|
||||
"analyze_javascript_application",
|
||||
).outputSchema.parse(JSON.parse(artifact.toString("utf8")));
|
||||
assert(extractEvidence(evidence));
|
||||
assert.equal(evidence.subject.local_path, target);
|
||||
const shape = evidence.normalized_result.graph.nodes
|
||||
.flatMap((n) => n.observations)
|
||||
.map((o) => o.properties)
|
||||
.find(
|
||||
(p) =>
|
||||
p.semantic_role === "export-return-shapes" &&
|
||||
p.exported_name === "result",
|
||||
);
|
||||
assert(shape?.return_shape_coverage?.projection_complete);
|
||||
const fields = shape.static_return_shapes.flatMap((s) => s.fields);
|
||||
assert(
|
||||
fields.some(
|
||||
(f) =>
|
||||
f.path === "/clientCompatibility" &&
|
||||
f.state === "literal" &&
|
||||
f.value === "REA_CLIENT_FIXTURE",
|
||||
),
|
||||
);
|
||||
assert(
|
||||
fields.some(
|
||||
(f) => f.path === "/count" && f.state === "literal" && f.value === 7,
|
||||
),
|
||||
);
|
||||
evidenceSeen = true;
|
||||
await writeFile(
|
||||
`${lab}/${client}-${mode}-evidence.json`,
|
||||
JSON.stringify(evidence),
|
||||
);
|
||||
}
|
||||
for (const message of body.messages ?? []) {
|
||||
if (message.role !== "tool") continue;
|
||||
const evidence = extractEvidence(message.content);
|
||||
if (evidence) {
|
||||
assert(
|
||||
JSON.stringify(evidence.normalized_result).includes(
|
||||
"REA_CLIENT_FIXTURE",
|
||||
),
|
||||
);
|
||||
evidenceSeen = true;
|
||||
await writeFile(
|
||||
`${lab}/${client}-${mode}-evidence.json`,
|
||||
JSON.stringify(evidence),
|
||||
);
|
||||
}
|
||||
}
|
||||
const selected = tools.find((t) =>
|
||||
t.name.includes("analyze_javascript_application"),
|
||||
);
|
||||
const reviewedNames = [
|
||||
...JSON.stringify(
|
||||
(body.messages ?? []).filter((m) => m.role === "tool"),
|
||||
).matchAll(
|
||||
/(?:\\?"name\\?"\s*:\s*\\?")([^"\\]*analyze_javascript_application)(?:\\?")/g,
|
||||
),
|
||||
]
|
||||
.map((m) => m[1])
|
||||
.sort((a, b) => b.length - a.length);
|
||||
const search = tools.find((t) => t.name === "tool_search");
|
||||
const invoke = tools.find((t) => t.name === "tool_call");
|
||||
const skill = tools.find((t) => t.name === "skill");
|
||||
const shell = tools.find((t) => t.name === "run_shell_command");
|
||||
const persisted = toolText.match(
|
||||
/Full output saved to: ([^\\"\n]+)(?:\\n|\n)/u,
|
||||
);
|
||||
const call = (name, args) => ({
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: `call_rea_fixture_${requestIndex}`,
|
||||
type: "function",
|
||||
function: { name, arguments: JSON.stringify(args) },
|
||||
},
|
||||
],
|
||||
});
|
||||
let message;
|
||||
if (mode === "chat" || evidenceSeen) {
|
||||
message = { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
|
||||
finalSeen = true;
|
||||
} else if (skill && !skillRequested) {
|
||||
skillRequested = true;
|
||||
message = call(skill.name, { skill: "reverse-engineer-anything" });
|
||||
} else if (persisted && shell) {
|
||||
artifactPath = persisted[1];
|
||||
assert(
|
||||
(await realpath(artifactPath)).startsWith(
|
||||
(await realpath(runtime)) + "/",
|
||||
),
|
||||
);
|
||||
const script = `const fs=require('node:fs');const e=JSON.parse(fs.readFileSync(${JSON.stringify(artifactPath)},'utf8'));console.log(JSON.stringify({REA_CLIENT_FIXTURE:JSON.stringify(e.normalized_result).includes('REA_CLIENT_FIXTURE'),evidence_id:e.evidence_id,graph_nodes:e.normalized_result.graph.nodes.length}));`;
|
||||
const quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'";
|
||||
message = call(shell.name, {
|
||||
command: `node -e ${quote(script)}`,
|
||||
description:
|
||||
"Read the complete saved REA Evidence and verify the fixture facts.",
|
||||
});
|
||||
} else if (selected)
|
||||
message = call(selected.name, {
|
||||
input_path: target,
|
||||
format: "directory",
|
||||
});
|
||||
else if (reviewedNames.length && invoke)
|
||||
message = call(invoke.name, {
|
||||
name: reviewedNames[0],
|
||||
arguments: { input_path: target, format: "directory" },
|
||||
});
|
||||
else if (search && !searchRequested) {
|
||||
searchRequested = true;
|
||||
message = call(search.name, { query: "analyze_javascript_application" });
|
||||
} else
|
||||
throw new Error(
|
||||
`REA analysis tool not exposed; tools: ${tools.map((t) => t.name).join(", ")}`,
|
||||
);
|
||||
return message;
|
||||
},
|
||||
});
|
||||
const endpoint = fixture.baseUrl;
|
||||
const prompt =
|
||||
mode === "chat"
|
||||
? "Say hello briefly."
|
||||
: `Use REA to analyze the JavaScript directory ${target}. Report the result.`;
|
||||
const env = {
|
||||
...baseEnvironment,
|
||||
OPENAI_API_KEY: "local-fixture-key",
|
||||
OPENAI_BASE_URL: endpoint,
|
||||
};
|
||||
const args = [
|
||||
"--auth-type",
|
||||
"openai",
|
||||
"--openai-base-url",
|
||||
endpoint,
|
||||
"--model",
|
||||
"rea-client-fixture",
|
||||
"--approval-mode",
|
||||
mode === "chat" ? "default" : "yolo",
|
||||
"--allowed-mcp-server-names",
|
||||
"rea",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--max-session-turns",
|
||||
"8",
|
||||
"-p",
|
||||
prompt,
|
||||
];
|
||||
let response;
|
||||
let failure;
|
||||
try {
|
||||
response = await exec(command, args, {
|
||||
env,
|
||||
cwd: workspace,
|
||||
timeout: 120_000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
await writeFile(join(lab, `${mode}.stdout`), response.stdout);
|
||||
await writeFile(join(lab, `${mode}.stderr`), response.stderr);
|
||||
const initialized = response.stdout
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line))
|
||||
.find((v) => v.type === "system" && v.subtype === "init");
|
||||
assert(initialized);
|
||||
const observed = initialized.tools
|
||||
.filter((n) => n.startsWith("mcp__rea__"))
|
||||
.sort();
|
||||
assert.deepEqual(
|
||||
observed,
|
||||
TOOL_CONTRACTS.map((c) => `mcp__rea__${c.name}`).sort(),
|
||||
"Qwen must discover the complete REA catalog",
|
||||
);
|
||||
assert.equal(fixture.failure, undefined);
|
||||
assert(finalSeen);
|
||||
if (mode === "call") {
|
||||
assert(evidenceSeen);
|
||||
assert(selectedSkillLoaded);
|
||||
assert(artifactReadByAgent);
|
||||
}
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
if (typeof cause?.stdout === "string")
|
||||
await writeFile(join(lab, `${mode}.stdout`), cause.stdout);
|
||||
if (typeof cause?.stderr === "string")
|
||||
await writeFile(join(lab, `${mode}.stderr`), cause.stderr);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
const lineage = await completeVerifierRun(verifier);
|
||||
const receipt = {
|
||||
client: "Qwen Code",
|
||||
clientVersion,
|
||||
mode,
|
||||
passed: failure === undefined,
|
||||
artifacts: lab,
|
||||
runtimeRoot,
|
||||
host: {
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
node: process.versions.node,
|
||||
},
|
||||
modelFixture:
|
||||
"loopback deterministic OpenAI-compatible endpoint; no live model-provider claim",
|
||||
skillDiscovery:
|
||||
"caller-configured isolated shared directory; default OS home discovery unverified",
|
||||
catalogSize: TOOL_CONTRACTS.length,
|
||||
requests: fixture.requests,
|
||||
probes: fixture.probes,
|
||||
evidenceSeen,
|
||||
selectedSkillLoaded,
|
||||
artifactReadByAgent,
|
||||
artifactPath,
|
||||
artifactDigest,
|
||||
finalSeen,
|
||||
fixtureFailure: fixture.failure,
|
||||
verifier: lineage,
|
||||
};
|
||||
await writeFile(join(lab, "receipt.json"), JSON.stringify(receipt, null, 2));
|
||||
console.log(JSON.stringify(receipt));
|
||||
}
|
||||
if (failure !== undefined) throw failure;
|
||||
@@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtemp, mkdir, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import {
|
||||
inspectMipsReadelf,
|
||||
assertMipsLoadedImage,
|
||||
assertMipsProbe,
|
||||
assertMipsGlobal,
|
||||
} from "../tests/conformance/ghidra/mips-checks.mjs";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js";
|
||||
import { functionDossierSchema } from "../dist/domain/hopperValues.js";
|
||||
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
|
||||
import { buildMipsFixture } from "../tests/conformance/ghidra/mips-fixture.mjs";
|
||||
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
// Optional cross-target lane. No tool acquisition and no target execution.
|
||||
if (process.argv.length !== 2)
|
||||
throw new Error("Usage: node scripts/verify-real-ghidra-mips.mjs");
|
||||
if (!["linux", "darwin"].includes(process.platform))
|
||||
throw new Error(
|
||||
"MIPS verification requires a supported Linux/macOS Ghidra host",
|
||||
);
|
||||
if (process.env.GHIDRA_INSTALL_DIR === undefined)
|
||||
throw new Error("Missing prerequisite: caller-supplied GHIDRA_INSTALL_DIR");
|
||||
const installation = inspectGhidraInstallation({
|
||||
environment: process.env,
|
||||
installDir: process.env.GHIDRA_INSTALL_DIR,
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { javaHome: process.env.JAVA_HOME }),
|
||||
});
|
||||
if (installation.status !== "available")
|
||||
throw new Error(
|
||||
`Ghidra prerequisite unavailable: ${JSON.stringify(installation)}`,
|
||||
);
|
||||
const execute = promisify(execFile);
|
||||
const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url));
|
||||
const run = createVerifierRun();
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-mips-proof-"));
|
||||
const runtime = join(workspace, "runtime");
|
||||
await mkdir(runtime);
|
||||
const env = {
|
||||
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
||||
TMPDIR: runtime,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_ANALYSIS_PROVIDER: "ghidra",
|
||||
REA_PROCESS_RUN_ID: run.run_id,
|
||||
GHIDRA_INSTALL_DIR: installation.installDir,
|
||||
JAVA_HOME: installation.javaHome,
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
||||
};
|
||||
const reports = [];
|
||||
let primaryFailure;
|
||||
try {
|
||||
for (const byteOrder of ["little", "big"]) {
|
||||
const fixture = await buildMipsFixture(workspace, byteOrder);
|
||||
const independent = await inspectMipsReadelf(fixture.path, byteOrder);
|
||||
const resolved = await parseBinaryTarget(fixture.path);
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
assert.deepEqual(
|
||||
resolved.value.mips,
|
||||
independent.mips,
|
||||
"REA ELF/ABI interpretation disagrees with GNU readelf",
|
||||
);
|
||||
assert.equal(resolved.value.sha256, fixture.sha256);
|
||||
const snapshotPath = join(workspace, `${byteOrder}.snapshot.json`);
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "rea-real-mips-proof", version: "1" });
|
||||
let stderr = "";
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
stderr = (stderr + chunk.toString()).slice(-65536);
|
||||
});
|
||||
const call = async (name, args = {}) => {
|
||||
const reply = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
undefined,
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
return requireMcpOperationResult(reply, name);
|
||||
};
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const target = await call("open_binary", {
|
||||
path: fixture.path,
|
||||
provider_id: "ghidra",
|
||||
});
|
||||
assert.equal(target.architecture, "mips");
|
||||
assert.equal(target.sha256, fixture.sha256);
|
||||
// ELF load-image verification is not the DOS comparison lane. Its retained
|
||||
// observations still expose the actual Ghidra language and loaded bytes.
|
||||
const image = await call("inspect_native_load_image");
|
||||
assertMipsLoadedImage(image.observations, byteOrder, fixture.sha256);
|
||||
const procedures = await call("list_procedures");
|
||||
const entry = procedures.find((p) => p.value === "rea_mips_entry");
|
||||
const leaf = procedures.find((p) => p.value === "rea_mips_leaf");
|
||||
assert.ok(entry, "Entry function missing from real Ghidra inventory");
|
||||
assert.ok(leaf, "Leaf function missing from real Ghidra inventory");
|
||||
assert.equal(
|
||||
BigInt(entry.address),
|
||||
BigInt(independent.symbols.rea_mips_entry),
|
||||
);
|
||||
assert.equal(
|
||||
BigInt(leaf.address),
|
||||
BigInt(independent.symbols.rea_mips_leaf),
|
||||
);
|
||||
const probe = independent.symbols.rea_mips_probe;
|
||||
assert.ok(
|
||||
procedures.some(
|
||||
(p) =>
|
||||
p.value === "rea_mips_probe" && BigInt(p.address) === BigInt(probe),
|
||||
),
|
||||
);
|
||||
const move = await call("inspect_native_instruction", { address: probe });
|
||||
const branch = await call("inspect_native_instruction", {
|
||||
address: `0x${(BigInt(probe) + 4n).toString(16)}`,
|
||||
});
|
||||
assertMipsProbe(move, branch, byteOrder, probe);
|
||||
const global = await call("read_bytes", {
|
||||
address: independent.symbols.rea_mips_global,
|
||||
length: 4,
|
||||
});
|
||||
assert.equal(global.returned_bytes, 4);
|
||||
assertMipsGlobal(global.bytes_hex, byteOrder);
|
||||
const marker = await call("read_bytes", {
|
||||
address: independent.symbols.rea_mips_marker,
|
||||
length: 30,
|
||||
});
|
||||
assert.equal(
|
||||
marker.bytes_hex,
|
||||
Buffer.from("rea-mips-source-owned-fixture\0").toString("hex"),
|
||||
);
|
||||
const callees = await call("procedure_callees", {
|
||||
procedure: entry.address,
|
||||
});
|
||||
assert.ok(
|
||||
callees.includes(leaf.address),
|
||||
"Expected direct call not recovered",
|
||||
);
|
||||
const assembly = await call("procedure_assembly", {
|
||||
procedure: entry.address,
|
||||
});
|
||||
assert.ok(assembly.trim().length > 0);
|
||||
const dossier = functionDossierSchema.parse(
|
||||
await call("analyze_function", { procedure: entry.address }),
|
||||
);
|
||||
assert.ok(dossier.pseudocode.trim().length > 0);
|
||||
const mapping = await call("address_to_file_offset", {
|
||||
address: entry.address,
|
||||
});
|
||||
const memory = await call("read_bytes", {
|
||||
address: entry.address,
|
||||
length: 16,
|
||||
});
|
||||
const sourceBytes = await readFile(fixture.path);
|
||||
assert.equal(memory.returned_bytes, 16);
|
||||
assert.equal(
|
||||
memory.bytes_hex,
|
||||
sourceBytes
|
||||
.subarray(mapping.file_offset, mapping.file_offset + 16)
|
||||
.toString("hex"),
|
||||
);
|
||||
const saved = await call("close_binary", {
|
||||
snapshot_path: snapshotPath,
|
||||
});
|
||||
assert.ok(saved.evidence_records > 0);
|
||||
const snapshot = parseAnalysisSnapshot(
|
||||
JSON.parse(await readFile(snapshotPath, "utf8")),
|
||||
);
|
||||
assert.equal(snapshot.target.architecture, "mips");
|
||||
assert.equal(
|
||||
snapshot.binding.analysis_profile.parameters.mips_elf.byte_order,
|
||||
byteOrder,
|
||||
);
|
||||
assert.ok(snapshot.evidence_bundle.records.length > 0);
|
||||
for (const evidence of snapshot.evidence_bundle.records)
|
||||
assert.equal(parseEvidence(evidence).subject?.architecture, "mips");
|
||||
await call("open_binary", {
|
||||
path: fixture.path,
|
||||
provider_id: "ghidra",
|
||||
snapshot_path: snapshotPath,
|
||||
});
|
||||
assert.deepEqual(await call("list_procedures"), procedures);
|
||||
await call("close_binary");
|
||||
|
||||
// Same public operation through CLI, not a private injected provider.
|
||||
const cli = await execute(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"function",
|
||||
fixture.path,
|
||||
"rea_mips_entry",
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const evidence = parseEvidence(JSON.parse(cli.stdout));
|
||||
assert.equal(evidence.subject?.architecture, "mips");
|
||||
assert.equal(evidence.subject?.digest.sha256, fixture.sha256);
|
||||
const cliDossier = functionDossierSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
assert.equal(cliDossier.procedure.address, dossier.procedure.address);
|
||||
assert.ok(cliDossier.pseudocode.trim().length > 0);
|
||||
assert.equal(
|
||||
createHash("sha256")
|
||||
.update(await readFile(fixture.path))
|
||||
.digest("hex"),
|
||||
fixture.sha256,
|
||||
"Analysis modified the source executable",
|
||||
);
|
||||
reports.push({
|
||||
...fixture,
|
||||
independent_reader: independent,
|
||||
effective_language: image.observations.language_id,
|
||||
effective_compiler_spec: image.observations.compiler_spec_id,
|
||||
checks: [
|
||||
"independent ELF/ABI and symbols",
|
||||
"loaded Ghidra identity",
|
||||
"fixed instruction bytes and immediate",
|
||||
"conditional branch destination",
|
||||
"global word and marker bytes",
|
||||
"CLI/MCP and snapshot identity",
|
||||
],
|
||||
procedures: procedures.length,
|
||||
status: "passed",
|
||||
});
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`MIPS ${byteOrder} verification failed; MCP stderr: ${stderr}`,
|
||||
{ cause },
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (cause) {
|
||||
primaryFailure = cause;
|
||||
}
|
||||
const completed = await completeVerifierRun(run);
|
||||
if (
|
||||
completed.process_lineage.status !== "verified" ||
|
||||
completed.process_lineage.descendants.length !== 0
|
||||
)
|
||||
throw new Error(
|
||||
`Cleanup unverified; retained diagnostic workspace: ${workspace}`,
|
||||
{ cause: primaryFailure },
|
||||
);
|
||||
if (primaryFailure !== undefined)
|
||||
throw new Error(
|
||||
`MIPS verification failed; retained workspace: ${workspace}`,
|
||||
{
|
||||
cause: primaryFailure,
|
||||
},
|
||||
);
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
console.log(JSON.stringify({ ...completed, fixtures: reports }, null, 2));
|
||||
@@ -24,6 +24,7 @@ import { windowsP0Capabilities } from "../dist/ghidra/GhidraProviderCapabilities
|
||||
import { GHIDRA_SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js";
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { verifyGhidraCompactReferences } from "./lib/real-ghidra-compact-references.mjs";
|
||||
|
||||
const verifierRun = createVerifierRun();
|
||||
const SESSION_CAPABILITIES = GHIDRA_SESSION_CAPABILITIES.filter(
|
||||
@@ -247,6 +248,14 @@ try {
|
||||
`Windows Ghidra operation proof was incomplete: ${JSON.stringify([...observed])}`,
|
||||
);
|
||||
|
||||
// The reference controls occupy the EXE fixture's additional function.
|
||||
// DLL fixtures retain their independent exported-caller/return controls.
|
||||
const compactReferences = dllFixture
|
||||
? null
|
||||
: await verifyGhidraCompactReferences(
|
||||
functionOperation,
|
||||
architecture === "x86",
|
||||
);
|
||||
report = {
|
||||
ok: true,
|
||||
provider: { id: "ghidra", version: SUPPORTED_GHIDRA_VERSION },
|
||||
@@ -261,6 +270,7 @@ try {
|
||||
transport: "authenticated-loopback-tcp",
|
||||
operations: [...observed].sort((left, right) => left.localeCompare(right)),
|
||||
cleanup: "complete",
|
||||
compact_references: compactReferences,
|
||||
native_controls: {
|
||||
job_object: true,
|
||||
protected_dacl: true,
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { cp, mkdir, readFile, readdir } from "node:fs/promises";
|
||||
import { dirname, join, relative } from "node:path";
|
||||
|
||||
import { exec, pathExists } from "./lib/verify-package-core.mjs";
|
||||
|
||||
/** Exercise Skills' default root discovery without ignored build outputs. */
|
||||
export async function verifyRepositorySkill({ root, workspace }) {
|
||||
const snapshot = join(workspace, "repository");
|
||||
const consumer = join(workspace, "skill-consumer");
|
||||
const home = join(workspace, "skill-consumer-home");
|
||||
const tracked = (
|
||||
await exec(
|
||||
"git",
|
||||
["ls-files", "--cached", "--format=%(objectmode) %(path)", "-z"],
|
||||
{ cwd: root },
|
||||
)
|
||||
).stdout
|
||||
.split("\0")
|
||||
.filter(Boolean);
|
||||
for (const entry of tracked) {
|
||||
const mode = entry.slice(0, 6);
|
||||
const path = entry.slice(7);
|
||||
const destination = join(snapshot, path);
|
||||
// A GitHub clone without submodule initialization has only these directories.
|
||||
if (mode === "160000") {
|
||||
await mkdir(destination, { recursive: true });
|
||||
continue;
|
||||
}
|
||||
await mkdir(dirname(destination), { recursive: true });
|
||||
await cp(join(root, path), destination, { verbatimSymlinks: true });
|
||||
}
|
||||
if (await pathExists(join(snapshot, "skills")))
|
||||
throw new Error("repository skill check must run without generated skills");
|
||||
await mkdir(consumer, { recursive: true });
|
||||
await mkdir(home, { recursive: true });
|
||||
const skillsVersion = "1.7.2";
|
||||
await exec(
|
||||
"npm",
|
||||
[
|
||||
"exec",
|
||||
"--yes",
|
||||
"--package",
|
||||
`skills@${skillsVersion}`,
|
||||
"--",
|
||||
"skills",
|
||||
"add",
|
||||
snapshot,
|
||||
"--skill",
|
||||
"reverse-engineer-anything",
|
||||
"--agent",
|
||||
"codex",
|
||||
"--yes",
|
||||
],
|
||||
{
|
||||
cwd: consumer,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
XDG_CONFIG_HOME: join(home, ".config"),
|
||||
CODEX_HOME: join(home, ".codex"),
|
||||
DO_NOT_TRACK: "1",
|
||||
SKILLS_NO_TELEMETRY: "1",
|
||||
CI: "1",
|
||||
},
|
||||
timeout: 120_000,
|
||||
},
|
||||
);
|
||||
const authoredRoot = join(
|
||||
snapshot,
|
||||
".agents/skills/reverse-engineer-anything",
|
||||
);
|
||||
const installedSkills = join(consumer, ".agents/skills");
|
||||
const installedRoot = join(installedSkills, "reverse-engineer-anything");
|
||||
const files = await filePaths(authoredRoot);
|
||||
if (
|
||||
JSON.stringify(await readdir(installedSkills)) !==
|
||||
JSON.stringify(["reverse-engineer-anything"]) ||
|
||||
JSON.stringify(await filePaths(installedRoot)) !== JSON.stringify(files)
|
||||
)
|
||||
throw new Error(
|
||||
"Skills did not install exactly the selected public bundle",
|
||||
);
|
||||
for (const path of files) {
|
||||
const authored = await readFile(join(authoredRoot, path));
|
||||
if (!authored.equals(await readFile(join(installedRoot, path))))
|
||||
throw new Error(`Skills changed authored skill content: ${path}`);
|
||||
}
|
||||
return { skillsVersion, files };
|
||||
}
|
||||
|
||||
async function filePaths(root) {
|
||||
return (await readdir(root, { recursive: true, withFileTypes: true }))
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => relative(root, join(entry.parentPath, entry.name)))
|
||||
.sort();
|
||||
}
|
||||
+103
-18
@@ -5,13 +5,79 @@ from pathlib import Path
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
from urllib.parse import quote, unquote, urlsplit
|
||||
import unicodedata
|
||||
from urllib.parse import unquote, urlsplit
|
||||
import xml.etree.ElementTree as ET
|
||||
from zipfile import BadZipFile, ZipFile
|
||||
|
||||
from website import NOTES_FIXTURES, SITE_ORIGIN, authored_markdown, page_url, sync_pages
|
||||
|
||||
SITE_ORIGIN = "https://rea.tools"
|
||||
NOTES_FIXTURES = {"examples/notes-electron/index.html", "examples/notes-web/index.html"}
|
||||
LEGACY_ORIGIN = "morluto.github.io"
|
||||
SITE_TEXT_URL = re.compile(
|
||||
r"https?://(?:rea\.tools|morluto\.github\.io)/[^\s<>\"'`]+", re.IGNORECASE
|
||||
)
|
||||
|
||||
|
||||
def markdown_ids(path):
|
||||
"""Collect GitHub heading anchors and explicit HTML IDs in local Markdown."""
|
||||
source = path.read_text(encoding="utf-8")
|
||||
parser = HtmlReferences()
|
||||
parser.feed(source)
|
||||
identifiers = set(parser.ids)
|
||||
counts = {}
|
||||
fenced = False
|
||||
for line in source.splitlines():
|
||||
if line.lstrip().startswith(("```", "~~~")):
|
||||
fenced = not fenced
|
||||
if fenced:
|
||||
continue
|
||||
heading = re.match(r"^ {0,3}#{1,6}\s+(.+?)(?:\s+#+)?\s*$", line)
|
||||
if not heading:
|
||||
continue
|
||||
text = re.sub(r"<[^>]*>", "", heading[1]).lower()
|
||||
slug = "".join(c for c in text if c in "-_ " or unicodedata.category(c)[0] in "LN").replace(" ", "-")
|
||||
count = counts.get(slug, 0)
|
||||
identifiers.add(f"{slug}-{count}" if count else slug)
|
||||
counts[slug] = count + 1
|
||||
return identifiers
|
||||
|
||||
|
||||
def check_repository_links(root):
|
||||
"""Check local links in website Markdown and current-repository source links."""
|
||||
errors = []
|
||||
sources = {}
|
||||
for path in authored_markdown(root):
|
||||
text = re.sub(r"(?ms)^\s*(```|~~~).*?^\s*\1[^\n]*$", "", path.read_text(encoding="utf-8"))
|
||||
text = re.sub(r"`[^`]*`", "", text)
|
||||
sources[path] = re.findall(r"\[[^\]\n]*\]\(([^\s)]+)\)", text)
|
||||
for path in sorted((root / "website/public").rglob("*.html")):
|
||||
parser = HtmlReferences()
|
||||
parser.feed(path.read_text(encoding="utf-8"))
|
||||
sources[path] = parser.references
|
||||
for path, references in sources.items():
|
||||
for reference in references:
|
||||
try:
|
||||
url = urlsplit(reference)
|
||||
except ValueError as error:
|
||||
errors.append(f"{path.relative_to(root)}: invalid URL {reference!r}: {error}")
|
||||
continue
|
||||
if url.hostname == LEGACY_ORIGIN and (url.path == "/rea" or url.path.startswith("/rea/")):
|
||||
errors.append(f"{path.relative_to(root)}: obsolete website URL: {reference}")
|
||||
continue
|
||||
if url.hostname == "github.com":
|
||||
match = re.match(r"^/morluto/rea/(?:blob|tree)/main/(.+)$", url.path)
|
||||
if not match:
|
||||
continue
|
||||
target = (root / unquote(match[1])).resolve()
|
||||
elif url.scheme or url.netloc or path.suffix == ".html":
|
||||
continue
|
||||
else:
|
||||
target = (path.parent / unquote(url.path)).resolve() if url.path else path
|
||||
if not target.is_relative_to(root) or not target.exists():
|
||||
errors.append(f"{path.relative_to(root)}: missing repository target: {reference}")
|
||||
elif target.suffix == ".md" and url.fragment and unquote(url.fragment) not in markdown_ids(target):
|
||||
errors.append(f"{path.relative_to(root)}: missing Markdown fragment: {reference}")
|
||||
return errors
|
||||
|
||||
|
||||
class HtmlReferences(HTMLParser):
|
||||
@@ -30,6 +96,8 @@ class HtmlReferences(HTMLParser):
|
||||
self.canonicals = []
|
||||
self.lang = None
|
||||
self.h1_count = 0
|
||||
self.text = []
|
||||
self.copy_targets = []
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
attributes = dict(attrs)
|
||||
@@ -67,6 +135,8 @@ class HtmlReferences(HTMLParser):
|
||||
reference = attributes.get(attribute)
|
||||
if reference is not None:
|
||||
self.references.append(reference)
|
||||
if attributes.get("data-copy"):
|
||||
self.copy_targets.append(attributes["data-copy"])
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == "head":
|
||||
@@ -75,6 +145,7 @@ class HtmlReferences(HTMLParser):
|
||||
self.in_title = False
|
||||
|
||||
def handle_data(self, data):
|
||||
self.text.append(data)
|
||||
if self.in_title:
|
||||
self.titles[-1] += data
|
||||
|
||||
@@ -104,15 +175,7 @@ def check_search_metadata(site, pages):
|
||||
)
|
||||
if page.noindex():
|
||||
continue
|
||||
route = (
|
||||
path.relative_to(site).parent.as_posix()
|
||||
if path.name == "index.html"
|
||||
else label
|
||||
)
|
||||
route = "" if route == "." else route
|
||||
url = f"{SITE_ORIGIN}/{quote(route, safe='/')}" + (
|
||||
"/" if route and path.name == "index.html" else ""
|
||||
)
|
||||
url = page_url(site, path)
|
||||
indexable.add(url)
|
||||
if page.canonicals != [url]:
|
||||
errors.append(f"{label}: needs exactly one head canonical for {url}")
|
||||
@@ -233,15 +296,32 @@ def check_site(site):
|
||||
label = path.relative_to(site)
|
||||
for identifier in page.duplicate_ids:
|
||||
errors.append(f"{label}: duplicate ID {identifier!r}")
|
||||
for reference in page.references:
|
||||
url = urlsplit(reference)
|
||||
if url.scheme or url.netloc:
|
||||
for identifier in page.copy_targets:
|
||||
if identifier not in page.ids:
|
||||
errors.append(f"{label}: missing copy target: {identifier}")
|
||||
text_urls = [match.group().rstrip(".,;)") for match in SITE_TEXT_URL.finditer(" ".join(page.text))]
|
||||
for reference in page.references + text_urls:
|
||||
try:
|
||||
url = urlsplit(reference)
|
||||
except ValueError as error:
|
||||
errors.append(f"{label}: invalid URL {reference!r}: {error}")
|
||||
continue
|
||||
references += 1
|
||||
if url.path.startswith("/"):
|
||||
if url.hostname == LEGACY_ORIGIN and (url.path == "/rea" or url.path.startswith("/rea/")):
|
||||
errors.append(f"{label}: obsolete website URL: {reference}; use rea.tools")
|
||||
continue
|
||||
if url.hostname == "rea.tools":
|
||||
if url.scheme != "https" or url.netloc != "rea.tools":
|
||||
errors.append(f"{label}: website URL must use {SITE_ORIGIN}: {reference}")
|
||||
continue
|
||||
target = (site / unquote(url.path).lstrip("/")).resolve()
|
||||
elif url.scheme or url.netloc:
|
||||
continue
|
||||
elif url.path.startswith("/"):
|
||||
errors.append(f"{label}: use a relative site reference: {reference}")
|
||||
continue
|
||||
target = (path.parent / unquote(url.path)).resolve() if url.path else path
|
||||
else:
|
||||
target = (path.parent / unquote(url.path)).resolve() if url.path else path
|
||||
references += 1
|
||||
if not target.is_relative_to(site):
|
||||
errors.append(f"{label}: reference leaves the website: {reference}")
|
||||
continue
|
||||
@@ -323,6 +403,11 @@ def main():
|
||||
pages, references, indexable, errors = check_site(root / "website/public")
|
||||
errors.extend(check_example_archive(root / "website/public"))
|
||||
errors.extend(check_publisher(root))
|
||||
errors.extend(check_repository_links(root))
|
||||
try:
|
||||
sync_pages(root, check=True)
|
||||
except (OSError, ValueError, KeyError) as error:
|
||||
errors.append(str(error))
|
||||
if errors:
|
||||
print("Website checks failed:\n" + "\n".join(errors), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
@@ -29,7 +29,7 @@ const LINK_MODES = [
|
||||
{
|
||||
mode: "dyld-info",
|
||||
flags: ["-Wl,-no_fixup_chains"],
|
||||
fixups: "LC_DYLD_INFO bind opcodes",
|
||||
fixups: "LC_DYLD_INFO rebase opcodes were not decoded",
|
||||
swift: true,
|
||||
},
|
||||
{ mode: "chained", flags: [], fixups: "chained fixups:", swift: true },
|
||||
@@ -135,7 +135,12 @@ const checkObjc = (metadata, mode) => {
|
||||
item.selector === "reaFixtureLength",
|
||||
) ||
|
||||
objcCoverage?.status !== "complete" ||
|
||||
fixupCoverage(metadata)?.reason?.startsWith(mode.fixups) !== true
|
||||
fixupCoverage(metadata)?.reason?.startsWith(mode.fixups) !== true ||
|
||||
(mode.mode === "dyld-info") !==
|
||||
(fixupCoverage(metadata)?.reason?.includes(
|
||||
"LC_DYLD_INFO bind opcodes",
|
||||
) ===
|
||||
true)
|
||||
)
|
||||
throw new Error(
|
||||
`Apple metadata fixture drifted (${mode.mode}): ${JSON.stringify(metadata)}`,
|
||||
@@ -165,7 +170,12 @@ const checkSwift = (metadata, mode) => {
|
||||
item.owner === "ReaWitnessFixture: ReaWitnessFixtureProtocol" &&
|
||||
item.implementation_address !== null,
|
||||
) ||
|
||||
fixupCoverage(metadata)?.reason?.startsWith(mode.fixups) !== true
|
||||
fixupCoverage(metadata)?.reason?.startsWith(mode.fixups) !== true ||
|
||||
(mode.mode === "dyld-info") !==
|
||||
(fixupCoverage(metadata)?.reason?.includes(
|
||||
"LC_DYLD_INFO bind opcodes",
|
||||
) ===
|
||||
true)
|
||||
)
|
||||
throw new Error(
|
||||
`Swift metadata fixture drifted (${mode.mode}): ${JSON.stringify(metadata)}`,
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createServer } from "node:http";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
/** Exercise native Anthropic adapters locally, without live-provider claims. */
|
||||
export const createAnthropicModelFixture = async ({ directory, onRequest }) => {
|
||||
const requests = [];
|
||||
const probes = [];
|
||||
let incoming = 0;
|
||||
let failure;
|
||||
const server = createServer(async (request, response) => {
|
||||
try {
|
||||
const chunks = [];
|
||||
let bytes = 0;
|
||||
for await (const chunk of request) {
|
||||
bytes += chunk.length;
|
||||
assert(
|
||||
bytes <= 10 * 1024 * 1024,
|
||||
"Model fixture request exceeds 10 MiB budget",
|
||||
);
|
||||
chunks.push(chunk);
|
||||
}
|
||||
const raw = Buffer.concat(chunks).toString("utf8");
|
||||
const body = raw.length === 0 ? null : JSON.parse(raw);
|
||||
const path = new URL(request.url, "http://localhost").pathname;
|
||||
// Retain producer bodies and URLs, without transport authentication headers.
|
||||
await writeFile(
|
||||
join(directory, `claude-incoming-${++incoming}.json`),
|
||||
JSON.stringify({ method: request.method, path: request.url, body }),
|
||||
);
|
||||
if (path === "/v1/messages/count_tokens") {
|
||||
probes.push({ path, kind: "synthetic-token-count" });
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.end(JSON.stringify({ input_tokens: 100 }));
|
||||
return;
|
||||
}
|
||||
if (path !== "/v1/messages") {
|
||||
probes.push({ path, status: 404 });
|
||||
response.statusCode = 404;
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
error: { message: "Unsupported fixture endpoint" },
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
assert.equal(request.method, "POST");
|
||||
assert(body && typeof body === "object" && !Array.isArray(body));
|
||||
assert(Array.isArray(body.messages));
|
||||
const tools = body.tools ?? [];
|
||||
requests.push({
|
||||
path: request.url,
|
||||
stream: body.stream,
|
||||
tools: tools.map((t) => t.name),
|
||||
messageRoles: body.messages.map((m) => m.role),
|
||||
});
|
||||
const content = await onRequest({
|
||||
body,
|
||||
tools,
|
||||
requestIndex: requests.length,
|
||||
});
|
||||
const stopReason = content.some((c) => c.type === "tool_use")
|
||||
? "tool_use"
|
||||
: "end_turn";
|
||||
const usage = {
|
||||
input_tokens: 100,
|
||||
output_tokens: 20,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
};
|
||||
const result = {
|
||||
id: `msg_rea_${requests.length}`,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
model: body.model,
|
||||
content,
|
||||
stop_reason: stopReason,
|
||||
stop_sequence: null,
|
||||
usage,
|
||||
};
|
||||
if (body.stream) {
|
||||
response.setHeader("Content-Type", "text/event-stream");
|
||||
const emit = (event, data) =>
|
||||
response.write(
|
||||
`event: ${event}\ndata: ${JSON.stringify({ type: event, ...data })}\n\n`,
|
||||
);
|
||||
emit("message_start", {
|
||||
message: {
|
||||
...result,
|
||||
content: [],
|
||||
stop_reason: null,
|
||||
usage: { ...usage, output_tokens: 0 },
|
||||
},
|
||||
});
|
||||
for (const [index, block] of content.entries()) {
|
||||
emit("content_block_start", {
|
||||
index,
|
||||
content_block:
|
||||
block.type === "tool_use"
|
||||
? { ...block, input: {} }
|
||||
: { type: "text", text: "" },
|
||||
});
|
||||
if (block.type === "tool_use") {
|
||||
const json = JSON.stringify(block.input);
|
||||
const split = Math.floor(json.length / 2);
|
||||
for (const partialJson of [
|
||||
json.slice(0, split),
|
||||
json.slice(split),
|
||||
]) {
|
||||
emit("content_block_delta", {
|
||||
index,
|
||||
delta: { type: "input_json_delta", partial_json: partialJson },
|
||||
});
|
||||
}
|
||||
} else {
|
||||
emit("content_block_delta", {
|
||||
index,
|
||||
delta: { type: "text_delta", text: block.text },
|
||||
});
|
||||
}
|
||||
emit("content_block_stop", { index });
|
||||
}
|
||||
emit("message_delta", {
|
||||
delta: { stop_reason: stopReason, stop_sequence: null },
|
||||
usage: { output_tokens: 20 },
|
||||
});
|
||||
emit("message_stop", {});
|
||||
response.end();
|
||||
} else {
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.end(JSON.stringify(result));
|
||||
}
|
||||
} catch (cause) {
|
||||
failure = String(cause);
|
||||
response.statusCode = 400;
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
error: { type: "invalid_request_error", message: failure },
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
assert(address !== null && typeof address !== "string");
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||
requests,
|
||||
probes,
|
||||
get failure() {
|
||||
return failure;
|
||||
},
|
||||
close: () => new Promise((resolve) => server.close(resolve)),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createServer } from "node:http";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
/** Exercise the native Gemini API adapter against a local model, without cloud claims. */
|
||||
export const createGeminiModelFixture = async ({ directory, onRequest }) => {
|
||||
const requests = [];
|
||||
const probes = [];
|
||||
let incoming = 0;
|
||||
let failure;
|
||||
const server = createServer(async (request, response) => {
|
||||
try {
|
||||
const chunks = [];
|
||||
let bytes = 0;
|
||||
for await (const chunk of request) {
|
||||
bytes += chunk.length;
|
||||
assert(
|
||||
bytes <= 10 * 1024 * 1024,
|
||||
"Model fixture request exceeds 10 MiB budget",
|
||||
);
|
||||
chunks.push(chunk);
|
||||
}
|
||||
const raw = Buffer.concat(chunks).toString("utf8");
|
||||
const body = raw.length === 0 ? null : JSON.parse(raw);
|
||||
await writeFile(
|
||||
join(directory, `gemini-incoming-${++incoming}.json`),
|
||||
JSON.stringify({ method: request.method, path: request.url, body }),
|
||||
);
|
||||
const path = new URL(request.url, "http://localhost").pathname;
|
||||
if (path.endsWith(":countTokens")) {
|
||||
probes.push({ path, kind: "synthetic-token-count" });
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.end(JSON.stringify({ totalTokens: 100 }));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!/\/models\/[^/]+:(?:streamGenerateContent|generateContent)$/u.test(
|
||||
path,
|
||||
)
|
||||
) {
|
||||
probes.push({ path, status: 404 });
|
||||
response.statusCode = 404;
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
error: { message: "Unsupported fixture endpoint" },
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
assert.equal(request.method, "POST");
|
||||
assert(body && typeof body === "object" && !Array.isArray(body));
|
||||
assert(Array.isArray(body.contents));
|
||||
const tools = (body.tools ?? []).flatMap(
|
||||
(t) => t.functionDeclarations ?? [],
|
||||
);
|
||||
requests.push({
|
||||
path,
|
||||
tools: tools.map((t) => t.name),
|
||||
contentRoles: body.contents.map((c) => c.role),
|
||||
});
|
||||
const parts = await onRequest({
|
||||
body,
|
||||
tools,
|
||||
requestIndex: requests.length,
|
||||
});
|
||||
const result = {
|
||||
candidates: [
|
||||
{ index: 0, content: { role: "model", parts }, finishReason: "STOP" },
|
||||
],
|
||||
usageMetadata: {
|
||||
promptTokenCount: 100,
|
||||
candidatesTokenCount: 20,
|
||||
totalTokenCount: 120,
|
||||
},
|
||||
modelVersion: "local-rea-fixture",
|
||||
};
|
||||
if (path.endsWith(":streamGenerateContent")) {
|
||||
response.setHeader("Content-Type", "text/event-stream");
|
||||
response.end(`data: ${JSON.stringify(result)}\n\n`);
|
||||
} else {
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.end(JSON.stringify(result));
|
||||
}
|
||||
} catch (cause) {
|
||||
failure = String(cause);
|
||||
response.statusCode = 400;
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.end(JSON.stringify({ error: { message: failure, code: 400 } }));
|
||||
}
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
assert(address !== null && typeof address !== "string");
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||
requests,
|
||||
probes,
|
||||
get failure() {
|
||||
return failure;
|
||||
},
|
||||
close: () => new Promise((resolve) => server.close(resolve)),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,171 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createServer } from "node:http";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
/** A loopback model fixture exercises real clients without claiming live-provider compatibility. */
|
||||
export const createOpenAiModelFixture = async ({
|
||||
directory,
|
||||
prefix,
|
||||
onRequest,
|
||||
}) => {
|
||||
const requests = [];
|
||||
const probes = [];
|
||||
let incoming = 0;
|
||||
let failure;
|
||||
const server = createServer(async (request, response) => {
|
||||
try {
|
||||
if (request.method === "GET" && request.url?.endsWith("/models")) {
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
object: "list",
|
||||
data: [
|
||||
{
|
||||
id: "rea-client-fixture",
|
||||
object: "model",
|
||||
owned_by: "local-fixture",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const chunks = [];
|
||||
let bytes = 0;
|
||||
for await (const chunk of request) {
|
||||
bytes += chunk.length;
|
||||
assert(
|
||||
bytes <= 10 * 1024 * 1024,
|
||||
"Local model fixture exceeded its 10 MiB request resource budget",
|
||||
);
|
||||
chunks.push(chunk);
|
||||
}
|
||||
const rawBody = Buffer.concat(chunks).toString("utf8");
|
||||
const body = rawBody.length === 0 ? null : JSON.parse(rawBody);
|
||||
await writeFile(
|
||||
join(directory, `${prefix}-incoming-${++incoming}.json`),
|
||||
JSON.stringify({ method: request.method, path: request.url, body }),
|
||||
);
|
||||
if (request.url !== "/v1/chat/completions") {
|
||||
// Hermes probes custom endpoints for Ollama/llama.cpp metadata.
|
||||
// Unsupported HTTP endpoints return 404, as on an OpenAI-only server.
|
||||
probes.push({
|
||||
method: request.method,
|
||||
path: request.url,
|
||||
body,
|
||||
status: 404,
|
||||
});
|
||||
response.statusCode = 404;
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
error:
|
||||
"This OpenAI fixture exposes models and chat completions only.",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
assert.equal(request.method, "POST");
|
||||
assert(body !== null && typeof body === "object" && !Array.isArray(body));
|
||||
assert(Array.isArray(body.messages));
|
||||
const tools = (body.tools ?? [])
|
||||
.filter((t) => t.type === "function")
|
||||
.map((t) => t.function ?? t);
|
||||
requests.push({
|
||||
path: request.url,
|
||||
tools: tools.map((t) => t.name),
|
||||
stream: body.stream,
|
||||
messageRoles: body.messages.map((m) => m.role),
|
||||
});
|
||||
await writeFile(
|
||||
join(directory, `${prefix}-request-${requests.length}.json`),
|
||||
JSON.stringify(body),
|
||||
);
|
||||
const message = await onRequest({
|
||||
body,
|
||||
tools,
|
||||
requestIndex: requests.length,
|
||||
});
|
||||
const base = {
|
||||
id: `chatcmpl-rea-${requests.length}`,
|
||||
object: "chat.completion",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: body.model,
|
||||
};
|
||||
const finish = message.tool_calls ? "tool_calls" : "stop";
|
||||
const usage = {
|
||||
prompt_tokens: 100,
|
||||
completion_tokens: 20,
|
||||
total_tokens: 120,
|
||||
};
|
||||
if (body.stream) {
|
||||
response.setHeader("Content-Type", "text/event-stream");
|
||||
const emit = (delta, finishReason = null) =>
|
||||
response.write(
|
||||
`data: ${JSON.stringify({ ...base, object: "chat.completion.chunk", choices: [{ index: 0, delta, finish_reason: finishReason }] })}\n\n`,
|
||||
);
|
||||
if (message.tool_calls) {
|
||||
const fragments = message.tool_calls.map((call, index) => {
|
||||
const split = Math.floor(call.function.arguments.length / 2);
|
||||
return {
|
||||
first: {
|
||||
index,
|
||||
...call,
|
||||
function: {
|
||||
name: call.function.name,
|
||||
arguments: call.function.arguments.slice(0, split),
|
||||
},
|
||||
},
|
||||
second: {
|
||||
index,
|
||||
function: { arguments: call.function.arguments.slice(split) },
|
||||
},
|
||||
};
|
||||
});
|
||||
emit({
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: fragments.map((f) => f.first),
|
||||
});
|
||||
emit({ tool_calls: fragments.map((f) => f.second) });
|
||||
} else emit(message);
|
||||
emit({}, finish);
|
||||
response.write(
|
||||
`data: ${JSON.stringify({ ...base, object: "chat.completion.chunk", choices: [], usage })}\n\n`,
|
||||
);
|
||||
response.end("data: [DONE]\n\n");
|
||||
} else {
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
...base,
|
||||
choices: [{ index: 0, message, finish_reason: finish }],
|
||||
usage,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (cause) {
|
||||
failure = String(cause);
|
||||
response.statusCode = 400;
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
error: { message: failure, type: "invalid_request_error" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
assert(address !== null && typeof address !== "string");
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${address.port}/v1`,
|
||||
requests,
|
||||
probes,
|
||||
get failure() {
|
||||
return failure;
|
||||
},
|
||||
close: () => new Promise((resolve) => server.close(resolve)),
|
||||
};
|
||||
};
|
||||
@@ -88,7 +88,10 @@ window.loadFile("renderer.html");require("node:fs").writeFileSync(${JSON.stringi
|
||||
ajv.compile(tool.inputSchema);
|
||||
return [
|
||||
tool.name,
|
||||
tool.outputSchema ? ajv.compile(tool.outputSchema) : undefined,
|
||||
tool.outputSchema
|
||||
? (tool.outputSchema.$id && ajv.getSchema(tool.outputSchema.$id)) ||
|
||||
ajv.compile(tool.outputSchema)
|
||||
: undefined,
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -15,10 +15,10 @@ import { buildRecoveryFixtures } from "../../fixtures/javascript-recovery/build.
|
||||
import { mcpTextValue } from "../../lib/mcp-verifier-results.mjs";
|
||||
|
||||
const OFFICIAL_BINARY_SHA256 =
|
||||
"e8c7ca052974604197389f6ace7464e4dd24dce64e2730971521bd6606ec9c9e";
|
||||
"664fa06782a61eda7ed15885134b52525fd565dd77607e642062ceb6cd927c38";
|
||||
if (!process.env.REA_WAKARU_COMMAND)
|
||||
throw new Error(
|
||||
"Provide REA_WAKARU_COMMAND for the official Wakaru 1.13.0 Linux x64 binary",
|
||||
"Provide REA_WAKARU_COMMAND for the official Wakaru 1.14.0 Linux x64 binary",
|
||||
);
|
||||
const entrypoint =
|
||||
process.argv[2] ?? fileURLToPath(new URL("../../rea.mjs", import.meta.url));
|
||||
@@ -130,7 +130,7 @@ try {
|
||||
}
|
||||
}
|
||||
finalReport = {
|
||||
engine: "wakaru 1.13.0",
|
||||
engine: "wakaru 1.14.0",
|
||||
toolchains: fixtures.toolchains,
|
||||
results: counts,
|
||||
verified: true,
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
"""Maintain shared HTML and run the static website's contributor commands."""
|
||||
|
||||
import argparse
|
||||
from functools import partial
|
||||
from html import escape
|
||||
from html.parser import HTMLParser
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path, PurePosixPath
|
||||
from string import Template
|
||||
import subprocess
|
||||
import sys
|
||||
from textwrap import indent
|
||||
from urllib.parse import quote, urlsplit
|
||||
|
||||
|
||||
SITE_ORIGIN = "https://rea.tools"
|
||||
NOTES_FIXTURES = {"examples/notes-electron/index.html", "examples/notes-web/index.html"}
|
||||
NAVIGATION = (
|
||||
("Showcases", "showcase/"),
|
||||
("Blog", "blog/"),
|
||||
("Get started", "get-started/"),
|
||||
("Guides", "guides/"),
|
||||
("FAQ", "faq/"),
|
||||
)
|
||||
|
||||
|
||||
def authored_markdown(root):
|
||||
"""Discover website documentation while excluding local environments and vendors."""
|
||||
site = root / "website"
|
||||
return [
|
||||
path for path in sorted(site.rglob("*.md"))
|
||||
if not any(part.startswith(".") or part in ("node_modules", "__pycache__")
|
||||
for part in path.relative_to(site).parts)
|
||||
]
|
||||
|
||||
|
||||
class PageMetadata(HTMLParser):
|
||||
"""Read the authored title, description and indexing policy from the head."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.in_head = False
|
||||
self.in_title = False
|
||||
self.titles = []
|
||||
self.descriptions = []
|
||||
self.noindex = False
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == "head":
|
||||
self.in_head = True
|
||||
if not self.in_head:
|
||||
return
|
||||
if tag == "title":
|
||||
self.in_title = True
|
||||
self.titles.append("")
|
||||
if tag == "meta":
|
||||
attributes = dict(attrs)
|
||||
name = (attributes.get("name") or "").lower()
|
||||
content = attributes.get("content") or ""
|
||||
if name == "description":
|
||||
self.descriptions.append(content)
|
||||
if name in ("robots", "googlebot"):
|
||||
self.noindex |= bool(
|
||||
set(content.lower().replace(",", " ").split()) & {"noindex", "none"}
|
||||
)
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == "head":
|
||||
self.in_head = False
|
||||
if tag == "title":
|
||||
self.in_title = False
|
||||
|
||||
def handle_data(self, data):
|
||||
if self.in_title:
|
||||
self.titles[-1] += data
|
||||
|
||||
|
||||
def page_route(site, path):
|
||||
"""Map an HTML file to its public route without a separate page registry."""
|
||||
route = path.relative_to(site).as_posix()
|
||||
return route.removesuffix("index.html") if path.name == "index.html" else route
|
||||
|
||||
|
||||
def page_url(site, path):
|
||||
"""Return the canonical public URL of an authored HTML file."""
|
||||
return f"{SITE_ORIGIN}/{quote(page_route(site, path), safe='/')}"
|
||||
|
||||
|
||||
def shared_blocks(root, path, source):
|
||||
"""Render shared regions while keeping the page's body and metadata authored."""
|
||||
site = root / "website/public"
|
||||
metadata = PageMetadata()
|
||||
metadata.feed(source)
|
||||
for name, values in (("title", metadata.titles), ("description", metadata.descriptions)):
|
||||
if len(values) != 1 or not values[0].strip():
|
||||
raise ValueError(f"{path.relative_to(site)}: needs one nonempty head {name}")
|
||||
depth = len(path.relative_to(site).parent.parts)
|
||||
base = "../" * depth or "./"
|
||||
route = page_route(site, path)
|
||||
links = []
|
||||
for label, destination in NAVIGATION:
|
||||
attributes = ' class="nav-start"' if destination == "get-started/" else ""
|
||||
if route.startswith(destination):
|
||||
attributes += ' aria-current="page"'
|
||||
links.append(f' <a href="{base}{destination}"{attributes}>{label}</a>')
|
||||
values = {
|
||||
"base": base,
|
||||
"navigation": "\n" + "\n".join(links) + "\n ",
|
||||
"canonical": escape(page_url(site, path), quote=True),
|
||||
"origin": SITE_ORIGIN,
|
||||
"title": escape(metadata.titles[0].strip(), quote=True),
|
||||
"description": escape(metadata.descriptions[0].strip(), quote=True),
|
||||
}
|
||||
templates = root / "website/templates"
|
||||
return {
|
||||
name: indent(Template((templates / f"{name}.html.tmpl").read_text(encoding="utf-8")).substitute(values), " ")
|
||||
for name in ("sharing", "header", "footer")
|
||||
}
|
||||
|
||||
|
||||
def sync_pages(root, check=False):
|
||||
"""Update shared regions, or reject drift without changing any authored file."""
|
||||
site = root / "website/public"
|
||||
changes = []
|
||||
for path in sorted(site.rglob("*.html")):
|
||||
if path.relative_to(site).as_posix() in NOTES_FIXTURES:
|
||||
continue
|
||||
original = path.read_text(encoding="utf-8")
|
||||
source = original
|
||||
for name, block in shared_blocks(root, path, source).items():
|
||||
start = f" <!-- website:{name}:start -->\n"
|
||||
end = f" <!-- website:{name}:end -->"
|
||||
if source.count(start) != 1 or source.count(end) != 1:
|
||||
raise ValueError(f"{path.relative_to(site)}: needs one {name} region; use new-page for a new reading page")
|
||||
before, _, remaining = source.partition(start)
|
||||
_, found, after = remaining.partition(end)
|
||||
if not found:
|
||||
raise ValueError(f"{path.relative_to(site)}: {name} region is out of order")
|
||||
source = before + start + block + end + after
|
||||
if source != original:
|
||||
changes.append((path, source))
|
||||
if check and changes:
|
||||
names = ", ".join(str(path.relative_to(site)) for path, _ in changes)
|
||||
raise ValueError(f"Shared HTML is stale: {names}. Run python3 scripts/website.py sync and commit the updated pages.")
|
||||
if not check:
|
||||
for path, source in changes:
|
||||
path.write_text(source, encoding="utf-8")
|
||||
print(f"{'Checked' if check else 'Synced'} shared HTML ({len(changes)} pages changed).")
|
||||
return [path for path, _ in changes]
|
||||
|
||||
|
||||
def new_page(root, route, title, description):
|
||||
"""Create a draft reading page with correct depth, metadata and shared regions."""
|
||||
parts = PurePosixPath(route)
|
||||
if not route or parts.is_absolute() or ".." in parts.parts or "\\" in route or parts.suffix:
|
||||
raise ValueError("Use a relative directory route, such as blog/my-article.")
|
||||
site = root / "website/public"
|
||||
path = site.joinpath(*parts.parts, "index.html")
|
||||
if not path.resolve().is_relative_to(site.resolve()):
|
||||
raise ValueError("The new page must stay inside website/public.")
|
||||
if path.exists():
|
||||
raise ValueError(f"Page already exists: {path.relative_to(site)}")
|
||||
base = "../" * len(parts.parts) or "./"
|
||||
source = Template((root / "website/templates/page.html").read_text(encoding="utf-8")).substitute(
|
||||
title=escape(title, quote=True), description=escape(description, quote=True), base=base,
|
||||
)
|
||||
# Render before writing, so invalid input cannot leave an incomplete page.
|
||||
blocks = shared_blocks(root, path, source)
|
||||
for name, block in blocks.items():
|
||||
source = source.replace(f" <!-- website:{name}:start -->\n", f" <!-- website:{name}:start -->\n{block}")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(source, encoding="utf-8")
|
||||
print(f"Created {path.relative_to(root)}. Write the page, add its index link, then remove the Draft label and noindex directive when ready.")
|
||||
return path
|
||||
|
||||
|
||||
def run_script(root, name, arguments=()):
|
||||
"""Use the active Python environment for the existing preparation and checks."""
|
||||
subprocess.run([sys.executable, str(root / "scripts" / name), *arguments], cwd=root, check=True)
|
||||
|
||||
|
||||
def preview_handler(site, prefix):
|
||||
"""Serve one static directory at root or a Pages prefix, preserving redirects."""
|
||||
class Handler(SimpleHTTPRequestHandler):
|
||||
def send_head(self):
|
||||
if not urlsplit(self.path).path.startswith(prefix):
|
||||
self.send_error(404)
|
||||
return None
|
||||
return super().send_head()
|
||||
|
||||
def translate_path(self, path):
|
||||
return super().translate_path("/" + path[len(prefix):])
|
||||
|
||||
return partial(Handler, directory=str(site))
|
||||
|
||||
|
||||
def main():
|
||||
"""Expose one small command for synchronizing, checking and previewing the site."""
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
commands = parser.add_subparsers(dest="command", required=True)
|
||||
sync = commands.add_parser("sync", help="Update shared HTML after editing templates or page metadata")
|
||||
sync.add_argument("--check", action="store_true", help="Report drift without rewriting pages")
|
||||
commands.add_parser("check", help="Check shared HTML, prepare assets, verify links and run regression tests")
|
||||
links = commands.add_parser("links", help="Audit external targets using gh and HTTP; separate from offline checks")
|
||||
links.add_argument("--report", help="Write the full external-link JSON report")
|
||||
create = commands.add_parser("new-page", help="Create a draft reading page")
|
||||
create.add_argument("route")
|
||||
create.add_argument("--title", required=True)
|
||||
create.add_argument("--description", required=True)
|
||||
serve = commands.add_parser("serve", help="Prepare and preview the static site")
|
||||
serve.add_argument("--port", type=int, default=4173)
|
||||
serve.add_argument("--base-path", choices=("/", "/rea/"), default="/")
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
try:
|
||||
if args.command == "new-page":
|
||||
new_page(root, args.route, args.title, args.description)
|
||||
elif args.command == "sync":
|
||||
sync_pages(root, check=args.check)
|
||||
elif args.command == "check":
|
||||
sync_pages(root, check=True)
|
||||
for name in ("prepare-website.py", "verify-website.py", "test-website.py"):
|
||||
run_script(root, name)
|
||||
elif args.command == "links":
|
||||
run_script(root, "audit-website-links.py", ["--report", args.report] if args.report else [])
|
||||
else:
|
||||
sync_pages(root)
|
||||
run_script(root, "prepare-website.py")
|
||||
prefix = args.base_path
|
||||
|
||||
handler = preview_handler(root / "website/public", prefix)
|
||||
with ThreadingHTTPServer(("127.0.0.1", args.port), handler) as server:
|
||||
print(f"Preview: http://127.0.0.1:{server.server_port}{prefix}", flush=True)
|
||||
server.serve_forever()
|
||||
except (OSError, ValueError, KeyError) as error:
|
||||
print(str(error), file=sys.stderr)
|
||||
return 1
|
||||
except subprocess.CalledProcessError as error:
|
||||
return error.returncode
|
||||
except KeyboardInterrupt:
|
||||
return 0
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
ADB_BINARY_ENV,
|
||||
parseAdbVersionOutput,
|
||||
resolveAdbBinary,
|
||||
} from "./AdbConfiguration.js";
|
||||
|
||||
describe("resolveAdbBinary", () => {
|
||||
it("prefers an absolute REA_ADB_PATH", () => {
|
||||
expect(resolveAdbBinary({ [ADB_BINARY_ENV]: "/opt/tools/adb" })).toEqual({
|
||||
binary: "/opt/tools/adb",
|
||||
pathSource: "environment",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to PATH resolution without the environment key", () => {
|
||||
expect(resolveAdbBinary({})).toEqual({
|
||||
binary: "adb",
|
||||
pathSource: "path",
|
||||
});
|
||||
expect(resolveAdbBinary({ [ADB_BINARY_ENV]: " " })).toEqual({
|
||||
binary: "adb",
|
||||
pathSource: "path",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a relative environment path explicitly", () => {
|
||||
expect(() => resolveAdbBinary({ [ADB_BINARY_ENV]: "tools/adb" })).toThrow(
|
||||
/must be an absolute path/u,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseAdbVersionOutput", () => {
|
||||
it("reads the build version and install path from real output", () => {
|
||||
expect(
|
||||
parseAdbVersionOutput(
|
||||
[
|
||||
"Android Debug Bridge version 1.0.41",
|
||||
"Version 34.0.5-debian",
|
||||
"Installed as /usr/lib/android-sdk/platform-tools/adb",
|
||||
"",
|
||||
].join("\n"),
|
||||
),
|
||||
).toEqual({
|
||||
version: "34.0.5-debian",
|
||||
installedPath: "/usr/lib/android-sdk/platform-tools/adb",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports null identity fields when the named lines are absent", () => {
|
||||
expect(parseAdbVersionOutput("unexpected output")).toEqual({
|
||||
version: null,
|
||||
installedPath: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import { isAbsolute } from "node:path";
|
||||
|
||||
import {
|
||||
execFileOutput,
|
||||
execFileOutputFailure,
|
||||
} from "../process/ExecFileOutput.js";
|
||||
import type { ProviderAvailability } from "../application/AnalysisProvider.js";
|
||||
import type { ProviderRejectionCode } from "../contracts/providerSelection.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
|
||||
/** Environment key selecting the caller's adb binary. */
|
||||
export const ADB_BINARY_ENV = "REA_ADB_PATH";
|
||||
|
||||
const VERSION_TIMEOUT_MS = 10_000;
|
||||
const VERSION_MAX_OUTPUT_BYTES = 64 * 1024;
|
||||
|
||||
/** How the adb binary was selected for this provider instance. */
|
||||
export interface AdbBinarySelection {
|
||||
readonly binary: string;
|
||||
readonly pathSource: "environment" | "path";
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the caller-supplied adb binary. `REA_ADB_PATH` must be absolute so
|
||||
* the selection stays explicit; without it, adb is resolved from `PATH` by
|
||||
* the platform exec at run time, like any bring-your-own tool.
|
||||
*/
|
||||
export const resolveAdbBinary = (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): AdbBinarySelection => {
|
||||
const configured = environment[ADB_BINARY_ENV];
|
||||
if (configured === undefined || configured.trim() === "")
|
||||
return { binary: "adb", pathSource: "path" };
|
||||
if (!isAbsolute(configured))
|
||||
throw new AdbConfigurationFailure(
|
||||
"not_absolute",
|
||||
`${ADB_BINARY_ENV} must be an absolute path to the adb binary`,
|
||||
{ configured_path: configured },
|
||||
);
|
||||
return { binary: configured, pathSource: "environment" };
|
||||
};
|
||||
|
||||
export type AdbConfigurationFailureCode =
|
||||
| "not_absolute"
|
||||
| "binary_missing"
|
||||
| "version_unresolved";
|
||||
|
||||
export class AdbConfigurationFailure extends Error {
|
||||
constructor(
|
||||
readonly code: AdbConfigurationFailureCode,
|
||||
message: string,
|
||||
readonly diagnostics: Readonly<Record<string, JsonValue>> = {},
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
super(message, options);
|
||||
}
|
||||
}
|
||||
|
||||
export interface AdbClientIdentity {
|
||||
readonly version: string | null;
|
||||
readonly installedPath: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `adb version` output. The first line carries the protocol version,
|
||||
* `Version ` the build version (no colon), and `Installed as` the binary path.
|
||||
*/
|
||||
export const parseAdbVersionOutput = (stdout: string): AdbClientIdentity => {
|
||||
const build = /^Version\s+(\S.*)$/mu.exec(stdout);
|
||||
const installed = /^Installed as\s+(.+)$/mu.exec(stdout);
|
||||
return {
|
||||
version: build === null ? null : build[1]!.trim(),
|
||||
installedPath: installed === null ? null : installed[1]!.trim(),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Probe the selected adb binary without contacting devices. `adb version`
|
||||
* never starts the adb server, so this stays a pure client check.
|
||||
*/
|
||||
export const inspectAdbClient = async (
|
||||
selection: AdbBinarySelection,
|
||||
signal?: AbortSignal,
|
||||
): Promise<AdbClientIdentity> => {
|
||||
try {
|
||||
const { stdout } = await execFileOutput(selection.binary, ["version"], {
|
||||
timeout: VERSION_TIMEOUT_MS,
|
||||
stopSignal: "SIGTERM",
|
||||
maxBuffer: VERSION_MAX_OUTPUT_BYTES,
|
||||
...(signal === undefined ? {} : { signal }),
|
||||
});
|
||||
return parseAdbVersionOutput(stdout);
|
||||
} catch (cause) {
|
||||
const failure = execFileOutputFailure(cause);
|
||||
if (signal?.aborted || failure?.code === "ABORT_ERR")
|
||||
throw new AnalysisCancelledError("inspect_adb_client", { cause });
|
||||
if (failure?.code === "ENOENT")
|
||||
throw new AdbConfigurationFailure(
|
||||
"binary_missing",
|
||||
`The selected adb binary was not found: ${selection.binary}`,
|
||||
{
|
||||
selected_binary: selection.binary,
|
||||
path_source: selection.pathSource,
|
||||
remediation: `Set ${ADB_BINARY_ENV} to an absolute path to an existing adb binary`,
|
||||
},
|
||||
{ cause },
|
||||
);
|
||||
throw new AdbConfigurationFailure(
|
||||
"version_unresolved",
|
||||
"The selected adb binary did not report a usable version",
|
||||
{
|
||||
selected_binary: selection.binary,
|
||||
...(failure === undefined
|
||||
? {}
|
||||
: {
|
||||
exit_code: failure.code,
|
||||
stderr_excerpt: failure.stderr.slice(0, 512),
|
||||
}),
|
||||
},
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** Availability in the shape shared by every provider. */
|
||||
const rejectionCodeOf = (
|
||||
code: AdbConfigurationFailureCode,
|
||||
): ProviderRejectionCode =>
|
||||
code === "binary_missing"
|
||||
? "executable_missing"
|
||||
: code === "not_absolute"
|
||||
? "not_configured"
|
||||
: "version_unresolved";
|
||||
|
||||
export const adbConfigurationAvailability = (
|
||||
cause: AdbConfigurationFailure,
|
||||
): ProviderAvailability =>
|
||||
({
|
||||
status: "unavailable",
|
||||
code: rejectionCodeOf(cause.code),
|
||||
reason: cause.message,
|
||||
diagnostics: { ...cause.diagnostics },
|
||||
}) as ProviderAvailability;
|
||||
@@ -0,0 +1,242 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
parseAdbDevicesOutput,
|
||||
parseGetpropOutput,
|
||||
parsePackageListOutput,
|
||||
parsePmPathOutput,
|
||||
} from "./AdbDeviceOutput.js";
|
||||
|
||||
describe("parseAdbDevicesOutput", () => {
|
||||
it("parses emulator, USB, and network devices with their metadata", () => {
|
||||
const output = [
|
||||
"List of devices attached",
|
||||
"emulator-5554 device product:sdk_gphone64_x86_64 model:sdk_gphone64_x86_64 device:emu64xa transport_id:1",
|
||||
"07ef9c2a device usb:1-1 product:raven model:Pixel_6_Pro device:raven transport_id:2",
|
||||
"192.168.1.42:5555 device product:mock model:mock transport_id:3",
|
||||
"",
|
||||
].join("\n");
|
||||
const { devices, unparsedLines, daemonStarted } =
|
||||
parseAdbDevicesOutput(output);
|
||||
expect(unparsedLines).toEqual([]);
|
||||
expect(daemonStarted).toBe(false);
|
||||
expect(devices).toEqual([
|
||||
{
|
||||
serial: "emulator-5554",
|
||||
state: "device",
|
||||
transport: "tcp",
|
||||
product: "sdk_gphone64_x86_64",
|
||||
model: "sdk_gphone64_x86_64",
|
||||
device: "emu64xa",
|
||||
transport_id: 1,
|
||||
kind: "emulator",
|
||||
kind_basis: "emulator_serial_prefix",
|
||||
},
|
||||
{
|
||||
serial: "07ef9c2a",
|
||||
state: "device",
|
||||
transport: "usb",
|
||||
product: "raven",
|
||||
model: "Pixel_6_Pro",
|
||||
device: "raven",
|
||||
transport_id: 2,
|
||||
kind: "physical",
|
||||
kind_basis: "usb_transport",
|
||||
},
|
||||
{
|
||||
serial: "192.168.1.42:5555",
|
||||
state: "device",
|
||||
transport: "tcp",
|
||||
product: "mock",
|
||||
model: "mock",
|
||||
device: null,
|
||||
transport_id: 3,
|
||||
kind: "unknown",
|
||||
kind_basis: "none",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("recognizes daemon startup notices instead of failing the listing", () => {
|
||||
const output = [
|
||||
"* daemon not running; starting now at tcp:5037",
|
||||
"* daemon started successfully",
|
||||
"List of devices attached",
|
||||
"emulator-5554 device transport_id:1",
|
||||
"",
|
||||
].join("\n");
|
||||
const { devices, daemonStarted } = parseAdbDevicesOutput(output);
|
||||
expect(daemonStarted).toBe(true);
|
||||
expect(devices).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("keeps non-device states such as unauthorized and offline", () => {
|
||||
const output = [
|
||||
"List of devices attached",
|
||||
"emulator-5556 offline transport_id:2",
|
||||
"HT84L0240512 unauthorized usb:1-2 transport_id:3",
|
||||
"",
|
||||
].join("\n");
|
||||
const { devices } = parseAdbDevicesOutput(output);
|
||||
expect(devices.map((device) => [device.serial, device.state])).toEqual([
|
||||
["emulator-5556", "offline"],
|
||||
["HT84L0240512", "unauthorized"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns unrecognized lines instead of dropping them", () => {
|
||||
const output = [
|
||||
"List of devices attached",
|
||||
"this is not a device line",
|
||||
"",
|
||||
].join("\n");
|
||||
const { unparsedLines } = parseAdbDevicesOutput(output);
|
||||
expect(unparsedLines).toEqual(["this is not a device line"]);
|
||||
});
|
||||
|
||||
it("reports a non-numeric transport_id as unknown", () => {
|
||||
const output = [
|
||||
"List of devices attached",
|
||||
"emulator-5554 device transport_id:not-a-number",
|
||||
"",
|
||||
].join("\n");
|
||||
const { devices } = parseAdbDevicesOutput(output);
|
||||
expect(devices[0]?.transport_id).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseGetpropOutput", () => {
|
||||
it("projects the whitelisted build identity from a real dump", () => {
|
||||
const dump = [
|
||||
"[ro.build.version.release]: [14]",
|
||||
"[ro.build.version.sdk]: [34]",
|
||||
"[ro.build.version.security_patch]: [2024-06-05]",
|
||||
"[ro.build.id]: [AP21.240617.014]",
|
||||
"[ro.build.fingerprint]: [google/sdk_gphone64_x86_64/emu64xa:14/AP21.240617.014/12094027:userdebug/dev-keys]",
|
||||
"[ro.product.brand]: [google]",
|
||||
"[ro.product.device]: [emu64xa]",
|
||||
"[ro.product.manufacturer]: [Google]",
|
||||
"[ro.product.model]: [sdk_gphone64_x86_64]",
|
||||
"[ro.product.cpu.abilist]: [x86_64,x86,arm64-v8a,armeabi-v7a,armeabi]",
|
||||
"[ro.kernel.qemu]: [1]",
|
||||
"[ro.debuggable]: [1]",
|
||||
"[sys.unlisted.property]: [dropped]",
|
||||
"",
|
||||
].join("\n");
|
||||
const { properties, emulatorObserved } = parseGetpropOutput(dump);
|
||||
expect(emulatorObserved).toBe(true);
|
||||
expect(properties).not.toContainEqual({
|
||||
name: "sys.unlisted.property",
|
||||
value: "dropped",
|
||||
});
|
||||
expect(properties).toEqual([
|
||||
{ name: "ro.build.version.release", value: "14" },
|
||||
{ name: "ro.build.version.sdk", value: "34" },
|
||||
{ name: "ro.build.version.security_patch", value: "2024-06-05" },
|
||||
{ name: "ro.build.id", value: "AP21.240617.014" },
|
||||
{
|
||||
name: "ro.build.fingerprint",
|
||||
value:
|
||||
"google/sdk_gphone64_x86_64/emu64xa:14/AP21.240617.014/12094027:userdebug/dev-keys",
|
||||
},
|
||||
{ name: "ro.debuggable", value: "1" },
|
||||
{ name: "ro.kernel.qemu", value: "1" },
|
||||
{ name: "ro.product.brand", value: "google" },
|
||||
{ name: "ro.product.device", value: "emu64xa" },
|
||||
{ name: "ro.product.manufacturer", value: "Google" },
|
||||
{ name: "ro.product.model", value: "sdk_gphone64_x86_64" },
|
||||
{
|
||||
name: "ro.product.cpu.abilist",
|
||||
value: "x86_64,x86,arm64-v8a,armeabi-v7a,armeabi",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports an explicit non-emulator build without inventing a value", () => {
|
||||
const dump = [
|
||||
"[ro.build.version.release]: [13]",
|
||||
"[ro.kernel.qemu]: [0]",
|
||||
"",
|
||||
].join("\n");
|
||||
const { emulatorObserved } = parseGetpropOutput(dump);
|
||||
expect(emulatorObserved).toBe(false);
|
||||
});
|
||||
|
||||
it("reports the emulator indicator unknown when the build omits it", () => {
|
||||
const { emulatorObserved } = parseGetpropOutput(
|
||||
"[ro.build.version.sdk]: [33]\n",
|
||||
);
|
||||
expect(emulatorObserved).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("parsePackageListOutput", () => {
|
||||
it("parses package lines with their base APK device paths", () => {
|
||||
const output = [
|
||||
"package:/data/app/~~xy==/com.example.npzyCvVb1I-YzKIsGFvVYw==/base.apk=com.example",
|
||||
"package:/system/app/WebView/WebView.apk=com.android.webview",
|
||||
"",
|
||||
].join("\n");
|
||||
const { packages, unparsedLines } = parsePackageListOutput(output);
|
||||
expect(unparsedLines).toEqual([]);
|
||||
expect(packages).toEqual([
|
||||
{
|
||||
package_name: "com.example",
|
||||
base_apk_device_path:
|
||||
"/data/app/~~xy==/com.example.npzyCvVb1I-YzKIsGFvVYw==/base.apk",
|
||||
},
|
||||
{
|
||||
package_name: "com.android.webview",
|
||||
base_apk_device_path: "/system/app/WebView/WebView.apk",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps unrecognized lines for partial coverage reporting", () => {
|
||||
const { packages, unparsedLines } = parsePackageListOutput(
|
||||
"package:/data/app/x/base.apk=com.ok\ngarbage line\n",
|
||||
);
|
||||
expect(packages).toHaveLength(1);
|
||||
expect(unparsedLines).toEqual(["garbage line"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parsePmPathOutput", () => {
|
||||
it("classifies base and split APKs from their file names", () => {
|
||||
const output = [
|
||||
"package:/data/app/~~a==/com.example.b==/base.apk",
|
||||
"package:/data/app/~~a==/com.example.b==/split_config.arm64_v8a.apk",
|
||||
"package:/data/app/~~a==/com.example.b==/split_config.en.apk",
|
||||
"",
|
||||
].join("\n");
|
||||
const { paths, unparsedLines } = parsePmPathOutput(output);
|
||||
expect(unparsedLines).toEqual([]);
|
||||
expect(paths).toEqual([
|
||||
{
|
||||
device_path: "/data/app/~~a==/com.example.b==/base.apk",
|
||||
file_name: "base.apk",
|
||||
role: "base",
|
||||
},
|
||||
{
|
||||
device_path:
|
||||
"/data/app/~~a==/com.example.b==/split_config.arm64_v8a.apk",
|
||||
file_name: "split_config.arm64_v8a.apk",
|
||||
role: "split",
|
||||
},
|
||||
{
|
||||
device_path: "/data/app/~~a==/com.example.b==/split_config.en.apk",
|
||||
file_name: "split_config.en.apk",
|
||||
role: "split",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports an unknown role for names that follow neither convention", () => {
|
||||
const { paths } = parsePmPathOutput("package:/system/app/X/Y.apk\n");
|
||||
expect(paths[0]).toEqual({
|
||||
device_path: "/system/app/X/Y.apk",
|
||||
file_name: "Y.apk",
|
||||
role: "unknown",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* Parsers for the exact adb and Package Manager output formats REA consumes.
|
||||
*
|
||||
* Each parser preserves what the tool actually printed: unparseable lines are
|
||||
* retained rather than silently dropped, and derived classifications carry
|
||||
* their evidence basis.
|
||||
*/
|
||||
|
||||
/** One device exactly as `adb devices -l` reports it. */
|
||||
export interface AdbDeviceEntry {
|
||||
readonly serial: string;
|
||||
readonly state: string;
|
||||
readonly transport: "usb" | "tcp" | "unknown";
|
||||
readonly product: string | null;
|
||||
readonly model: string | null;
|
||||
readonly device: string | null;
|
||||
readonly transport_id: number | null;
|
||||
readonly kind: "emulator" | "physical" | "unknown";
|
||||
readonly kind_basis: "emulator_serial_prefix" | "usb_transport" | "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* Device states adb prints after the serial, longest first so two-word
|
||||
* states match before their single-word prefixes. A line whose remainder
|
||||
* starts with none of them is not a device line.
|
||||
*/
|
||||
const DEVICE_STATES = [
|
||||
"no permissions",
|
||||
"unauthorized",
|
||||
"authorizing",
|
||||
"offline",
|
||||
"recovery",
|
||||
"sideload",
|
||||
"connecting",
|
||||
"bootloader",
|
||||
"device",
|
||||
"unknown",
|
||||
"host",
|
||||
] as const;
|
||||
|
||||
const DEVICE_LINE = /^(\S+)\s+(.*)$/u;
|
||||
|
||||
const metadataValue = (remainder: string, key: string): string | null => {
|
||||
const match = new RegExp(`(?:^|\\s)${key}:(\\S+)`, "u").exec(remainder);
|
||||
return match === null ? null : match[1]!;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse `adb devices -l` device lines. The banner, blank lines, and the
|
||||
* daemon startup notices adb prints on first use are recognized separately;
|
||||
* every other unrecognized line is returned for honest reporting.
|
||||
*/
|
||||
export const parseAdbDevicesOutput = (
|
||||
stdout: string,
|
||||
): {
|
||||
devices: readonly AdbDeviceEntry[];
|
||||
unparsedLines: readonly string[];
|
||||
daemonStarted: boolean;
|
||||
} => {
|
||||
const devices: AdbDeviceEntry[] = [];
|
||||
const unparsedLines: string[] = [];
|
||||
let daemonStarted = false;
|
||||
for (const rawLine of stdout.split(/\r?\n/u)) {
|
||||
const line = rawLine.trimEnd();
|
||||
if (line === "" || line.startsWith("List of devices attached")) continue;
|
||||
if (line.startsWith("* daemon")) {
|
||||
if (line.includes("started successfully")) daemonStarted = true;
|
||||
continue;
|
||||
}
|
||||
const match = DEVICE_LINE.exec(line);
|
||||
const state = DEVICE_STATES.find((candidate) =>
|
||||
match?.[2]?.startsWith(candidate),
|
||||
);
|
||||
if (match === null || state === undefined) {
|
||||
if (line.trim() !== "") unparsedLines.push(line);
|
||||
continue;
|
||||
}
|
||||
const serial = match[1]!;
|
||||
const remainder = match[2]!.slice(state.length);
|
||||
const usb = metadataValue(remainder, "usb");
|
||||
const transportIdText = metadataValue(remainder, "transport_id");
|
||||
const parsedTransportId =
|
||||
transportIdText === null || !/^\d+$/u.test(transportIdText)
|
||||
? null
|
||||
: Number(transportIdText);
|
||||
const isEmulator = /^emulator-\d+$/u.test(serial);
|
||||
const kind = isEmulator
|
||||
? ("emulator" as const)
|
||||
: usb !== null
|
||||
? ("physical" as const)
|
||||
: ("unknown" as const);
|
||||
devices.push({
|
||||
serial,
|
||||
state,
|
||||
transport:
|
||||
usb !== null
|
||||
? "usb"
|
||||
: isEmulator || serial.includes(":")
|
||||
? "tcp"
|
||||
: "unknown",
|
||||
product: metadataValue(remainder, "product"),
|
||||
model: metadataValue(remainder, "model"),
|
||||
device: metadataValue(remainder, "device"),
|
||||
transport_id: parsedTransportId,
|
||||
kind,
|
||||
kind_basis:
|
||||
kind === "emulator"
|
||||
? "emulator_serial_prefix"
|
||||
: kind === "physical"
|
||||
? "usb_transport"
|
||||
: "none",
|
||||
});
|
||||
}
|
||||
return { devices, unparsedLines, daemonStarted };
|
||||
};
|
||||
|
||||
/**
|
||||
* Properties projected from a `getprop` dump. The whitelist keeps device
|
||||
* inspection to build identity facts; a full dump is out of scope.
|
||||
*/
|
||||
const DEVICE_PROPERTY_WHITELIST = [
|
||||
"ro.build.version.release",
|
||||
"ro.build.version.sdk",
|
||||
"ro.build.version.security_patch",
|
||||
"ro.build.id",
|
||||
"ro.build.fingerprint",
|
||||
"ro.build.characteristics",
|
||||
"ro.debuggable",
|
||||
"ro.kernel.qemu",
|
||||
"ro.product.brand",
|
||||
"ro.product.device",
|
||||
"ro.product.manufacturer",
|
||||
"ro.product.model",
|
||||
"ro.product.cpu.abilist",
|
||||
] as const;
|
||||
|
||||
export interface AdbDeviceProperty {
|
||||
readonly name: string;
|
||||
readonly value: string;
|
||||
}
|
||||
|
||||
/** Parse a full `getprop` dump into `[name]: [value]` pairs. */
|
||||
export const parseGetpropOutput = (
|
||||
stdout: string,
|
||||
): {
|
||||
properties: readonly AdbDeviceProperty[];
|
||||
emulatorObserved: boolean | null;
|
||||
} => {
|
||||
const values = new Map<string, string>();
|
||||
for (const rawLine of stdout.split(/\r?\n/u)) {
|
||||
const match = /^\[([^\]]+)\]:\s*\[(.*)\]$/u.exec(rawLine.trim());
|
||||
if (match === null) continue;
|
||||
values.set(match[1]!, match[2]!);
|
||||
}
|
||||
const properties = DEVICE_PROPERTY_WHITELIST.flatMap((name) => {
|
||||
const value = values.get(name);
|
||||
return value === undefined || value === ""
|
||||
? []
|
||||
: [{ name, value } satisfies AdbDeviceProperty];
|
||||
});
|
||||
const qemu = values.get("ro.kernel.qemu");
|
||||
return {
|
||||
properties,
|
||||
emulatorObserved: qemu === undefined ? null : qemu === "1",
|
||||
};
|
||||
};
|
||||
|
||||
/** One installed package exactly as `pm list packages -f` reports it. */
|
||||
export interface AdbPackageEntry {
|
||||
readonly package_name: string;
|
||||
readonly base_apk_device_path: string;
|
||||
}
|
||||
|
||||
const PACKAGE_LINE = /^package:(.+)=([^\s=]+)$/u;
|
||||
|
||||
/** Parse `pm list packages -f` lines, retaining unrecognized lines. */
|
||||
export const parsePackageListOutput = (
|
||||
stdout: string,
|
||||
): {
|
||||
packages: readonly AdbPackageEntry[];
|
||||
unparsedLines: readonly string[];
|
||||
} => {
|
||||
const packages: AdbPackageEntry[] = [];
|
||||
const unparsedLines: string[] = [];
|
||||
for (const rawLine of stdout.split(/\r?\n/u)) {
|
||||
const line = rawLine.trim();
|
||||
if (line === "") continue;
|
||||
const match = PACKAGE_LINE.exec(line);
|
||||
if (match === null) {
|
||||
unparsedLines.push(line);
|
||||
continue;
|
||||
}
|
||||
packages.push({
|
||||
package_name: match[2]!,
|
||||
base_apk_device_path: match[1]!,
|
||||
});
|
||||
}
|
||||
return { packages, unparsedLines };
|
||||
};
|
||||
|
||||
/** One APK of an installed package exactly as `pm path` reports it. */
|
||||
export interface AdbPackagePathEntry {
|
||||
readonly device_path: string;
|
||||
readonly file_name: string;
|
||||
readonly role: "base" | "split" | "unknown";
|
||||
}
|
||||
|
||||
const fileNameOf = (devicePath: string): string => {
|
||||
const parts = devicePath.split(/\//u);
|
||||
return parts[parts.length - 1] ?? devicePath;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse `pm path PACKAGE` lines. The base/split role is derived from the file
|
||||
* name (`base.apk`, `split_*.apk`), which is the only signal the output
|
||||
* carries; any other name reports an unknown role.
|
||||
*/
|
||||
export const parsePmPathOutput = (
|
||||
stdout: string,
|
||||
): {
|
||||
paths: readonly AdbPackagePathEntry[];
|
||||
unparsedLines: readonly string[];
|
||||
} => {
|
||||
const paths: AdbPackagePathEntry[] = [];
|
||||
const unparsedLines: string[] = [];
|
||||
for (const rawLine of stdout.split(/\r?\n/u)) {
|
||||
const line = rawLine.trim();
|
||||
if (line === "") continue;
|
||||
const match = /^package:(\S+)$/u.exec(line);
|
||||
if (match === null) {
|
||||
unparsedLines.push(line);
|
||||
continue;
|
||||
}
|
||||
const devicePath = match[1]!;
|
||||
const fileName = fileNameOf(devicePath);
|
||||
paths.push({
|
||||
device_path: devicePath,
|
||||
file_name: fileName,
|
||||
role:
|
||||
fileName === "base.apk"
|
||||
? "base"
|
||||
: fileName.startsWith("split_") && fileName.endsWith(".apk")
|
||||
? "split"
|
||||
: "unknown",
|
||||
});
|
||||
}
|
||||
return { paths, unparsedLines };
|
||||
};
|
||||
@@ -0,0 +1,960 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
chmod,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, onTestFinished, test } from "vitest";
|
||||
|
||||
import { AdbProvider } from "./AdbProvider.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { AdbRequest } from "../domain/adb/adbDeviceAnalysis.js";
|
||||
|
||||
const it = test.skipIf(process.platform === "win32");
|
||||
|
||||
const VERSION_BLOCK =
|
||||
"printf 'Android Debug Bridge version 1.0.41\\nVersion 34.0.5-stub\\nInstalled as %s\\n' \"$0\"";
|
||||
|
||||
const writeAdbStub = async (
|
||||
body: (root: string) => string,
|
||||
): Promise<{ readonly binary: string; readonly root: string }> => {
|
||||
const root = await mkdtemp(join("/tmp", "rea-adb-"));
|
||||
onTestFinished(() => rm(root, { recursive: true, force: true }));
|
||||
const binary = join(root, "adb");
|
||||
await writeFile(binary, `#!/bin/sh\n${body(root)}\n`, { mode: 0o755 });
|
||||
await chmod(binary, 0o755);
|
||||
return { binary, root };
|
||||
};
|
||||
|
||||
const providerOver = (binary: string): AdbProvider =>
|
||||
new AdbProvider({ environment: { REA_ADB_PATH: binary } });
|
||||
|
||||
const evidenceResult = async (
|
||||
provider: AdbProvider,
|
||||
request: AdbRequest,
|
||||
): Promise<Record<string, unknown>> => {
|
||||
const result = await provider.execute(request);
|
||||
if (!result.ok) throw new Error(result.error.message);
|
||||
return result.value.result as Record<string, unknown>;
|
||||
};
|
||||
|
||||
const refusedBy = async (
|
||||
provider: AdbProvider,
|
||||
request: AdbRequest,
|
||||
): Promise<ReturnType<typeof projectAnalysisError>> => {
|
||||
const result = await provider.execute(request);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("expected a refusal");
|
||||
return projectAnalysisError(result.error);
|
||||
};
|
||||
|
||||
it("inspects the adb client without contacting the server", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; fi`,
|
||||
);
|
||||
const result = await evidenceResult(providerOver(binary), {
|
||||
operation: "inspect_adb_client",
|
||||
input: {},
|
||||
});
|
||||
expect(result.client).toMatchObject({
|
||||
path_source: "environment",
|
||||
version: "34.0.5-stub",
|
||||
installed_path: binary,
|
||||
});
|
||||
expect(result.contacted_adb_server).toBe(false);
|
||||
});
|
||||
|
||||
it("lists devices with inferred kinds and observed daemon startup", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "devices" ]; then
|
||||
printf '* daemon not running; starting now at tcp:5037\\n* daemon started successfully\\n'
|
||||
printf 'List of devices attached\\n'
|
||||
printf 'emulator-5554 device product:sdk_gphone64_x86_64 model:sdk_gphone64_x86_64 device:emu64xa transport_id:1\\n'
|
||||
exit 0
|
||||
fi`,
|
||||
);
|
||||
const result = await evidenceResult(providerOver(binary), {
|
||||
operation: "list_adb_devices",
|
||||
input: {},
|
||||
});
|
||||
expect(result.may_have_started_adb_server).toBe(true);
|
||||
expect(result.devices).toEqual([
|
||||
{
|
||||
serial: "emulator-5554",
|
||||
state: "device",
|
||||
transport: "tcp",
|
||||
product: "sdk_gphone64_x86_64",
|
||||
model: "sdk_gphone64_x86_64",
|
||||
device: "emu64xa",
|
||||
transport_id: 1,
|
||||
kind: "emulator",
|
||||
kind_basis: "emulator_serial_prefix",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("inspects a device through the getprop whitelist", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$3" = "shell" ] && [ "$4" = "getprop" ]; then
|
||||
printf '[ro.build.version.sdk]: [34]\\n[ro.kernel.qemu]: [1]\\n[sys.private]: [x]\\n'
|
||||
exit 0
|
||||
fi`,
|
||||
);
|
||||
const result = await evidenceResult(providerOver(binary), {
|
||||
operation: "inspect_adb_device",
|
||||
input: { serial: "emulator-5554" },
|
||||
});
|
||||
expect(result.emulator_observed).toBe(true);
|
||||
expect(result.properties).toEqual([
|
||||
{ name: "ro.build.version.sdk", value: "34" },
|
||||
{ name: "ro.kernel.qemu", value: "1" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("passes the caller scope as an exact fixed argv vector", async () => {
|
||||
const { binary, root } = await writeAdbStub(
|
||||
(root) => `printf '%s\\n' "$@" >> ${join(root, "argv.log")}
|
||||
if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$3" = "shell" ]; then printf 'package:/data/app/x/base.apk=com.example\\n'; exit 0; fi`,
|
||||
);
|
||||
const result = await evidenceResult(providerOver(binary), {
|
||||
operation: "list_adb_packages",
|
||||
input: { serial: "emulator-5554", scope: "third_party" },
|
||||
});
|
||||
expect(result.coverage).toBe("complete");
|
||||
expect(result.packages).toEqual([
|
||||
{
|
||||
package_name: "com.example",
|
||||
base_apk_device_path: "/data/app/x/base.apk",
|
||||
},
|
||||
]);
|
||||
const logged = (await readFile(join(root, "argv.log"), "utf8"))
|
||||
.trim()
|
||||
.split("\n");
|
||||
expect(logged).toEqual([
|
||||
"version",
|
||||
"-s",
|
||||
"emulator-5554",
|
||||
"shell",
|
||||
"pm",
|
||||
"list",
|
||||
"packages",
|
||||
"-f",
|
||||
"-3",
|
||||
]);
|
||||
});
|
||||
|
||||
const pullStub = (
|
||||
paths: string,
|
||||
pullBody: string,
|
||||
): string => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$4" = "pm" ] && [ "$5" = "path" ]; then
|
||||
printf '%s\\n' ${paths}
|
||||
exit 0
|
||||
fi
|
||||
if [ "$1" = "-s" ] && [ "$3" = "pull" ]; then
|
||||
${pullBody}
|
||||
exit 0
|
||||
fi
|
||||
printf 'unexpected arguments: %s\\n' "$@"
|
||||
exit 1`;
|
||||
|
||||
/** Stub dispatching one canned stdout per first shell word (and optional second). */
|
||||
const shellStub = (
|
||||
dispatch: Readonly<Record<string, string>>,
|
||||
): ((root: string) => string) => {
|
||||
const cases = Object.entries(dispatch)
|
||||
.map(([key, output]) => {
|
||||
const [first, second] = key.split(":");
|
||||
const guard =
|
||||
second === undefined
|
||||
? `[ "$4" = "${first}" ]`
|
||||
: `[ "$4" = "${first}" ] && [ "$5" = "${second}" ]`;
|
||||
return ` if ${guard}; then printf '%b' '${output.replace(/'/gu, "'\\''")}'; exit 0; fi`;
|
||||
})
|
||||
.join("\n");
|
||||
return () => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$3" = "shell" ]; then
|
||||
${cases}
|
||||
printf 'unexpected shell command: %s %s %s\\n' "$4" "$5" "$6" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'unexpected arguments: %s\\n' "$@"
|
||||
exit 1`;
|
||||
};
|
||||
|
||||
it("pulls a complete split set with digests into a new package directory", async () => {
|
||||
const { binary, root } = await writeAdbStub(() =>
|
||||
pullStub(
|
||||
"'package:/data/app/~~a==/com.example.b==/base.apk' 'package:/data/app/~~a==/com.example.b==/split_config.en.apk'",
|
||||
` printf 'apk-bytes-for-testing' > "$5"
|
||||
printf '1 file pulled, 0 skipped.\\n'`,
|
||||
),
|
||||
);
|
||||
const output = join(root, "pulls");
|
||||
const result = await evidenceResult(providerOver(binary), {
|
||||
operation: "pull_adb_package",
|
||||
input: {
|
||||
serial: "emulator-5554",
|
||||
package: "com.example",
|
||||
output_directory: output,
|
||||
},
|
||||
});
|
||||
expect(result.coverage).toBe("complete");
|
||||
expect(result.failures).toEqual([]);
|
||||
const artifacts = result.artifacts as {
|
||||
file_name: string;
|
||||
local_path: string;
|
||||
sha256: string;
|
||||
role: string;
|
||||
}[];
|
||||
expect(
|
||||
artifacts.map((artifact) => [artifact.file_name, artifact.role]),
|
||||
).toEqual([
|
||||
["base.apk", "base"],
|
||||
["split_config.en.apk", "split"],
|
||||
]);
|
||||
const digest = createHash("sha256")
|
||||
.update("apk-bytes-for-testing")
|
||||
.digest("hex");
|
||||
for (const artifact of artifacts) expect(artifact.sha256).toBe(digest);
|
||||
const pulled = await readFile(
|
||||
join(output, "com.example", "base.apk"),
|
||||
"utf8",
|
||||
);
|
||||
expect(pulled).toBe("apk-bytes-for-testing");
|
||||
});
|
||||
|
||||
it("keeps successful artifacts when one split pull fails", async () => {
|
||||
const { binary, root } = await writeAdbStub(() =>
|
||||
pullStub(
|
||||
"'package:/data/app/y/base.apk' 'package:/data/app/y/split_dpi.apk'",
|
||||
`case "$4" in
|
||||
*/split_dpi.apk) printf 'error: closed\\n' >&2; exit 1;;
|
||||
*) printf 'apk-bytes-for-testing' > "$5"; printf '1 file pulled.\\n';;
|
||||
esac`,
|
||||
),
|
||||
);
|
||||
const result = await evidenceResult(providerOver(binary), {
|
||||
operation: "pull_adb_package",
|
||||
input: {
|
||||
serial: "emulator-5554",
|
||||
package: "com.partial",
|
||||
output_directory: join(root, "pulls"),
|
||||
},
|
||||
});
|
||||
expect(result.coverage).toBe("partial");
|
||||
expect(result.artifacts).toHaveLength(1);
|
||||
expect(result.failures).toEqual([
|
||||
{
|
||||
device_path: "/data/app/y/split_dpi.apk",
|
||||
stage: "pull",
|
||||
message: expect.stringContaining("closed") as unknown,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("refuses to reuse an existing package output directory", async () => {
|
||||
const { binary, root } = await writeAdbStub(() =>
|
||||
pullStub("'package:/data/app/z/base.apk'", ` printf 'x' > "$5"`),
|
||||
);
|
||||
const output = join(root, "existing");
|
||||
await mkdir(join(output, "com.example"), { recursive: true });
|
||||
const provider = providerOver(binary);
|
||||
const result = await provider.execute({
|
||||
operation: "pull_adb_package",
|
||||
input: {
|
||||
serial: "emulator-5554",
|
||||
package: "com.example",
|
||||
output_directory: output,
|
||||
},
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "unreadable_output",
|
||||
});
|
||||
expect(result.error.message).toContain("already exists");
|
||||
});
|
||||
|
||||
it("reports an uninstalled package from empty pm path output", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$4" = "pm" ] && [ "$5" = "path" ]; then exit 0; fi`,
|
||||
);
|
||||
const projected = await refusedBy(providerOver(binary), {
|
||||
operation: "pull_adb_package",
|
||||
input: {
|
||||
serial: "emulator-5554",
|
||||
package: "com.absent",
|
||||
output_directory: "/tmp/rea-adb-absent",
|
||||
},
|
||||
});
|
||||
expect(projected).toMatchObject({ code: "unsupported_target" });
|
||||
expect(projected.message).toContain("no APK paths");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["not found", "error: device 'emulator-9' not found", "not found"],
|
||||
["offline", "error: device offline", "offline"],
|
||||
["unauthorized", "error: device unauthorized", "unauthorized"],
|
||||
] as const)(
|
||||
"reports the %s device state as a target refusal",
|
||||
async (_, stderr, excerpt) => {
|
||||
const { binary } = await writeAdbStub(
|
||||
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "-s" ]; then printf '%s\\n' "${stderr.replace(/'/gu, "'\\''")}" >&2; exit 1; fi`,
|
||||
);
|
||||
const projected = await refusedBy(providerOver(binary), {
|
||||
operation: "inspect_adb_device",
|
||||
input: { serial: "emulator-5554" },
|
||||
});
|
||||
expect(projected).toMatchObject({ code: "unsupported_target" });
|
||||
expect(projected.message).toContain(excerpt);
|
||||
},
|
||||
);
|
||||
|
||||
it("reports a missing selected binary as a capability gap with recovery", async () => {
|
||||
const provider = new AdbProvider({
|
||||
environment: { REA_ADB_PATH: "/nonexistent/adb" },
|
||||
});
|
||||
const availability = await provider.inspectAvailability();
|
||||
expect(availability).toMatchObject({ status: "unavailable" });
|
||||
if (availability.status !== "unavailable") return;
|
||||
expect(availability.diagnostics.remediation).toContain("REA_ADB_PATH");
|
||||
const result = await provider.execute({
|
||||
operation: "list_adb_devices",
|
||||
input: {},
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected).toMatchObject({ code: "capability_unavailable" });
|
||||
expect(projected.details).toMatchObject({
|
||||
provider_id: "adb",
|
||||
reason: expect.stringContaining("not found") as unknown,
|
||||
});
|
||||
});
|
||||
|
||||
it("cancels before spawning anything", async () => {
|
||||
const { binary, root } = await writeAdbStub(
|
||||
(root) => `: > ${join(root, "spawned")}
|
||||
if [ "$1" = "version" ]; then ${VERSION_BLOCK}; fi`,
|
||||
);
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
const cancelled = await providerOver(binary).execute(
|
||||
{ operation: "list_adb_devices", input: {} },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(cancelled.ok).toBe(false);
|
||||
if (cancelled.ok) return;
|
||||
expect(projectAnalysisError(cancelled.error)).toMatchObject({
|
||||
code: "cancelled",
|
||||
});
|
||||
expect(
|
||||
await readFile(join(root, "spawned")).then(
|
||||
() => true,
|
||||
() => false,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("reads a bounded logcat dump with the caller count and buffer", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
shellStub({
|
||||
logcat:
|
||||
"08-10 13:00:00.000 I/Tag( 123): first\\n08-10 13:00:01.000 I/Tag( 123): second\\n",
|
||||
}),
|
||||
);
|
||||
const result = await evidenceResult(providerOver(binary), {
|
||||
operation: "read_adb_logcat",
|
||||
input: { serial: "emulator-5554", count: 500, buffer: "main" },
|
||||
});
|
||||
expect(result.lines).toEqual([
|
||||
"08-10 13:00:00.000 I/Tag( 123): first",
|
||||
"08-10 13:00:01.000 I/Tag( 123): second",
|
||||
]);
|
||||
});
|
||||
|
||||
it("projects package details from dumpsys package output", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
shellStub({
|
||||
"dumpsys:package": `Packages:
|
||||
Package [com.example] (abcdef):
|
||||
userId=10234
|
||||
versionName=1.2.3
|
||||
versionCode=45 minSdk=24 targetSdk=34
|
||||
firstInstallTime=2026-01-02 03:04:05
|
||||
lastUpdateTime=2026-02-03 04:05:06
|
||||
installerPackageName=com.android.vending
|
||||
pkgFlags=[ HAS_CODE ALLOW_CLEAR_USER_DATA ]
|
||||
requested permissions:
|
||||
android.permission.INTERNET
|
||||
android.permission.CAMERA
|
||||
`,
|
||||
}),
|
||||
);
|
||||
const result = await evidenceResult(providerOver(binary), {
|
||||
operation: "inspect_adb_package",
|
||||
input: { serial: "emulator-5554", package: "com.example" },
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
version_name: "1.2.3",
|
||||
version_code: 45,
|
||||
first_install_time: "2026-01-02 03:04:05",
|
||||
last_update_time: "2026-02-03 04:05:06",
|
||||
installer_package_name: "com.android.vending",
|
||||
user_id: 10234,
|
||||
requested_permissions_count: 2,
|
||||
coverage: "complete",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports an uninstalled package from dumpsys refusal output", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
shellStub({ "dumpsys:package": "Unable to find package: com.absent\n" }),
|
||||
);
|
||||
const projected = await refusedBy(providerOver(binary), {
|
||||
operation: "inspect_adb_package",
|
||||
input: { serial: "emulator-5554", package: "com.absent" },
|
||||
});
|
||||
expect(projected).toMatchObject({ code: "unsupported_target" });
|
||||
});
|
||||
|
||||
it("pulls one device file with a digest", async () => {
|
||||
const { binary, root } = await writeAdbStub(
|
||||
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$3" = "pull" ]; then printf 'file-bytes' > "$5"; exit 0; fi
|
||||
exit 1`,
|
||||
);
|
||||
const output = join(root, "pulls");
|
||||
const result = await evidenceResult(providerOver(binary), {
|
||||
operation: "pull_adb_file",
|
||||
input: {
|
||||
serial: "emulator-5554",
|
||||
device_path: "/data/local/tmp/notes.txt",
|
||||
output_directory: output,
|
||||
},
|
||||
});
|
||||
const digest = createHash("sha256").update("file-bytes").digest("hex");
|
||||
expect(result).toMatchObject({
|
||||
device_path: "/data/local/tmp/notes.txt",
|
||||
local_path: join(output, "notes.txt"),
|
||||
bytes: 10,
|
||||
sha256: digest,
|
||||
});
|
||||
});
|
||||
|
||||
it("pushes a file and verifies the device-side digest", async () => {
|
||||
const { binary, root } = await writeAdbStub(
|
||||
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$4" = "test" ]; then exit 1; fi
|
||||
if [ "$1" = "-s" ] && [ "$3" = "push" ]; then exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$4" = "sha256sum" ]; then printf '%s %s\\n' "8748b5d17d1ebcec7d46674fa6a8b92c0f95598a552f27c1c8a132eb58c04043" "$5"; exit 0; fi
|
||||
exit 1`,
|
||||
);
|
||||
const localFile = join(root, "payload.bin");
|
||||
await writeFile(localFile, "push-payload");
|
||||
const digest = createHash("sha256").update("push-payload").digest("hex");
|
||||
const pushed = await evidenceResult(providerOver(binary), {
|
||||
operation: "push_adb_file",
|
||||
input: {
|
||||
serial: "emulator-5554",
|
||||
local_path: localFile,
|
||||
device_path: "/data/local/tmp/payload.bin",
|
||||
overwrite: false,
|
||||
},
|
||||
});
|
||||
expect(pushed).toMatchObject({
|
||||
bytes: 12,
|
||||
sha256: digest,
|
||||
device_sha256: digest,
|
||||
overwritten: false,
|
||||
device_digest_source: "device_sha256sum",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports a digest mismatch after push as a changed artifact", async () => {
|
||||
const { binary, root } = await writeAdbStub(
|
||||
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$4" = "test" ]; then exit 1; fi
|
||||
if [ "$1" = "-s" ] && [ "$3" = "push" ]; then exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$4" = "sha256sum" ]; then printf '%s %s\\n' "$(printf 'a%.0s' 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64)" "$5"; exit 0; fi
|
||||
exit 1`,
|
||||
);
|
||||
const localFile = join(root, "payload.bin");
|
||||
await writeFile(localFile, "push-payload");
|
||||
const projected = await refusedBy(providerOver(binary), {
|
||||
operation: "push_adb_file",
|
||||
input: {
|
||||
serial: "emulator-5554",
|
||||
local_path: localFile,
|
||||
device_path: "/data/local/tmp/payload.bin",
|
||||
overwrite: false,
|
||||
},
|
||||
});
|
||||
expect(projected).toMatchObject({ code: "artifact_changed" });
|
||||
});
|
||||
|
||||
it("refuses a push onto an existing device path without overwrite", async () => {
|
||||
const { binary, root } = await writeAdbStub(
|
||||
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$4" = "test" ]; then exit 0; fi
|
||||
exit 1`,
|
||||
);
|
||||
const localFile = join(root, "payload.bin");
|
||||
await writeFile(localFile, "x");
|
||||
const result = await providerOver(binary).execute({
|
||||
operation: "push_adb_file",
|
||||
input: {
|
||||
serial: "emulator-5554",
|
||||
local_path: localFile,
|
||||
device_path: "/data/local/tmp/exists.bin",
|
||||
overwrite: false,
|
||||
},
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "unreadable_output",
|
||||
});
|
||||
expect(result.error.message).toContain("overwrite");
|
||||
});
|
||||
|
||||
it("reports a local push source that does not exist", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
exit 1`,
|
||||
);
|
||||
const projected = await refusedBy(providerOver(binary), {
|
||||
operation: "push_adb_file",
|
||||
input: {
|
||||
serial: "emulator-5554",
|
||||
local_path: "/nonexistent/payload.bin",
|
||||
device_path: "/data/local/tmp/payload.bin",
|
||||
overwrite: false,
|
||||
},
|
||||
});
|
||||
expect(projected).toMatchObject({ code: "invalid_request" });
|
||||
});
|
||||
|
||||
it("captures the screen as a digested PNG with its dimensions", async () => {
|
||||
const { binary, root } = await writeAdbStub(
|
||||
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$3" = "exec-out" ]; then
|
||||
printf '\\211PNG\\r\\n\\032\\n\\0\\0\\0\\rIHDR\\0\\0\\0\\004\\0\\0\\0\\003'
|
||||
exit 0
|
||||
fi
|
||||
exit 1`,
|
||||
);
|
||||
const output = join(root, "captures");
|
||||
const result = await evidenceResult(providerOver(binary), {
|
||||
operation: "capture_adb_screen",
|
||||
input: { serial: "emulator-5554", output_directory: output },
|
||||
});
|
||||
expect(result).toMatchObject({ width: 4, height: 3 });
|
||||
expect(
|
||||
await readFile(join(output, "screen.png")).then(
|
||||
(bytes) => bytes.subarray(1, 4).toString("latin1") === "PNG",
|
||||
() => false,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("lists processes, features, and services through fixed commands", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
shellStub({
|
||||
ps: "USER PID PPID VSZ RSS WCHAN ADDR S NAME\nu0_a1 1234 567 123456 7890 0 0 S com.example.app\n",
|
||||
"pm:list":
|
||||
"feature:android.hardware.camera\nfeature:android.hardware.nfc=1\n",
|
||||
"service:list":
|
||||
"Found 2 services:\n 1 package: [android.content.pm.IPackageManager]\n 2 activity: [android.app.IActivityManager]\n",
|
||||
}),
|
||||
);
|
||||
const processes = await evidenceResult(providerOver(binary), {
|
||||
operation: "list_adb_processes",
|
||||
input: { serial: "emulator-5554" },
|
||||
});
|
||||
expect(processes.processes).toEqual([
|
||||
{
|
||||
user: "u0_a1",
|
||||
pid: 1234,
|
||||
ppid: 567,
|
||||
rss: 7890,
|
||||
state: "S",
|
||||
name: "com.example.app",
|
||||
},
|
||||
]);
|
||||
const features = await evidenceResult(providerOver(binary), {
|
||||
operation: "list_adb_features",
|
||||
input: { serial: "emulator-5554" },
|
||||
});
|
||||
expect(features.features).toEqual([
|
||||
{ name: "android.hardware.camera", version: null },
|
||||
{ name: "android.hardware.nfc", version: 1 },
|
||||
]);
|
||||
const services = await evidenceResult(providerOver(binary), {
|
||||
operation: "list_adb_services",
|
||||
input: { serial: "emulator-5554" },
|
||||
});
|
||||
expect(services.services).toEqual([
|
||||
{ name: "package", interface: "android.content.pm.IPackageManager" },
|
||||
{ name: "activity", interface: "android.app.IActivityManager" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("lists a device directory with kinds and symlink targets", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
shellStub({
|
||||
ls: "total 4\n-rw-rw---- u0_a1 u0_a1 4096 2026-01-02 11:00 app.db\nlrwxrwxrwx root root 2026-01-01 10:00 lib -> /apex/com.android.art/lib\n",
|
||||
}),
|
||||
);
|
||||
const result = await evidenceResult(providerOver(binary), {
|
||||
operation: "list_adb_directory",
|
||||
input: {
|
||||
serial: "emulator-5554",
|
||||
device_path: "/data/data/com.example.app",
|
||||
},
|
||||
});
|
||||
expect(result.entries).toEqual([
|
||||
{
|
||||
name: "app.db",
|
||||
kind: "file",
|
||||
permissions: "-rw-rw----",
|
||||
owner: "u0_a1",
|
||||
group: "u0_a1",
|
||||
bytes: 4096,
|
||||
date: "2026-01-02 11:00",
|
||||
link_target: null,
|
||||
},
|
||||
{
|
||||
name: "lib",
|
||||
kind: "symlink",
|
||||
permissions: "lrwxrwxrwx",
|
||||
owner: "root",
|
||||
group: "root",
|
||||
bytes: null,
|
||||
date: "2026-01-01 10:00",
|
||||
link_target: "/apex/com.android.art/lib",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("inspects the display and window focus", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
shellStub({
|
||||
"wm:size": "Physical size: 1080x2340\n",
|
||||
"wm:density": "Physical density: 420\n",
|
||||
"dumpsys:window":
|
||||
" mCurrentFocus=Window{abcdef u0 com.example.app/com.example.app.MainActivity}\n",
|
||||
}),
|
||||
);
|
||||
const display = await evidenceResult(providerOver(binary), {
|
||||
operation: "inspect_adb_display",
|
||||
input: { serial: "emulator-5554" },
|
||||
});
|
||||
expect(display.physical_size).toEqual({ width: 1080, height: 2340 });
|
||||
expect(display.physical_density).toBe(420);
|
||||
const window = await evidenceResult(providerOver(binary), {
|
||||
operation: "inspect_adb_window",
|
||||
input: { serial: "emulator-5554" },
|
||||
});
|
||||
expect(window.focused_window).toBe(
|
||||
"Window{abcdef u0 com.example.app/com.example.app.MainActivity}",
|
||||
);
|
||||
});
|
||||
|
||||
it("reads one settings value and reports device-side nulls", async () => {
|
||||
const { binary } = await writeAdbStub(shellStub({ "settings:get": "1\n" }));
|
||||
const enabled = await evidenceResult(providerOver(binary), {
|
||||
operation: "read_adb_setting",
|
||||
input: {
|
||||
serial: "emulator-5554",
|
||||
namespace: "secure",
|
||||
key: "adb_enabled",
|
||||
},
|
||||
});
|
||||
expect(enabled.value).toBe("1");
|
||||
const { binary: nullBinary } = await writeAdbStub(
|
||||
shellStub({ "settings:get": "null\n" }),
|
||||
);
|
||||
const absent = await evidenceResult(providerOver(nullBinary), {
|
||||
operation: "read_adb_setting",
|
||||
input: {
|
||||
serial: "emulator-5554",
|
||||
namespace: "global",
|
||||
key: "absent_key",
|
||||
},
|
||||
});
|
||||
expect(absent.value).toBeNull();
|
||||
expect(absent.device_reported_null).toBe(true);
|
||||
});
|
||||
|
||||
it("resolves packages by case-insensitive substring", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
shellStub({
|
||||
"pm:list":
|
||||
"package:/data/app/x/base.apk=com.whatsapp\\npackage:/data/app/y/base.apk=com.whatsapp.w4b\\npackage:/data/app/z/base.apk=com.example\\n",
|
||||
}),
|
||||
);
|
||||
const result = await evidenceResult(providerOver(binary), {
|
||||
operation: "resolve_adb_packages",
|
||||
input: { serial: "emulator-5554", query: "WhatsApp" },
|
||||
});
|
||||
expect(result.matches).toEqual([
|
||||
{
|
||||
package_name: "com.whatsapp",
|
||||
base_apk_device_path: "/data/app/x/base.apk",
|
||||
},
|
||||
{
|
||||
package_name: "com.whatsapp.w4b",
|
||||
base_apk_device_path: "/data/app/y/base.apk",
|
||||
},
|
||||
]);
|
||||
expect(result.exact_match).toBe(false);
|
||||
expect(result.coverage).toBe("complete");
|
||||
});
|
||||
|
||||
it("installs a local APK and preserves the device's failure verdict", async () => {
|
||||
const { binary, root } = await writeAdbStub(
|
||||
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$3" = "install" ]; then printf 'Success\\n'; exit 0; fi
|
||||
exit 1`,
|
||||
);
|
||||
const apk = join(root, "app.apk");
|
||||
await writeFile(apk, "apk");
|
||||
const digest = createHash("sha256").update("apk").digest("hex");
|
||||
const installed = await evidenceResult(providerOver(binary), {
|
||||
operation: "install_adb_package",
|
||||
input: { serial: "emulator-5554", apk_path: apk, replace: false },
|
||||
});
|
||||
expect(installed).toMatchObject({
|
||||
bytes: 3,
|
||||
sha256: digest,
|
||||
replaced: false,
|
||||
});
|
||||
|
||||
const { binary: failing } = await writeAdbStub(
|
||||
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$3" = "install" ]; then printf 'Failure [INSTALL_FAILED_ALREADY_EXISTS]\\n'; exit 0; fi
|
||||
exit 1`,
|
||||
);
|
||||
const projected = await refusedBy(providerOver(failing), {
|
||||
operation: "install_adb_package",
|
||||
input: {
|
||||
serial: "emulator-5554",
|
||||
apk_path: apk,
|
||||
replace: false,
|
||||
},
|
||||
});
|
||||
expect(projected).toMatchObject({ code: "unsupported_target" });
|
||||
expect(projected.message).toContain("INSTALL_FAILED_ALREADY_EXISTS");
|
||||
});
|
||||
|
||||
it("uninstalls a package and reports the device's refusal for absent ones", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$3" = "uninstall" ]; then printf 'Success\\n'; exit 0; fi
|
||||
exit 1`,
|
||||
);
|
||||
const removed = await evidenceResult(providerOver(binary), {
|
||||
operation: "uninstall_adb_package",
|
||||
input: { serial: "emulator-5554", package: "com.example" },
|
||||
});
|
||||
expect(removed.package_name).toBe("com.example");
|
||||
|
||||
const { binary: refusing } = await writeAdbStub(
|
||||
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$3" = "uninstall" ]; then printf 'Failure [not installed for 0]\\n'; exit 0; fi
|
||||
exit 1`,
|
||||
);
|
||||
const projected = await refusedBy(providerOver(refusing), {
|
||||
operation: "uninstall_adb_package",
|
||||
input: { serial: "emulator-5554", package: "com.absent" },
|
||||
});
|
||||
expect(projected).toMatchObject({ code: "unsupported_target" });
|
||||
expect(projected.message).toContain("not installed for 0");
|
||||
});
|
||||
|
||||
it("starts an app through resolved launcher component and force-stops it", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
shellStub({
|
||||
"cmd:package":
|
||||
"priority=0 preferredOrder=0 match=0x00108000 specificIndex=-1 isDefault=true\ncom.example/.Main",
|
||||
"am:start":
|
||||
"Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example/.Main }\n",
|
||||
"am:force-stop": "",
|
||||
}),
|
||||
);
|
||||
const provider = providerOver(binary);
|
||||
const started = await evidenceResult(provider, {
|
||||
operation: "start_adb_app",
|
||||
input: { serial: "emulator-5554", package: "com.example" },
|
||||
});
|
||||
expect(started).toMatchObject({
|
||||
package_name: "com.example",
|
||||
component: "com.example/.Main",
|
||||
started_activity: expect.stringContaining(
|
||||
"cmp=com.example/.Main",
|
||||
) as unknown,
|
||||
});
|
||||
const stopped = await evidenceResult(provider, {
|
||||
operation: "stop_adb_app",
|
||||
input: { serial: "emulator-5554", package: "com.example" },
|
||||
});
|
||||
expect(stopped.package_name).toBe("com.example");
|
||||
});
|
||||
|
||||
it("reports an app without a launcher activity as unsupported", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
shellStub({ "cmd:package": "No activity found\n" }),
|
||||
);
|
||||
const projected = await refusedBy(providerOver(binary), {
|
||||
operation: "start_adb_app",
|
||||
input: { serial: "emulator-5554", package: "com.library.only" },
|
||||
});
|
||||
expect(projected).toMatchObject({ code: "unsupported_target" });
|
||||
expect(projected.message).toContain("No launcher activity");
|
||||
});
|
||||
|
||||
it("starts an intent with typed extras as a fixed argv vector", async () => {
|
||||
const { binary, root } = await writeAdbStub(
|
||||
(root) => `printf '%s\\n' "$@" >> ${join(root, "argv.log")}
|
||||
${shellStub({ "am:start": "Starting: Intent { act=android.intent.action.VIEW dat=https://example.com/ }\n" })(root)}`,
|
||||
);
|
||||
const started = await evidenceResult(providerOver(binary), {
|
||||
operation: "start_adb_activity",
|
||||
input: {
|
||||
serial: "emulator-5554",
|
||||
action: "android.intent.action.VIEW",
|
||||
data_uri: "https://example.com/",
|
||||
extras: [
|
||||
{ key: "level", type: "int", value: "3" },
|
||||
{ key: "label", type: "string", value: "probe" },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(started.action).toBe("android.intent.action.VIEW");
|
||||
const logged = (await readFile(join(root, "argv.log"), "utf8"))
|
||||
.trim()
|
||||
.split("\n");
|
||||
expect(logged).toEqual([
|
||||
"version",
|
||||
"-s",
|
||||
"emulator-5554",
|
||||
"shell",
|
||||
"am",
|
||||
"start",
|
||||
"-W",
|
||||
"-a",
|
||||
"android.intent.action.VIEW",
|
||||
"-d",
|
||||
"https://example.com/",
|
||||
"--ei",
|
||||
"level",
|
||||
"3",
|
||||
"--es",
|
||||
"label",
|
||||
"probe",
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects mistyped intent extras before touching the device", async () => {
|
||||
const { binary } = await writeAdbStub(
|
||||
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
: > should-not-exist`,
|
||||
);
|
||||
const projected = await refusedBy(providerOver(binary), {
|
||||
operation: "start_adb_activity",
|
||||
input: {
|
||||
serial: "emulator-5554",
|
||||
action: "com.example.ACTION",
|
||||
extras: [{ key: "flag", type: "boolean", value: "yes" }],
|
||||
},
|
||||
});
|
||||
expect(projected).toMatchObject({ code: "invalid_request" });
|
||||
});
|
||||
|
||||
it("collects a device bugreport archive with its digest", async () => {
|
||||
const { binary, root } = await writeAdbStub(
|
||||
(root) => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$3" = "bugreport" ]; then
|
||||
printf 'bugreport-bytes' > "${join(root, "bugreport test.zip")}"
|
||||
printf 'Bug report copied to ${join(root, "bugreport test.zip")}\\n'
|
||||
exit 0
|
||||
fi
|
||||
exit 1`,
|
||||
);
|
||||
const result = await evidenceResult(providerOver(binary), {
|
||||
operation: "collect_adb_bugreport",
|
||||
input: { serial: "emulator-5554", output_directory: root },
|
||||
});
|
||||
const digest = createHash("sha256").update("bugreport-bytes").digest("hex");
|
||||
expect(result).toMatchObject({
|
||||
local_path: join(root, "bugreport test.zip"),
|
||||
bytes: 15,
|
||||
sha256: digest,
|
||||
adb_reported_path: join(root, "bugreport test.zip"),
|
||||
});
|
||||
});
|
||||
|
||||
it("quotes caller text before adb joins the remote shell command", async () => {
|
||||
const { binary, root } = await writeAdbStub(
|
||||
(root) => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
shift 3
|
||||
# Model adb's remote command joining, then execute the resulting shell string.
|
||||
printf '#!/bin/sh\nprintf "%%s" "$@" > "${join(root, "observed")}"\n' > "${join(root, "ls")}"
|
||||
chmod +x "${join(root, "ls")}"
|
||||
PATH="${root}:$PATH" sh -c "$*"
|
||||
`,
|
||||
);
|
||||
const value = `/sdcard/a 'quoted'; touch ${join(root, "injected")}`;
|
||||
await providerOver(binary).execute({
|
||||
operation: "list_adb_directory",
|
||||
input: { serial: "device", device_path: value },
|
||||
});
|
||||
expect(await readFile(join(root, "observed"), "utf8")).toContain(value);
|
||||
await expect(readFile(join(root, "injected"))).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("preserves a screen output created while capture is running", async () => {
|
||||
const { binary, root } = await writeAdbStub(
|
||||
(root) => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
printf 'existing evidence' > "${join(root, "captures", "screen.png")}"
|
||||
printf '\\211PNG\\r\\n\\032\\n\\0\\0\\0\\rIHDR\\0\\0\\0\\004\\0\\0\\0\\003'
|
||||
`,
|
||||
);
|
||||
const result = await providerOver(binary).execute({
|
||||
operation: "capture_adb_screen",
|
||||
input: { serial: "device", output_directory: join(root, "captures") },
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
expect(await readFile(join(root, "captures", "screen.png"), "utf8")).toBe(
|
||||
"existing evidence",
|
||||
);
|
||||
});
|
||||
|
||||
it("reports cancellation arriving during the version probe", async () => {
|
||||
const { binary } = await writeAdbStub(() => "exec sleep 30");
|
||||
const controller = new AbortController();
|
||||
const pending = providerOver(binary).execute(
|
||||
{ operation: "inspect_adb_client", input: {} },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
const timer = setTimeout(() => controller.abort(), 50);
|
||||
try {
|
||||
const result = await pending;
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(projectAnalysisError(result.error).code).toBe("cancelled");
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* Parsers for fixed observation commands REA runs through `adb shell`.
|
||||
*
|
||||
* Formats follow toybox output on Android 8+ (verified against an Android 14
|
||||
* emulator). Unparseable lines are retained rather than dropped so callers
|
||||
* can see the format drift instead of silently losing rows.
|
||||
*/
|
||||
|
||||
export interface AdbProcessEntry {
|
||||
readonly user: string;
|
||||
readonly pid: number;
|
||||
readonly ppid: number | null;
|
||||
readonly rss: number | null;
|
||||
readonly state: string | null;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
const PROCESS_LINE =
|
||||
/^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)$/u;
|
||||
|
||||
/** Parse `ps -A` output; the header line is preserved verbatim as `columns`. */
|
||||
export const parseProcessListOutput = (
|
||||
stdout: string,
|
||||
): {
|
||||
columns: string | null;
|
||||
processes: readonly AdbProcessEntry[];
|
||||
unparsedLines: readonly string[];
|
||||
} => {
|
||||
const processes: AdbProcessEntry[] = [];
|
||||
const unparsedLines: string[] = [];
|
||||
let columns: string | null = null;
|
||||
for (const rawLine of stdout.split(/\r?\n/u)) {
|
||||
const line = rawLine.trimEnd();
|
||||
if (line === "") continue;
|
||||
if (/^USER\s+PID\s+PPID\b/u.test(line)) {
|
||||
columns = line.trim();
|
||||
continue;
|
||||
}
|
||||
const match = PROCESS_LINE.exec(line);
|
||||
if (match === null) {
|
||||
unparsedLines.push(line);
|
||||
continue;
|
||||
}
|
||||
processes.push({
|
||||
user: match[1]!,
|
||||
pid: Number(match[2]!),
|
||||
ppid: Number.isSafeInteger(Number(match[3]!)) ? Number(match[3]!) : null,
|
||||
rss: Number.isSafeInteger(Number(match[5]!)) ? Number(match[5]!) : null,
|
||||
state: match[8]!,
|
||||
name: match[9]!.trim(),
|
||||
});
|
||||
}
|
||||
return { columns, processes, unparsedLines };
|
||||
};
|
||||
|
||||
export interface AdbDirectoryEntry {
|
||||
readonly name: string;
|
||||
readonly kind: "directory" | "file" | "symlink" | "other";
|
||||
readonly permissions: string | null;
|
||||
readonly owner: string | null;
|
||||
readonly group: string | null;
|
||||
readonly bytes: number | null;
|
||||
readonly date: string | null;
|
||||
readonly link_target: string | null;
|
||||
}
|
||||
|
||||
const LS_LINE =
|
||||
/^([bcdlps-][rwxstST-]{9})\s+(?:(\d+)\s+)?(\S+)\s+(\S+)\s+(?:(\d+)\s+)?([\d-]{10})\s+([\d:]{4,8})\s+(.+)$/u;
|
||||
|
||||
/** Parse `ls -la` output, including `total` headers and symlink targets. */
|
||||
export const parseLsOutput = (
|
||||
stdout: string,
|
||||
): {
|
||||
entries: readonly AdbDirectoryEntry[];
|
||||
unparsedLines: readonly string[];
|
||||
} => {
|
||||
const entries: AdbDirectoryEntry[] = [];
|
||||
const unparsedLines: string[] = [];
|
||||
for (const rawLine of stdout.split(/\r?\n/u)) {
|
||||
const line = rawLine.trimEnd();
|
||||
if (line === "" || line.startsWith("total ")) continue;
|
||||
const match = LS_LINE.exec(line);
|
||||
if (match === null) {
|
||||
if (line.trim() !== "") unparsedLines.push(line);
|
||||
continue;
|
||||
}
|
||||
const permissions = match[1]!;
|
||||
let name = match[8]!.trim();
|
||||
let linkTarget: string | null = null;
|
||||
const linkArrow = name.indexOf(" -> ");
|
||||
if (linkArrow !== -1 && permissions.startsWith("l")) {
|
||||
linkTarget = name.slice(linkArrow + 4);
|
||||
name = name.slice(0, linkArrow);
|
||||
}
|
||||
entries.push({
|
||||
name,
|
||||
kind: permissions.startsWith("d")
|
||||
? "directory"
|
||||
: permissions.startsWith("l")
|
||||
? "symlink"
|
||||
: permissions.startsWith("-")
|
||||
? "file"
|
||||
: "other",
|
||||
permissions,
|
||||
owner: match[3] ?? null,
|
||||
group: match[4] ?? null,
|
||||
bytes: match[5] === undefined ? null : Number(match[5]),
|
||||
date: `${match[6] ?? ""} ${match[7] ?? ""}`.trim() || null,
|
||||
link_target: linkTarget,
|
||||
});
|
||||
}
|
||||
return { entries, unparsedLines };
|
||||
};
|
||||
|
||||
export interface AdbFeatureEntry {
|
||||
readonly name: string;
|
||||
readonly version: number | null;
|
||||
}
|
||||
|
||||
const FEATURE_LINE = /^feature:([^=\s]+)(?:=(0x[0-9a-fA-F]+|\d+))?$/u;
|
||||
|
||||
const parseFeatureVersion = (text: string): number =>
|
||||
text.startsWith("0x") ? Number.parseInt(text.slice(2), 16) : Number(text);
|
||||
|
||||
/** Parse `pm list features` output, including hex GL versions. */
|
||||
export const parseFeatureListOutput = (
|
||||
stdout: string,
|
||||
): {
|
||||
features: readonly AdbFeatureEntry[];
|
||||
unparsedLines: readonly string[];
|
||||
} => {
|
||||
const features: AdbFeatureEntry[] = [];
|
||||
const unparsedLines: string[] = [];
|
||||
for (const rawLine of stdout.split(/\r?\n/u)) {
|
||||
const line = rawLine.trim();
|
||||
if (line === "") continue;
|
||||
const match = FEATURE_LINE.exec(line);
|
||||
if (match === null) {
|
||||
unparsedLines.push(line);
|
||||
continue;
|
||||
}
|
||||
features.push({
|
||||
name: match[1]!,
|
||||
version: match[2] === undefined ? null : parseFeatureVersion(match[2]!),
|
||||
});
|
||||
}
|
||||
return { features, unparsedLines };
|
||||
};
|
||||
|
||||
export interface AdbServiceEntry {
|
||||
readonly name: string;
|
||||
readonly interface: string | null;
|
||||
}
|
||||
|
||||
const SERVICE_LINE = /^\s*(?:\d+\s+)?([A-Za-z0-9._$/-]+):\s*\[([^\]]*)\]\s*$/u;
|
||||
|
||||
/** Parse `service list` output, tolerating numbered and plain formats. */
|
||||
export const parseServiceListOutput = (
|
||||
stdout: string,
|
||||
): {
|
||||
services: readonly AdbServiceEntry[];
|
||||
unparsedLines: readonly string[];
|
||||
} => {
|
||||
const services: AdbServiceEntry[] = [];
|
||||
const unparsedLines: string[] = [];
|
||||
for (const rawLine of stdout.split(/\r?\n/u)) {
|
||||
const line = rawLine.trimEnd();
|
||||
if (line.trim() === "" || /^Found \d+ services:/u.test(line.trim()))
|
||||
continue;
|
||||
const match = SERVICE_LINE.exec(line);
|
||||
if (match === null) {
|
||||
if (line.trim() !== "") unparsedLines.push(line.trim());
|
||||
continue;
|
||||
}
|
||||
services.push({
|
||||
name: match[1]!,
|
||||
interface: match[2] === "" ? null : match[2]!,
|
||||
});
|
||||
}
|
||||
return { services, unparsedLines };
|
||||
};
|
||||
|
||||
const SIZE_PAIR = /^Physical size:\s+(\d+)x(\d+)$/u;
|
||||
const SIZE_OVERRIDE = /^Override size:\s+(\d+)x(\d+)$/u;
|
||||
const DENSITY_PHYSICAL = /^Physical density:\s+(\d+)$/u;
|
||||
const DENSITY_OVERRIDE = /^Override density:\s+(\d+)$/u;
|
||||
|
||||
/** Parse `wm size` output into physical and optional override sizes. */
|
||||
export const parseWmSizeOutput = (
|
||||
stdout: string,
|
||||
): {
|
||||
physical: { readonly width: number; readonly height: number } | null;
|
||||
override: { readonly width: number; readonly height: number } | null;
|
||||
} => {
|
||||
let physical = null;
|
||||
let override = null;
|
||||
for (const line of stdout.split(/\r?\n/u)) {
|
||||
const trimmed = line.trim();
|
||||
const physicalMatch = SIZE_PAIR.exec(trimmed);
|
||||
if (physicalMatch !== null)
|
||||
physical = {
|
||||
width: Number(physicalMatch[1]!),
|
||||
height: Number(physicalMatch[2]!),
|
||||
};
|
||||
const overrideMatch = SIZE_OVERRIDE.exec(trimmed);
|
||||
if (overrideMatch !== null)
|
||||
override = {
|
||||
width: Number(overrideMatch[1]!),
|
||||
height: Number(overrideMatch[2]!),
|
||||
};
|
||||
}
|
||||
return { physical, override };
|
||||
};
|
||||
|
||||
/** Parse `wm density` output. Older builds print a bare number. */
|
||||
export const parseWmDensityOutput = (
|
||||
stdout: string,
|
||||
): {
|
||||
physical: number | null;
|
||||
override: number | null;
|
||||
} => {
|
||||
let physical = null;
|
||||
let override = null;
|
||||
for (const line of stdout.split(/\r?\n/u)) {
|
||||
const trimmed = line.trim();
|
||||
const physicalMatch = DENSITY_PHYSICAL.exec(trimmed);
|
||||
if (physicalMatch !== null) physical = Number(physicalMatch[1]!);
|
||||
const overrideMatch = DENSITY_OVERRIDE.exec(trimmed);
|
||||
if (overrideMatch !== null) override = Number(overrideMatch[1]!);
|
||||
}
|
||||
return { physical, override };
|
||||
};
|
||||
|
||||
/** Parse the focus declarations from `dumpsys window windows`. */
|
||||
export const parseWindowFocusOutput = (
|
||||
stdout: string,
|
||||
): {
|
||||
focusedWindow: string | null;
|
||||
focusedApp: string | null;
|
||||
} => {
|
||||
let focusedWindow = null;
|
||||
let focusedApp = null;
|
||||
for (const line of stdout.split(/\r?\n/u)) {
|
||||
const trimmed = line.trim();
|
||||
if (focusedWindow === null && trimmed.startsWith("mCurrentFocus="))
|
||||
focusedWindow = trimmed.slice("mCurrentFocus=".length).trim();
|
||||
if (focusedApp === null && trimmed.startsWith("mFocusedApp="))
|
||||
focusedApp = trimmed.slice("mFocusedApp=".length).trim();
|
||||
}
|
||||
return { focusedWindow, focusedApp };
|
||||
};
|
||||
|
||||
export interface AdbPackageDetails {
|
||||
readonly versionName: string | null;
|
||||
readonly versionCode: number | null;
|
||||
readonly firstInstallTime: string | null;
|
||||
readonly lastUpdateTime: string | null;
|
||||
readonly installerPackageName: string | null;
|
||||
readonly userId: number | null;
|
||||
readonly pkgFlags: string | null;
|
||||
readonly requestedPermissions: number | null;
|
||||
}
|
||||
|
||||
const DETAIL_LINE = /^\s+([A-Za-z][A-Za-z0-9_]*)=(.*)$/u;
|
||||
|
||||
/**
|
||||
* Project `dumpsys package PACKAGE` output. Only the listed fields are
|
||||
* extracted; the input is version-dependent and everything else is ignored
|
||||
* by design rather than guessed.
|
||||
*/
|
||||
export const parseDumpsysPackageOutput = (
|
||||
stdout: string,
|
||||
): AdbPackageDetails => {
|
||||
const details: {
|
||||
versionName: string | null;
|
||||
versionCode: number | null;
|
||||
firstInstallTime: string | null;
|
||||
lastUpdateTime: string | null;
|
||||
installerPackageName: string | null;
|
||||
userId: number | null;
|
||||
pkgFlags: string | null;
|
||||
} = {
|
||||
versionName: null,
|
||||
versionCode: null,
|
||||
firstInstallTime: null,
|
||||
lastUpdateTime: null,
|
||||
installerPackageName: null,
|
||||
userId: null,
|
||||
pkgFlags: null,
|
||||
};
|
||||
let inPermissions = false;
|
||||
let permissions = 0;
|
||||
for (const line of stdout.split(/\r?\n/u)) {
|
||||
if (line.trim().startsWith("requested permissions:")) {
|
||||
inPermissions = true;
|
||||
continue;
|
||||
}
|
||||
if (inPermissions && /^\s{2,}[A-Za-z0-9._]+$/.test(line)) {
|
||||
permissions += 1;
|
||||
continue;
|
||||
}
|
||||
inPermissions = false;
|
||||
const match = DETAIL_LINE.exec(line);
|
||||
if (match === null) continue;
|
||||
const key = match[1]!;
|
||||
const value = match[2]!.trim();
|
||||
if (key === "versionName" && details.versionName === null)
|
||||
details.versionName = value === "" ? null : value;
|
||||
else if (key === "versionCode" && details.versionCode === null) {
|
||||
const parsed = /^(\d+)/u.exec(value);
|
||||
if (parsed !== null) details.versionCode = Number(parsed[1]!);
|
||||
} else if (key === "firstInstallTime" && details.firstInstallTime === null)
|
||||
details.firstInstallTime = value === "" ? null : value;
|
||||
else if (key === "lastUpdateTime" && details.lastUpdateTime === null)
|
||||
details.lastUpdateTime = value === "" ? null : value;
|
||||
else if (
|
||||
key === "installerPackageName" &&
|
||||
details.installerPackageName === null
|
||||
)
|
||||
details.installerPackageName = value === "" ? null : value;
|
||||
else if (key === "userId" && details.userId === null) {
|
||||
const parsed = /^(\d+)/u.exec(value);
|
||||
if (parsed !== null) details.userId = Number(parsed[1]!);
|
||||
} else if (key === "pkgFlags" && details.pkgFlags === null)
|
||||
details.pkgFlags = value === "" ? null : value;
|
||||
}
|
||||
return { ...details, requestedPermissions: permissions };
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse `settings get` output. Android prints the literal string `null` for
|
||||
* absent keys; that is reported as a device-side null observation.
|
||||
*/
|
||||
export const parseSettingsGetOutput = (
|
||||
stdout: string,
|
||||
): {
|
||||
value: string | null;
|
||||
deviceReportedNull: boolean;
|
||||
} => {
|
||||
const trimmed = stdout.replace(/\r?\n$/u, "");
|
||||
if (trimmed === "null") return { value: null, deviceReportedNull: true };
|
||||
return { value: trimmed, deviceReportedNull: false };
|
||||
};
|
||||
|
||||
/** Parse the host-side completion line from `adb bugreport`. */
|
||||
export const parseBugreportCopiedPath = (stdout: string): string | null => {
|
||||
const match = /^Bug report copied to (.+)\r?$/mu.exec(stdout.trim());
|
||||
return match === null ? null : match[1]!;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse `adb install`/`adb uninstall` output. adb can print `Failure [...]`
|
||||
* with a zero exit code, so the text decides, not the exit status.
|
||||
*/
|
||||
export const parsePackageChangeOutput = (
|
||||
stdout: string,
|
||||
): {
|
||||
readonly status: "success" | "failure";
|
||||
readonly reason: string | null;
|
||||
} => {
|
||||
const text = stdout.trim();
|
||||
if (/^Success$/mu.test(text)) return { status: "success", reason: null };
|
||||
const failure = /Failure \[([^\]]+)\]/u.exec(text);
|
||||
return {
|
||||
status: "failure",
|
||||
reason: failure?.[1] ?? (text === "" ? null : text.slice(0, 512)),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse `cmd package resolve-activity --brief` output: the component is the
|
||||
* last non-empty line.
|
||||
*/
|
||||
export const parseResolveActivityOutput = (stdout: string): string | null => {
|
||||
const lines = stdout
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line !== "" && !/^No activity found$/iu.test(line));
|
||||
return lines.length === 0 ? null : lines[lines.length - 1]!;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse `am start` output. Success prints `Starting: Intent { ... }`;
|
||||
* refusals print lines starting with `Error:` or `Warning:` and activity
|
||||
* classes that do not exist name the component.
|
||||
*/
|
||||
export const parseAmStartOutput = (
|
||||
stdout: string,
|
||||
): {
|
||||
readonly status: "started" | "refused";
|
||||
readonly startedActivity: string | null;
|
||||
readonly reason: string | null;
|
||||
} => {
|
||||
const text = stdout.trim();
|
||||
const starting = /^Starting:\s+Intent\s*\{(.*)\}\s*$/mu.exec(text);
|
||||
if (starting !== null)
|
||||
return {
|
||||
status: "started",
|
||||
startedActivity: starting[1]!.trim(),
|
||||
reason: null,
|
||||
};
|
||||
const error = /^(?:Error|Warning|Exception)(?::|\b)(.*)$/mu.exec(text);
|
||||
return {
|
||||
status: "refused",
|
||||
startedActivity: null,
|
||||
reason:
|
||||
error?.[1]?.trim() ??
|
||||
(text === "" ? "am start returned no activity" : text.slice(0, 512)),
|
||||
};
|
||||
};
|
||||
|
||||
const PNG_SIGNATURE = Buffer.from([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
||||
]);
|
||||
|
||||
/** Read image dimensions from a PNG header, or null for other payloads. */
|
||||
export const parsePngDimensions = (
|
||||
bytes: Uint8Array,
|
||||
): {
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
} => {
|
||||
if (
|
||||
bytes.length < 24 ||
|
||||
!Buffer.from(bytes.subarray(0, 8)).equals(PNG_SIGNATURE)
|
||||
)
|
||||
return { width: null, height: null };
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
return {
|
||||
width: view.getUint32(16),
|
||||
height: view.getUint32(20),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
APKTOOL_COMMAND_ENV,
|
||||
parseApktoolVersionOutput,
|
||||
resolveApktoolCommand,
|
||||
} from "./ApktoolConfiguration.js";
|
||||
|
||||
describe("resolveApktoolCommand", () => {
|
||||
it("prefers an absolute REA_APKTOOL_COMMAND", () => {
|
||||
expect(
|
||||
resolveApktoolCommand({
|
||||
[APKTOOL_COMMAND_ENV]: "/usr/local/bin/apktool",
|
||||
}),
|
||||
).toEqual({
|
||||
command: "/usr/local/bin/apktool",
|
||||
commandSource: "environment",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to PATH resolution without the environment key", () => {
|
||||
expect(resolveApktoolCommand({})).toEqual({
|
||||
command: "apktool",
|
||||
commandSource: "path",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a relative environment path explicitly", () => {
|
||||
expect(() =>
|
||||
resolveApktoolCommand({ [APKTOOL_COMMAND_ENV]: "bin/apktool" }),
|
||||
).toThrow(/must be an absolute path/u);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseApktoolVersionOutput", () => {
|
||||
it("reads the version from real apktool output", () => {
|
||||
expect(parseApktoolVersionOutput("2.7.0-dirty\n")).toEqual({
|
||||
version: "2.7.0-dirty",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports null when the launcher prints nothing usable", () => {
|
||||
expect(parseApktoolVersionOutput(" \n")).toEqual({ version: null });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import { isAbsolute } from "node:path";
|
||||
|
||||
import {
|
||||
execFileOutput,
|
||||
execFileOutputFailure,
|
||||
} from "../process/ExecFileOutput.js";
|
||||
import type { ProviderAvailability } from "../application/AnalysisProvider.js";
|
||||
import type { ProviderRejectionCode } from "../contracts/providerSelection.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
|
||||
/** Environment key selecting the caller's apktool launcher. */
|
||||
export const APKTOOL_COMMAND_ENV = "REA_APKTOOL_COMMAND";
|
||||
|
||||
const VERSION_TIMEOUT_MS = 20_000;
|
||||
const VERSION_MAX_OUTPUT_BYTES = 64 * 1024;
|
||||
|
||||
/** How the apktool launcher was selected for this provider instance. */
|
||||
export interface ApktoolCommandSelection {
|
||||
readonly command: string;
|
||||
readonly commandSource: "environment" | "path";
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the caller-supplied apktool launcher. `REA_APKTOOL_COMMAND` must
|
||||
* be absolute so the selection stays explicit; without it, apktool is
|
||||
* resolved from `PATH` at run time. Both the upstream wrapper script and
|
||||
* `java -jar` launcher scripts work: REA only runs `<command> --version`
|
||||
* and `<command> d ...`.
|
||||
*/
|
||||
export const resolveApktoolCommand = (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): ApktoolCommandSelection => {
|
||||
const configured = environment[APKTOOL_COMMAND_ENV];
|
||||
if (configured === undefined || configured.trim() === "")
|
||||
return { command: "apktool", commandSource: "path" };
|
||||
if (!isAbsolute(configured))
|
||||
throw new ApktoolConfigurationFailure(
|
||||
"not_absolute",
|
||||
`${APKTOOL_COMMAND_ENV} must be an absolute path to the apktool launcher`,
|
||||
{ configured_command: configured },
|
||||
);
|
||||
return { command: configured, commandSource: "environment" };
|
||||
};
|
||||
|
||||
export type ApktoolConfigurationFailureCode =
|
||||
| "not_absolute"
|
||||
| "command_missing"
|
||||
| "version_unresolved";
|
||||
|
||||
export class ApktoolConfigurationFailure extends Error {
|
||||
constructor(
|
||||
readonly code: ApktoolConfigurationFailureCode,
|
||||
message: string,
|
||||
readonly diagnostics: Readonly<Record<string, JsonValue>> = {},
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
super(message, options);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ApktoolClientIdentity {
|
||||
readonly version: string | null;
|
||||
}
|
||||
|
||||
/** Read the version from `apktool --version` output (first non-empty line). */
|
||||
export const parseApktoolVersionOutput = (
|
||||
stdout: string,
|
||||
): ApktoolClientIdentity => {
|
||||
const line = stdout
|
||||
.split(/\r?\n/u)
|
||||
.map((entry) => entry.trim())
|
||||
.find((entry) => entry !== "");
|
||||
return { version: line === undefined ? null : line };
|
||||
};
|
||||
|
||||
/** Probe the selected apktool launcher without decoding anything. */
|
||||
export const inspectApktoolClient = async (
|
||||
selection: ApktoolCommandSelection,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ApktoolClientIdentity> => {
|
||||
try {
|
||||
const { stdout } = await execFileOutput(selection.command, ["--version"], {
|
||||
timeout: VERSION_TIMEOUT_MS,
|
||||
stopSignal: "SIGTERM",
|
||||
maxBuffer: VERSION_MAX_OUTPUT_BYTES,
|
||||
...(signal === undefined ? {} : { signal }),
|
||||
});
|
||||
return parseApktoolVersionOutput(stdout);
|
||||
} catch (cause) {
|
||||
const failure = execFileOutputFailure(cause);
|
||||
if (signal?.aborted || failure?.code === "ABORT_ERR")
|
||||
throw new AnalysisCancelledError("inspect_apktool_client", { cause });
|
||||
if (failure?.code === "ENOENT")
|
||||
throw new ApktoolConfigurationFailure(
|
||||
"command_missing",
|
||||
`The selected apktool launcher was not found: ${selection.command}`,
|
||||
{
|
||||
selected_command: selection.command,
|
||||
command_source: selection.commandSource,
|
||||
remediation: `Set ${APKTOOL_COMMAND_ENV} to an absolute apktool launcher (the upstream wrapper or a java -jar script)`,
|
||||
},
|
||||
{ cause },
|
||||
);
|
||||
throw new ApktoolConfigurationFailure(
|
||||
"version_unresolved",
|
||||
"The selected apktool launcher did not report a usable version",
|
||||
{
|
||||
selected_command: selection.command,
|
||||
...(failure === undefined
|
||||
? {}
|
||||
: {
|
||||
exit_code: failure.code,
|
||||
stderr_excerpt: failure.stderr.slice(0, 512),
|
||||
}),
|
||||
},
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** Availability in the shape shared by every provider. */
|
||||
const rejectionCodeOf = (
|
||||
code: ApktoolConfigurationFailureCode,
|
||||
): ProviderRejectionCode =>
|
||||
code === "command_missing"
|
||||
? "executable_missing"
|
||||
: code === "not_absolute"
|
||||
? "not_configured"
|
||||
: "version_unresolved";
|
||||
|
||||
export const apktoolConfigurationAvailability = (
|
||||
cause: ApktoolConfigurationFailure,
|
||||
): ProviderAvailability =>
|
||||
({
|
||||
status: "unavailable",
|
||||
code: rejectionCodeOf(cause.code),
|
||||
reason: cause.message,
|
||||
diagnostics: { ...cause.diagnostics },
|
||||
}) as ProviderAvailability;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user