mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 17:59:08 +00:00
Merge upstream/main into fix/release-exposure-audit
Resolve the docs/index.md guide table conflict by taking main's table (Linux kernel modules and wxapkg links) and keeping this branch's Release exposure link.
This commit is contained in:
601 files changed
+28500
-3060
No files matched your search
@@ -2,7 +2,7 @@
|
||||
name: reverse-engineer-anything
|
||||
description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA, and build agent environments (AgentEnv bundles) of web applications from a live instance or a capture. 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: "59"
|
||||
version: "60"
|
||||
---
|
||||
|
||||
# REA
|
||||
@@ -121,6 +121,15 @@ explicit path or endpoint and do not need it.
|
||||
cleanup effects, and do not require a preliminary static session unless their
|
||||
own contract says so.
|
||||
- User-owned browser page already open: `list_browser_targets`.
|
||||
- Passive traffic capture of a SPA: the default scope is the page's own origin,
|
||||
and data APIs usually live on another origin. Discover API origins from
|
||||
absolute URLs in loaded bundles or script text via JavaScript analysis tools
|
||||
(another passive capture without those origins still scopes to the page
|
||||
origin, and `completeness.excluded` counts lack URLs). Pass both the page
|
||||
origin and those API origins in `allowed_origins` — the list replaces the
|
||||
default scope — and read `completeness.excluded` before reporting that an
|
||||
API is absent. For long captures with body shapes, use
|
||||
`export_evidence_bundle` instead of expecting an inline result.
|
||||
- Browser action the agent must perform (click, type, navigate):
|
||||
`capture_browser_scenario` with declared actions. See
|
||||
[runtime observation](references/runtime-observation.md) for connect/launch
|
||||
|
||||
@@ -718,6 +718,13 @@ cache one decoded block and support retained filtered/paged views. No
|
||||
decryption or SerializedFile object decoding is provided. See
|
||||
[UnityFS](https://github.com/morluto/rea/blob/main/docs/unityfs.md).
|
||||
|
||||
WeChat mini-program packages (`.wxapkg` files with `0xBE`/`0xED` marks) list
|
||||
members with declared names and absolute carrier offsets; member bytes are
|
||||
stored verbatim. `V1MMWX` PC-client packages report `encrypted` directory
|
||||
status with unknown coverage and are refused on extraction; no decryption is
|
||||
attempted. See
|
||||
[wxapkg](https://github.com/morluto/rea/blob/main/docs/wxapkg.md).
|
||||
|
||||
U-Boot legacy uImages use the same inspection/extraction workflows. Prefer a
|
||||
retained summary and selected inventory views for header facts or component
|
||||
IDs. Header addresses/CPU/OS are declarations; payloads remain as stored.
|
||||
|
||||
@@ -106,7 +106,8 @@ limits, security/privacy/containment implications, and meaningful proof gaps. --
|
||||
|
||||
## Author readiness
|
||||
|
||||
<!-- The human owner completes these declarations before requesting review.
|
||||
<!-- The human owner completes these declarations before requesting review,
|
||||
unless the PR author is exempt as listed in CONTRIBUTING.md.
|
||||
The PR author is the owner by default. A different owner must explicitly accept
|
||||
ownership on this PR.
|
||||
AI agents must leave these declarations to a human to dictate to them or there could be catastrophic consequences for many users of REA.
|
||||
|
||||
@@ -807,6 +807,8 @@ jobs:
|
||||
tests/boundary/mcp/nativeDemangleMcp.test.ts
|
||||
tests/boundary/mcp/nativeMachoSummaryMcp.test.ts
|
||||
tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts
|
||||
tests/boundary/providers/native/nativeUiOffscreenWindow.test.ts
|
||||
tests/boundary/process/nativeUiVerifierLane.test.ts
|
||||
tests/boundary/cli/nativeSignatureCli.test.ts
|
||||
tests/boundary/cli/nativeMachoArm64_32Cli.test.ts
|
||||
tests/boundary/cli/nativeMachoEncryptionCli.test.ts
|
||||
|
||||
@@ -55,12 +55,12 @@ jobs:
|
||||
- uses: ./.github/actions/portable-build-cache
|
||||
with:
|
||||
build_cache_artifact: ${{ inputs.build_cache_artifact }}
|
||||
- name: Install ELF32 cross toolchains for source-owned fixtures
|
||||
- name: Install AArch64 and ELF32 cross toolchains for source-owned fixtures
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes --no-install-recommends \
|
||||
gcc-i686-linux-gnu gcc-arm-linux-gnueabi gcc-mips-linux-gnu gcc-mipsel-linux-gnu \
|
||||
libc6-dev-i386-cross libc6-dev-armel-cross libc6-dev-mips-cross libc6-dev-mipsel-cross
|
||||
gcc-aarch64-linux-gnu gcc-i686-linux-gnu gcc-arm-linux-gnueabi gcc-mips-linux-gnu gcc-mipsel-linux-gnu \
|
||||
libc6-dev-arm64-cross libc6-dev-i386-cross libc6-dev-armel-cross libc6-dev-mips-cross libc6-dev-mipsel-cross
|
||||
- name: Acquire exact upstream libraries in an isolated runner environment
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -21,6 +21,10 @@ jobs:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: website/requirements.txt
|
||||
- name: Set up Node.js for the website Worker checks
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Install sharing-image dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
|
||||
@@ -54,6 +54,10 @@ jobs:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: website/requirements.txt
|
||||
- name: Set up Node.js for the website Worker checks
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Install sharing-image dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -75,10 +79,21 @@ jobs:
|
||||
PY
|
||||
- name: Configure Pages
|
||||
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0
|
||||
# upload-pages-artifact excludes dot directories, including .well-known.
|
||||
- name: Package the website including agent discovery
|
||||
run: |
|
||||
tar --dereference --hard-dereference --directory website/_site \
|
||||
--exclude=.git --exclude=.github -cf "$RUNNER_TEMP/artifact.tar" .
|
||||
tar -tf "$RUNNER_TEMP/artifact.tar" \
|
||||
./.well-known/ard.json ./.well-known/ai-catalog.json \
|
||||
./.well-known/agent-skills/index.json > /dev/null
|
||||
- name: Upload the website
|
||||
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
path: website/_site
|
||||
name: github-pages
|
||||
path: ${{ runner.temp }}/artifact.tar
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
deploy:
|
||||
needs: prepare
|
||||
@@ -130,14 +145,24 @@ jobs:
|
||||
PUBLISH_TARGET: ${{ inputs.target }}
|
||||
run: |
|
||||
python3 -u - <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from urllib.error import URLError
|
||||
from urllib.parse import urljoin
|
||||
from urllib.parse import urljoin, urlsplit
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
def open_public(url, accept=None):
|
||||
headers = {
|
||||
"Cache-Control": "no-cache",
|
||||
"User-Agent": "REA-Website-Verifier/1.0 (+https://github.com/morluto/rea)",
|
||||
}
|
||||
if accept:
|
||||
headers["Accept"] = accept
|
||||
return urlopen(Request(url, headers=headers), timeout=10)
|
||||
|
||||
expected = os.environ["GITHUB_SHA"]
|
||||
pages_url = os.environ["PAGES_URL"]
|
||||
if not pages_url:
|
||||
@@ -153,11 +178,7 @@ jobs:
|
||||
for name, base in list(pending.items()):
|
||||
url = urljoin(base, "deployment.json") + "?commit=" + expected
|
||||
try:
|
||||
request = Request(url, headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"User-Agent": "REA-Website-Verifier/1.0 (+https://github.com/morluto/rea)",
|
||||
})
|
||||
with urlopen(request, timeout=10) as response:
|
||||
with open_public(url) as response:
|
||||
actual = json.load(response).get("commit")
|
||||
if actual == expected:
|
||||
print(f"{name} serves {expected}: {base}")
|
||||
@@ -172,6 +193,27 @@ jobs:
|
||||
time.sleep(5)
|
||||
if pending:
|
||||
sys.exit("Could not verify published revision on: " + ", ".join(pending))
|
||||
for name, base in hosts.items():
|
||||
with open_public(urljoin(base, ".well-known/agent-skills/index.json")) as response:
|
||||
skill, = json.load(response)["skills"]
|
||||
# Fetch from each selected host, including the /rea/ mirror.
|
||||
artifact_url = urljoin(base, urlsplit(skill["url"]).path.lstrip("/"))
|
||||
with open_public(artifact_url) as response:
|
||||
digest = "sha256:" + hashlib.sha256(response.read()).hexdigest()
|
||||
if digest != skill["digest"]:
|
||||
sys.exit(name + " serves a skill archive with the wrong digest")
|
||||
for manifest in ("ard.json", "ai-catalog.json"):
|
||||
with open_public(urljoin(base, ".well-known/" + manifest)) as response:
|
||||
if not json.load(response).get("entries"):
|
||||
sys.exit(name + " serves an empty discovery manifest")
|
||||
if name == "Cloudflare":
|
||||
with open_public(base, accept="text/markdown") as response:
|
||||
vary = {value.strip().lower() for value in response.headers.get("Vary", "").split(",")}
|
||||
if response.headers.get_content_type() != "text/markdown" or "accept" not in vary:
|
||||
sys.exit("Cloudflare did not negotiate Markdown with Vary: Accept")
|
||||
if 'rel="ard"' not in response.headers.get("Link", ""):
|
||||
sys.exit("Cloudflare is missing its discovery Link header")
|
||||
print(name + " serves the discovery manifests and verified skill archive")
|
||||
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary:
|
||||
links = " and ".join(f"[{name}]({url})" for name, url in hosts.items())
|
||||
summary.write(f"Published commit `{expected}` to {links}.\n")
|
||||
|
||||
@@ -128,8 +128,8 @@ owner to complete. Never attest to human review on their behalf. Draft status
|
||||
is optional; preserve the PR's current draft status unless the user asks to change
|
||||
it. Later pushes do not invalidate the readiness declaration or require another
|
||||
human sign-off. Keep the PR body and affected validation current as work changes.
|
||||
The readiness check validates
|
||||
the filled declaration; CI and automated review results do not establish that
|
||||
The readiness check validates the filled declaration or the author exemption
|
||||
listed in CONTRIBUTING.md; CI and automated review results do not establish that
|
||||
human review occurred, maintainer scope approval, or merge permission.
|
||||
|
||||
<!-- BEGIN:turborepo-agent-rules -->
|
||||
|
||||
+8
-4
@@ -93,7 +93,8 @@ Before requesting maintainer review, the human owner must:
|
||||
passing. Follow the [verification requirements](#development-feedback-and-pr-verification)
|
||||
for required checks and real-provider coverage.
|
||||
- Complete the **Author readiness** declaration in the PR template, explicitly
|
||||
stating that the PR is ready for initial review.
|
||||
stating that the PR is ready for initial review, unless the PR author is
|
||||
exempt from the declaration as listed below.
|
||||
|
||||
Keep the PR description concise and current. Explain the problem, observable
|
||||
outcome, approach, and useful evidence; link long logs or put them in optional
|
||||
@@ -114,9 +115,12 @@ validation current as work changes. AI agents must leave human declarations to
|
||||
the human owner and must not attest to a human review on their behalf.
|
||||
|
||||
The `PR readiness` check reports pending for drafts and requires a named human
|
||||
owner and all three template declarations for non-drafts. It checks that the
|
||||
declaration is filled, not whether the code or evidence is correct. Existing PRs
|
||||
can copy the **Author readiness** section from the current template.
|
||||
owner and all three template declarations for non-drafts. PRs authored by
|
||||
@morluto, @kaoru0822-kitauji, @N0zoM1z0, or @gensokyo-dev are exempt from
|
||||
the declaration; their human ownership, review, and evidence responsibilities
|
||||
above still apply. The check identifies authors by GitHub account ID. It checks
|
||||
that the declaration is filled, not whether the code or evidence is correct.
|
||||
Existing PRs can copy the **Author readiness** section from the current template.
|
||||
Open PRs must have distinct head commits because GitHub shares commit statuses.
|
||||
|
||||
Maintainers enable merge enforcement after the workflow is merged: run
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[Website](https://rea.tools/) · [Guides](https://rea.tools/guides/) · [Showcases](https://rea.tools/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Join the REA community on Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[Quick start](#quick-start) · [How REA works](#how-rea-works) · [What you can analyze](#what-you-can-analyze) · [Showcases](#showcases) · [Build RL environments](#build-rl-environments) · [FAQ](#faq) · [Documentation](#documentation)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -60,7 +60,7 @@ pin inspection sources; they are not included in the npm package.
|
||||
| ghidra-nativeaot | [MIT](https://github.com/Washi1337/ghidra-nativeaot/blob/effeb734fc570c32650f88b159608979dc7b423e/LICENSE.md) | Pinned inspection source; derived parser attribution above. |
|
||||
| JADX | [Apache-2.0](https://github.com/skylot/jadx/blob/v1.5.6/LICENSE) | The audited jadx-headless-mcp source uses JADX 1.5.6 for Android decompilation; the engine JAR is separately supplied. |
|
||||
| jadx-headless-mcp | [Apache-2.0](https://github.com/1013503897/jadx-headless-mcp/blob/5844800a486d2248fa949b74c9896285f5b54de2/LICENSE) | v0.7.1; pinned submodule and user-supplied JAR. |
|
||||
| Kuna | [Apache-2.0](https://github.com/Noelo-Lab/kuna/blob/v1.988/LICENSE), with its [NOTICE](https://github.com/Noelo-Lab/kuna/blob/v1.988/NOTICE) | Bring your own `kuna`; REA verified 1.988 and never downloads, installs or bundles it. REA invokes it as a separate process. |
|
||||
| Kuna | [Apache-2.0](https://github.com/Noelo-Lab/kuna/blob/v1.990/LICENSE), with its [NOTICE](https://github.com/Noelo-Lab/kuna/blob/v1.990/NOTICE) | Bring your own `kuna`; REA verified 1.990 and never downloads, installs or bundles it. REA invokes it as a separate process. |
|
||||
| Apktool | [Apache-2.0](https://github.com/iBotPeaches/Apktool/blob/master/LICENSE.md) | User-supplied Android resource decoder. |
|
||||
| Binwalk | [MIT](https://github.com/ReFirmLabs/binwalk/blob/4fdab3d464d97b68e0af9088df3f9e2e1545b21c/LICENSE) | v3.1.0; pinned submodule and user-supplied firmware engine. |
|
||||
| Unblob | [MIT](https://github.com/onekey-sec/unblob/blob/1fcc7a0a584a70a96c31f5a276c20944d199a089/LICENSE) | 26.6.4; pinned submodule and user-supplied firmware engine. |
|
||||
|
||||
@@ -179,6 +179,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
private List<FunctionEntry> procedureInventory;
|
||||
private List<InventoryItem> stringInventory;
|
||||
private DecompInterface decompiler;
|
||||
// A refused Program may only report its admission failure to the client.
|
||||
private String identityAdmissionFailure;
|
||||
/** Per-function native decompiler limit from the session; 0 leaves it unbounded. */
|
||||
private int decompileTimeoutSeconds;
|
||||
// DecompileProcess converts seconds to an int millisecond timer delay.
|
||||
@@ -224,14 +226,17 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
throw new IllegalStateException("Ghidra provider version does not match the session");
|
||||
}
|
||||
String importedSha256 = currentProgram.getExecutableSHA256();
|
||||
if (importedSha256 == null ||
|
||||
!constantTimeEquals(
|
||||
importedSha256.toLowerCase(Locale.ROOT),
|
||||
descriptor.targetSha256
|
||||
)) {
|
||||
throw new IllegalStateException(
|
||||
"Ghidra imported-byte digest does not match the admitted target"
|
||||
);
|
||||
if (importedSha256 == null || !importedSha256.matches("(?i)[0-9a-f]{64}")) {
|
||||
identityAdmissionFailure = "import_identity_unavailable";
|
||||
}
|
||||
else if (!constantTimeEquals(importedSha256.toLowerCase(Locale.ROOT),
|
||||
descriptor.targetSha256)) {
|
||||
identityAdmissionFailure = "import_identity_mismatch";
|
||||
}
|
||||
if (identityAdmissionFailure != null) {
|
||||
end(false);
|
||||
serve(descriptor);
|
||||
return;
|
||||
}
|
||||
admittedTargetSha256 = descriptor.targetSha256;
|
||||
// GhidraScript wraps run() in a transaction. End it before serving so
|
||||
@@ -405,6 +410,14 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
writeSuccess(writer, request.id, result);
|
||||
return;
|
||||
}
|
||||
if (identityAdmissionFailure != null) {
|
||||
// Keep the producer alive until the client has consumed the
|
||||
// refusal and closed or sent shutdown. Every other request
|
||||
// remains refused, including further startup attempts.
|
||||
writeFailure(writer, request.id, identityAdmissionFailure,
|
||||
"Ghidra imported identity admission failed");
|
||||
continue;
|
||||
}
|
||||
decompileTimeoutSeconds = request.decompileTimeoutSeconds() == null
|
||||
? descriptor.decompileTimeoutSeconds() : request.decompileTimeoutSeconds();
|
||||
writeSuccess(writer, request.id, handleRequest(request, descriptor));
|
||||
@@ -771,6 +784,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return result;
|
||||
}
|
||||
|
||||
private record ByteSearchBlock(MemoryBlock block, Address low, Address high) {}
|
||||
|
||||
private JsonObject searchMemoryBytes(JsonObject params, SessionDescriptor descriptor) throws Exception {
|
||||
// mask, start, end, and cursor are omitted when absent; JSON encoding drops those keys.
|
||||
Set<String> keys = params.keySet();
|
||||
@@ -831,16 +846,13 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
JsonArray blocks = new JsonArray();
|
||||
JsonArray skipped = new JsonArray();
|
||||
JsonArray notLoaded = new JsonArray();
|
||||
long covered = 0;
|
||||
String next = null;
|
||||
List<ByteSearchBlock> eligible = new ArrayList<>();
|
||||
for (MemoryBlock block : ordered) {
|
||||
monitor.checkCancelled();
|
||||
Address low = block.getStart();
|
||||
Address high = block.getEnd();
|
||||
Address bound = start == null ? end : start;
|
||||
if (bound != null && !bound.getAddressSpace().equals(low.getAddressSpace())) continue;
|
||||
// Without bounds, search loaded memory only. Non-loaded file parts
|
||||
// (such as .symtab) and overlays stay reachable through explicit bounds.
|
||||
if (bound == null && (!block.isLoaded() || block.isOverlay())) {
|
||||
notLoaded.add(byteSearchCoverage(block, low, high));
|
||||
continue;
|
||||
@@ -852,40 +864,66 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
skipped.add(byteSearchCoverage(block, low, high));
|
||||
continue;
|
||||
}
|
||||
if (next != null) continue;
|
||||
if (resume != null && low.compareTo(resume) < 0) low = resume;
|
||||
if (low.compareTo(high) > 0 || !low.getAddressSpace().equals(high.getAddressSpace())) continue;
|
||||
Address at = low;
|
||||
Address examinedEnd = high;
|
||||
while (at != null && at.compareTo(high) <= 0 && high.subtract(at) >= bytes.length - 1L) {
|
||||
eligible.add(new ByteSearchBlock(block, low, high));
|
||||
}
|
||||
long covered = 0;
|
||||
String next = null;
|
||||
for (int first = 0; first < eligible.size() && next == null;) {
|
||||
monitor.checkCancelled();
|
||||
int last = first;
|
||||
while (last + 1 < eligible.size()) {
|
||||
monitor.checkCancelled();
|
||||
// Ghidra's end address bounds candidate starts, not the final matched byte.
|
||||
Address found = currentProgram.getMemory().findBytes(at, high.subtract(bytes.length - 1L), bytes, masks, true, monitor);
|
||||
monitor.checkCancelled();
|
||||
if (found == null || high.subtract(found) < bytes.length - 1L) break;
|
||||
if (matches.size() == limit) {
|
||||
next = binding + ":" + canonicalAddress(found);
|
||||
examinedEnd = found.addNoWrap(bytes.length - 1L);
|
||||
break;
|
||||
}
|
||||
JsonObject match = new JsonObject();
|
||||
match.addProperty("address", canonicalAddress(found));
|
||||
match.addProperty("segment", block.getName());
|
||||
Long offset = targetFileOffset(block, found).offset();
|
||||
match.add("file_offset", offset == null ? JsonNull.INSTANCE : GSON.toJsonTree(offset));
|
||||
Function function = currentProgram.getFunctionManager().getFunctionContaining(found);
|
||||
JsonObject procedure = new JsonObject();
|
||||
if (function != null) {
|
||||
procedure.addProperty("address", canonicalAddress(function.getEntryPoint()));
|
||||
procedure.addProperty("name", function.getName(true));
|
||||
}
|
||||
match.add("procedure", function == null ? JsonNull.INSTANCE : procedure);
|
||||
matches.add(match);
|
||||
at = found.next();
|
||||
ByteSearchBlock previous = eligible.get(last);
|
||||
ByteSearchBlock following = eligible.get(last + 1);
|
||||
if (!previous.high().getAddressSpace().equals(following.low().getAddressSpace()) ||
|
||||
previous.high().next() == null || !previous.high().next().equals(following.low())) break;
|
||||
last++;
|
||||
}
|
||||
JsonObject coverage = byteSearchCoverage(block, low, examinedEnd);
|
||||
covered = Math.addExact(covered, coverage.get("bytes").getAsLong());
|
||||
blocks.add(coverage);
|
||||
Address low = eligible.get(first).low();
|
||||
Address high = eligible.get(last).high();
|
||||
if (resume != null && low.compareTo(resume) < 0) low = resume;
|
||||
if (low.compareTo(high) <= 0 && low.getAddressSpace().equals(high.getAddressSpace())) {
|
||||
Address at = low;
|
||||
Address examinedEnd = high;
|
||||
while (at != null && at.compareTo(high) <= 0 && high.subtract(at) >= bytes.length - 1L) {
|
||||
monitor.checkCancelled();
|
||||
// findBytes bounds candidate starts. The entire candidate remains in this eligible run.
|
||||
Address found = currentProgram.getMemory().findBytes(at, high.subtract(bytes.length - 1L), bytes, masks, true, monitor);
|
||||
monitor.checkCancelled();
|
||||
if (found == null || high.subtract(found) < bytes.length - 1L) break;
|
||||
if (matches.size() == limit) {
|
||||
next = binding + ":" + canonicalAddress(found);
|
||||
examinedEnd = found.addNoWrap(bytes.length - 1L);
|
||||
break;
|
||||
}
|
||||
MemoryBlock matchBlock = currentProgram.getMemory().getBlock(found);
|
||||
JsonObject match = new JsonObject();
|
||||
match.addProperty("address", canonicalAddress(found));
|
||||
match.addProperty("segment", matchBlock.getName());
|
||||
Long offset = targetFileOffset(matchBlock, found).offset();
|
||||
match.add("file_offset", offset == null ? JsonNull.INSTANCE : GSON.toJsonTree(offset));
|
||||
Function function = currentProgram.getFunctionManager().getFunctionContaining(found);
|
||||
JsonObject procedure = new JsonObject();
|
||||
if (function != null) {
|
||||
procedure.addProperty("address", canonicalAddress(function.getEntryPoint()));
|
||||
procedure.addProperty("name", function.getName(true));
|
||||
}
|
||||
match.add("procedure", function == null ? JsonNull.INSTANCE : procedure);
|
||||
matches.add(match);
|
||||
at = found.next();
|
||||
}
|
||||
for (int index = first; index <= last; index++) {
|
||||
monitor.checkCancelled();
|
||||
ByteSearchBlock item = eligible.get(index);
|
||||
Address coveredLow = item.low().compareTo(low) < 0 ? low : item.low();
|
||||
Address coveredHigh = item.high().compareTo(examinedEnd) > 0 ? examinedEnd : item.high();
|
||||
if (coveredLow.compareTo(coveredHigh) > 0) continue;
|
||||
JsonObject coverage = byteSearchCoverage(item.block(), coveredLow, coveredHigh);
|
||||
covered = Math.addExact(covered, coverage.get("bytes").getAsLong());
|
||||
blocks.add(coverage);
|
||||
}
|
||||
}
|
||||
first = last + 1;
|
||||
}
|
||||
JsonObject searched = new JsonObject();
|
||||
searched.add("blocks", blocks);
|
||||
@@ -1010,7 +1048,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
} catch (RequestFailure failure) {
|
||||
// Existing names/addresses win. Only an unresolved numeric input
|
||||
// can fall back to source coordinates; ambiguity never falls back.
|
||||
if (!failure.code.equals("not_found") || !selector.matches("0x[0-9a-fA-F]+"))
|
||||
if (!failure.code.equals("procedure_not_found") || !selector.matches("0x[0-9a-fA-F]+"))
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
@@ -1037,17 +1075,17 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
List<Address> candidates = new ArrayList<>(mappedAddresses);
|
||||
if (candidates.size() > 1)
|
||||
throw new RequestFailure("ambiguous", "File offset " + coordinate +
|
||||
throw new RequestFailure("file_offset_ambiguous", "File offset " + coordinate +
|
||||
" has multiple loaded mappings: " + candidates.stream().map(ReaGhidraBridge::canonicalAddress).toList() +
|
||||
"; select an explicit analysis address");
|
||||
if (candidates.isEmpty())
|
||||
throw new RequestFailure("not_found", "File offset " + coordinate +
|
||||
throw new RequestFailure("file_offset_unmapped", "File offset " + coordinate +
|
||||
" has no loaded source mapping for the admitted target file; Ghidra image base: " + canonicalAddress(currentProgram.getImageBase()) +
|
||||
". Inspect mappings with rea inspect-native-load-image <path> --provider ghidra --json or MCP inspect_native_load_image.");
|
||||
Address mapped = candidates.get(0);
|
||||
Function function = functionAtOrContaining(mapped);
|
||||
if (function == null)
|
||||
throw new RequestFailure("not_found", "File offset " + coordinate +
|
||||
throw new RequestFailure("procedure_not_found", "File offset " + coordinate +
|
||||
" maps to analysis address " + canonicalAddress(mapped) +
|
||||
", but no procedure starts at or contains that address in the current Ghidra analysis. " +
|
||||
"Choose an observed procedure with CLI rea search <path> . --kind procedures --mode regex --provider ghidra --json or MCP list_procedures.");
|
||||
@@ -1250,6 +1288,12 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
if (component.getDataType() instanceof ghidra.program.model.data.BitFieldDataType bitfield) {
|
||||
field.addProperty("bit_size", bitfield.getBitSize());
|
||||
field.addProperty("bit_offset", bitfield.getBitOffset());
|
||||
JsonObject bitField = new JsonObject();
|
||||
bitField.addProperty("base_type_id", bitfield.getBaseDataType().getPathName());
|
||||
bitField.addProperty("declared_bit_size", bitfield.getDeclaredBitSize());
|
||||
bitField.addProperty("component_type_lookup", typeLookupResult(bitfield));
|
||||
bitField.addProperty("base_type_lookup", typeLookupResult(bitfield.getBaseDataType()));
|
||||
field.add("bit_field", bitField);
|
||||
}
|
||||
field.addProperty("status", "observed");
|
||||
field.addProperty("flexible_tail", "unknown");
|
||||
@@ -1280,7 +1324,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
result.add("members", members);
|
||||
result.addProperty("total_fields", totalFields);
|
||||
result.addProperty("truncated", totalFields > 20_000);
|
||||
result.add("limitations", GSON.toJsonTree(List.of("Layouts are Ghidra data-type database observations; original debug/source authority and flexible-tail semantics are not inferred. Nested and recursive types retain category-path identities for separate inspection.", "At most 20000 fields or enum members are serialized per type to bound provider output. total_fields and truncated report omissions. Enum values preserve Ghidra's signed long representation.")));
|
||||
result.add("limitations", GSON.toJsonTree(List.of("Layouts are Ghidra data-type database observations; original debug/source authority and flexible-tail semantics are not inferred. Component type paths do not necessarily identify separately registered types; bit-field lookups report whether the manager resolves equivalent types.", "At most 20000 fields or enum members are serialized per type to bound provider output. total_fields and truncated report omissions. Enum values preserve Ghidra's signed long representation.")));
|
||||
if (type != null) {
|
||||
JsonObject metadata = analysisExtensions.inspectDataType(currentProgram, type, address, monitor);
|
||||
if (metadata != null) result.add("metadata_recovery", metadata);
|
||||
@@ -1288,6 +1332,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return result;
|
||||
}
|
||||
|
||||
private String typeLookupResult(DataType type) {
|
||||
DataType resolved = currentProgram.getDataTypeManager().getDataType(type.getPathName());
|
||||
return resolved == null ? "not-found" : resolved.isEquivalent(type) ? "equivalent" : "different";
|
||||
}
|
||||
|
||||
private JsonObject instructionReference(Reference reference) {
|
||||
JsonObject item = new JsonObject();
|
||||
JsonObject kind = referenceKind(reference);
|
||||
@@ -2458,7 +2507,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
if (matchProcedureNames(entry, selector).matched()) matches.add(entry.function);
|
||||
}
|
||||
if (matches.size() > 1)
|
||||
throw new RequestFailure("ambiguous", "Ghidra procedure name is ambiguous: " + value +
|
||||
throw new RequestFailure("procedure_ambiguous", "Ghidra procedure name is ambiguous: " + value +
|
||||
"; select an exact entry address from " + matches.stream()
|
||||
.map(function -> canonicalAddress(function.getEntryPoint())).toList());
|
||||
if (matches.size() == 1) return matches.get(0);
|
||||
@@ -2466,11 +2515,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
// Retain legacy bare-hex address input only when no symbol matches.
|
||||
Address address = explicitAddress ? parseReaAddress(value) : tryParseAddress(value);
|
||||
if (address == null)
|
||||
throw new RequestFailure("not_found", "Unknown Ghidra procedure name or address: " + value);
|
||||
throw new RequestFailure("procedure_not_found", "Unknown Ghidra procedure name or address: " + value);
|
||||
Function function = functionAtOrContaining(address);
|
||||
if (function == null)
|
||||
// Other procedure operations retain their analysis-address semantics.
|
||||
throw new RequestFailure("not_found", "No procedure exists at the requested address: " + value +
|
||||
throw new RequestFailure("procedure_not_found", "No procedure exists at the requested address: " + value +
|
||||
"; Ghidra image base: " + canonicalAddress(currentProgram.getImageBase()) +
|
||||
". Choose an observed analysis address. CLI: rea search <path> . --kind procedures --mode regex --provider ghidra --json; " +
|
||||
"MCP: list_procedures. Inspect the loaded base and source mappings with CLI rea analyze <path> --provider ghidra --json " +
|
||||
@@ -4916,7 +4965,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
// without retaining its cause. This producer reason is not bad syntax.
|
||||
if (exception.getDescription().equals("Stack overflow during pattern compilation"))
|
||||
throw regexStackExhausted(expression, "compiling");
|
||||
throw new RequestFailure("invalid_request", "Invalid regex pattern " + expression + ": " + exception.getDescription() + " at index " + exception.getIndex());
|
||||
throw new RequestFailure("invalid_regex_pattern", "Invalid regex pattern " + expression + ": " + exception.getDescription() + " at index " + exception.getIndex());
|
||||
}
|
||||
catch (StackOverflowError exhausted) {
|
||||
throw regexStackExhausted(expression, "compiling");
|
||||
|
||||
@@ -65,7 +65,10 @@ func observe(_ request: Request) async throws -> [String: Any] {
|
||||
guard SHA256.hash(data: executableBytes).map({ String(format: "%02x", $0) }).joined() == request.sha256 else {
|
||||
try fail("target-mismatch", "Running executable bytes differ from the active target digest")
|
||||
}
|
||||
guard let windows = CGWindowListCopyWindowInfo([.optionIncludingWindow], request.window_id) as? [[String: Any]],
|
||||
// .optionIncludingWindow omits a selected window while it is off screen, which
|
||||
// is not absence: select from every window, and let capture report what it can.
|
||||
guard let windows = (CGWindowListCopyWindowInfo([.optionAll], kCGNullWindowID) as? [[String: Any]])?
|
||||
.filter({ ($0[kCGWindowNumber as String] as? NSNumber)?.uint32Value == request.window_id }),
|
||||
windows.count == 1, let window = windows.first,
|
||||
(window[kCGWindowOwnerPID as String] as? NSNumber)?.int32Value == request.pid,
|
||||
(window[kCGWindowLayer as String] as? NSNumber)?.intValue == 0,
|
||||
|
||||
@@ -26,7 +26,6 @@ ARGUMENT_REGISTERS = {
|
||||
"x86_64h": ["rdi", "rsi", "rdx", "rcx", "r8", "r9"],
|
||||
}
|
||||
MAX_SELECTOR_BYTES = 1024
|
||||
MAX_REPORTED_LOCATIONS = 64
|
||||
MAX_OTHER_STOPS = 256
|
||||
MAX_TRACE_BYTES = 8 * 1024 * 1024
|
||||
MAX_TRACE_FRAMES = 65_536
|
||||
@@ -505,20 +504,19 @@ def _breakpoint_report(target, created, config):
|
||||
if not _wanted(config["breakpoints"][index], location["symbol"] or ""):
|
||||
continue
|
||||
count += 1
|
||||
if len(locations) < MAX_REPORTED_LOCATIONS:
|
||||
remaining_bytes = (
|
||||
MAX_BREAKPOINT_LOCATION_BYTES - retained_location_bytes
|
||||
)
|
||||
if not _fits_trace_budget(location, remaining_bytes):
|
||||
locations_truncated = True
|
||||
continue
|
||||
location_bytes = len(json.dumps(location).encode("utf-8"))
|
||||
addition = location_bytes + (1 if locations else 0)
|
||||
if addition > remaining_bytes:
|
||||
locations_truncated = True
|
||||
continue
|
||||
locations.append(location)
|
||||
retained_location_bytes += addition
|
||||
remaining_bytes = (
|
||||
MAX_BREAKPOINT_LOCATION_BYTES - retained_location_bytes
|
||||
)
|
||||
if not _fits_trace_budget(location, remaining_bytes):
|
||||
locations_truncated = True
|
||||
continue
|
||||
location_bytes = len(json.dumps(location).encode("utf-8"))
|
||||
addition = location_bytes + (1 if locations else 0)
|
||||
if addition > remaining_bytes:
|
||||
locations_truncated = True
|
||||
continue
|
||||
locations.append(location)
|
||||
retained_location_bytes += addition
|
||||
report.append({"index": index, "location_count": count, "locations": locations})
|
||||
return report, locations_truncated
|
||||
|
||||
|
||||
@@ -430,6 +430,34 @@ class NativeTraceBudgetTests(unittest.TestCase):
|
||||
self.assertEqual(report[0]["locations"], [])
|
||||
self.assertFalse(locations_truncated)
|
||||
|
||||
def test_breakpoint_report_keeps_all_locations_under_aggregate_budget(self):
|
||||
class Location:
|
||||
def __init__(self, index):
|
||||
self.index = index
|
||||
|
||||
def GetAddress(self):
|
||||
return self.index
|
||||
|
||||
class Breakpoint:
|
||||
def GetNumLocations(self):
|
||||
return 65
|
||||
|
||||
def GetLocationAtIndex(self, index):
|
||||
return Location(index)
|
||||
|
||||
with patch.object(
|
||||
tracer,
|
||||
"_location",
|
||||
side_effect=lambda address, _target: {"address": address, "symbol": None},
|
||||
), patch.object(tracer, "_wanted", return_value=True):
|
||||
report, locations_truncated = tracer._breakpoint_report(
|
||||
object(), [[Breakpoint()]], {"breakpoints": [{}]}
|
||||
)
|
||||
|
||||
self.assertEqual(report[0]["location_count"], 65)
|
||||
self.assertEqual(len(report[0]["locations"]), 65)
|
||||
self.assertFalse(locations_truncated)
|
||||
|
||||
def test_breakpoint_report_bounds_aggregate_location_json(self):
|
||||
tracer.lldb.LLDB_INVALID_ADDRESS = -1
|
||||
symbol_name = (
|
||||
|
||||
@@ -228,6 +228,22 @@ func processRunTokenObservation(
|
||||
parse: Result<ParsedRunToken, RunTokenReadError>,
|
||||
codeSigningFlags: () -> UInt32?
|
||||
) -> RunTokenObservation {
|
||||
// Native inspection restrictions apply even when argv is incomplete or
|
||||
// overwritten, before parsing reaches the environment. Keep this fact at
|
||||
// the native boundary; cleanup still decides whether the process is related.
|
||||
func withheldEnvironment() -> RunTokenObservation? {
|
||||
guard let flags = codeSigningFlags() else { return nil }
|
||||
if (flags & csPlatformBinary) != 0 {
|
||||
return unavailable(pid: pid, reason: "platform_binary_environment_withheld")
|
||||
}
|
||||
if (flags & csRestrict) != 0 {
|
||||
return unavailable(pid: pid, reason: "restricted_process_environment_withheld")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func unavailableEnvironment(_ reason: String) -> RunTokenObservation {
|
||||
withheldEnvironment() ?? unavailable(pid: pid, reason: reason)
|
||||
}
|
||||
switch parse {
|
||||
case .success(let parsed):
|
||||
if let runId = parsed.runId {
|
||||
@@ -238,40 +254,25 @@ func processRunTokenObservation(
|
||||
if parsed.environmentEntryCount > 0 {
|
||||
return RunTokenObservation(pid: pid, state: "readable", run_id: nil, reason: nil)
|
||||
}
|
||||
let flags = codeSigningFlags()
|
||||
if let flags, (flags & csPlatformBinary) != 0 {
|
||||
return unavailable(pid: pid, reason: "platform_binary_environment_withheld")
|
||||
}
|
||||
if let flags, (flags & csRestrict) != 0 {
|
||||
return unavailable(pid: pid, reason: "restricted_process_environment_withheld")
|
||||
}
|
||||
if let withheld = withheldEnvironment() { return withheld }
|
||||
return RunTokenObservation(pid: pid, state: "readable", run_id: nil, reason: nil)
|
||||
case .failure(let failure):
|
||||
switch failure {
|
||||
case .environmentUnavailable:
|
||||
let flags = codeSigningFlags()
|
||||
if let flags, (flags & csPlatformBinary) != 0 {
|
||||
return unavailable(pid: pid, reason: "platform_binary_environment_withheld")
|
||||
}
|
||||
if let flags, (flags & csRestrict) != 0 {
|
||||
return unavailable(pid: pid, reason: "restricted_process_environment_withheld")
|
||||
}
|
||||
return unavailable(pid: pid, reason: "environment_unavailable")
|
||||
return unavailableEnvironment("environment_unavailable")
|
||||
case .ambiguousEnvironmentBoundary:
|
||||
if let flags = codeSigningFlags(), (flags & csRestrict) != 0 {
|
||||
return unavailable(pid: pid, reason: "restricted_process_environment_withheld")
|
||||
}
|
||||
return unavailable(pid: pid, reason: "ambiguous_environment_boundary")
|
||||
return unavailableEnvironment("ambiguous_environment_boundary")
|
||||
// These failures already establish that token bytes were available.
|
||||
case .duplicateToken:
|
||||
return unavailable(pid: pid, reason: "duplicate_run_token")
|
||||
case .invalidTokenEncoding:
|
||||
return unavailable(pid: pid, reason: "invalid_run_token_encoding")
|
||||
case .appleVectorUnavailable:
|
||||
return unavailable(pid: pid, reason: "apple_vector_unavailable")
|
||||
return unavailableEnvironment("apple_vector_unavailable")
|
||||
case .invalidBuffer:
|
||||
return unavailable(pid: pid, reason: "malformed_procargs")
|
||||
return unavailableEnvironment("malformed_procargs")
|
||||
case .invalidBufferAt(let stage):
|
||||
return unavailable(pid: pid, reason: "malformed_procargs_\(stage.rawValue)")
|
||||
return unavailableEnvironment("malformed_procargs_\(stage.rawValue)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ PROFILE = "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2"
|
||||
PACKAGES = {"pwntools": "4.15.0", "pyelftools": "0.33", "unicorn": "2.1.2"}
|
||||
OUTPUT_BYTES = 64 * 1024 * 1024
|
||||
# (e_machine, ELF class bits, byte order) -> architecture pwntools must also report.
|
||||
PROFILES = {(62, 64, "little"): "amd64", (3, 32, "little"): "i386", (40, 32, "little"): "arm",
|
||||
(8, 32, "little"): "mips", (8, 32, "big"): "mips"}
|
||||
PROFILE_SCOPE = ("Layout profile requires ELF64 x86-64, ELF32 i386, ELF32 ARM little-endian "
|
||||
"or ELF32 MIPS little- or big-endian objects.")
|
||||
PROFILES = {(62, 64, "little"): "amd64", (183, 64, "little"): "aarch64", (3, 32, "little"): "i386",
|
||||
(40, 32, "little"): "arm", (8, 32, "little"): "mips", (8, 32, "big"): "mips"}
|
||||
PROFILE_SCOPE = ("Layout profile requires ELF64 x86-64, ELF64 AArch64 little-endian, ELF32 i386, "
|
||||
"ELF32 ARM little-endian or ELF32 MIPS little- or big-endian objects.")
|
||||
|
||||
|
||||
class LayoutFailure(Exception):
|
||||
|
||||
@@ -203,8 +203,12 @@ and during shutdown.
|
||||
`uninstall_adb_package` declare `mutatesTarget` and never run implicitly:
|
||||
pushes check for existing device files before transfer and refuse them
|
||||
unless `overwrite` is set. A failed or unexpected existence probe stops
|
||||
transfer even with `overwrite`; only a clean `test -e` exit of 1 with no
|
||||
output is interpreted as absence. The check is not atomic, so a different
|
||||
transfer even with `overwrite`. A completed `test -e` exit of 1 establishes
|
||||
absence only with empty stdout and either empty stderr or a recognized,
|
||||
completed adb-server startup notice sequence. Startup notices are host
|
||||
observations, not remote-path diagnostics; successful push receipts retain
|
||||
them in `raw_result.device_path_probes`. Incomplete startup, extra diagnostics,
|
||||
truncation and cancellation do not establish absence. The check is not atomic, so a different
|
||||
process can create the path before `adb push` writes it. When available, the device's
|
||||
`sha256sum` verifies the transfer. A missing command or output without a
|
||||
recognizable digest reports `device_digest_source: "unavailable"` with an
|
||||
|
||||
@@ -1149,7 +1149,12 @@ global conflict involving an equal pair is not used to normalize either pair; th
|
||||
|
||||
A step agrees when the statuses and the normalised bodies are equal. The report gives agreement overall, per probe and
|
||||
per tool, `agreement_ignoring_order` for steps that differ only in item order, and each step's differences, at most 20,
|
||||
with a JSON pointer and both values cut short:
|
||||
with a JSON pointer and both values cut short. Each step reports `differences_total` before that cap,
|
||||
`differences_omitted`, and `differences_truncated` (true when entries were omitted). Agreement and categories use the
|
||||
complete comparison. Omitted entries are not retained, so `--full-output` cannot recover them; inspect the original
|
||||
capture and probe Evidence to investigate beyond the returned pointers.
|
||||
|
||||
The difference categories are:
|
||||
|
||||
| Category | Meaning |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
|
||||
@@ -73,6 +73,9 @@ spellings; the original SDK declaration strings remain unchanged.
|
||||
it remains null for unknown provider target SDKs, nonliteral declarations, or
|
||||
components with filters targeting API 31 or later that omit the now-required
|
||||
explicit declaration. A limitation records the latter validation error.
|
||||
As in Android's package parser, an activity, alias or receiver filter without
|
||||
an `<action>` is discarded and does not count toward either rule; a service
|
||||
keeps such a filter. `intent_filter_count` still counts every declared filter.
|
||||
Provider authorities and all inventory dimensions are bounded. `status:
|
||||
"partial"` and `limitations` identify omitted declarations or values; a
|
||||
capacity limitation appears only when an actual collection limit was reached.
|
||||
|
||||
@@ -9,9 +9,11 @@ ordinary binary inspection use the same modular capability.
|
||||
## Bring your own engine
|
||||
|
||||
Real verification covers **Linux x64** hosts and **EXEC/DYN/REL** objects in
|
||||
these profiles: ELF64 x86-64, ELF32 i386, ELF32 little-endian ARM and ELF32
|
||||
little- or big-endian MIPS. Other machines, classes and byte orders, including
|
||||
AArch64 and big-endian ARM, return `unsupported_target`. Provide an absolute
|
||||
these profiles: ELF64 x86-64, ELF64 little-endian AArch64, ELF32 i386, ELF32
|
||||
little-endian ARM and ELF32 little- or big-endian MIPS. Other machines, classes
|
||||
and byte orders, including big-endian ARM, return `unsupported_target`. Other
|
||||
hosts, including Linux arm64, return `capability_unavailable` naming the
|
||||
detected host before any target is read. Provide an absolute
|
||||
Python executable whose environment already contains unchanged pwntools 4.15.0,
|
||||
pyelftools 0.33 and Unicorn 2.1.2:
|
||||
|
||||
|
||||
@@ -500,7 +500,7 @@ REA retains local observation data and removes only URL userinfo credentials, ex
|
||||
connection/frame inventories; matching direction/opcode sequences alone do
|
||||
not prove alignment after drops. Redirect resource types use only shared known metadata:
|
||||
an omitted type leaves equality unknown rather than proving a modification.
|
||||
- Console observations with a stack source retain call type, argument types, the CDP `Runtime.Timestamp` in milliseconds since Unix epoch, and source location. An absent or invalid timestamp is `null` and makes `console_events` incomplete; an observed zero remains zero. Argument types are listed by CDP argument position. An absent or non-array argument inventory is `null`, and a malformed argument keeps its position as `unknown`; either makes `console_events` incomplete, while an observed empty inventory remains `[]`. A malformed stack or top stack frame is not replaced by its caller: the console event is excluded, and a request initiator location or WebMCP `registration_source` is `null`; each is reported as `invalid_protocol_value`. This epoch-millisecond clock is separate from the monotonic-second timestamps in `observe-web-session`. When console text capture is selected, already-delivered primitive values are retained verbatim with their original `argument_index`; objects, getters, and remote properties are never expanded. Text capture is `unavailable` when the argument inventory is unknown.
|
||||
- Console observations with a stack source retain call type, argument types, the CDP `Runtime.Timestamp` in milliseconds since Unix epoch, and source location. An absent or invalid timestamp is `null` and makes `console_events` incomplete; an observed zero remains zero. Argument types are listed by CDP argument position. An absent or non-array argument inventory is `null`, and a malformed argument keeps its position as `unknown`; either makes `console_events` incomplete, while an observed empty inventory remains `[]`. A malformed stack or top stack frame is not replaced by its caller: the console event is excluded, and a request initiator location or WebMCP `registration_source` is `null`; each is reported as `invalid_protocol_value`. Source locations are CDP's zero-based lines and UTF-16 columns. A negative column, which Chrome reports on the first line of some inline scripts, is kept as reported and does not locate a column in the source URL; an invalid line or a non-integer column is `null` (WebMCP `registration_source` is `null`) and is reported as `invalid_protocol_value`. This epoch-millisecond clock is separate from the monotonic-second timestamps in `observe-web-session`. When console text capture is selected, already-delivered primitive values are retained verbatim with their original `argument_index`; objects, getters, and remote properties are never expanded. Text capture is `unavailable` when the argument inventory is unknown.
|
||||
- WebSocket observations retain direction, opcode, and payload byte length. A missing or malformed CDP opcode is returned as `null`, with opcode-dependent payload byte length and shape also `null`, and marks `websocket_frames` incomplete. Missing or wrong-typed payload data is not treated as empty; its byte length is `null`, and a text payload shape is unknown. Numeric opcode `0` and an observed empty payload remain distinct from missing data. Opcode is nullable in the public inspection schema, so clients must handle unknown protocol values. When shape capture is selected, text messages are classified as text or complete value-free JSON shape; binary bytes, hashes, prefixes, and raw payloads are never retained.
|
||||
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` selection. Cookie fingerprints cover cookies applicable to the current main-frame URL, including its path, rather than every cookie path on that origin. Other storage remains scoped to the origin. Cache fingerprints hash all decoded response bytes, including bodies above 64 KiB, subject to the [64 MiB per-message CDP limit](#request-boundary). Missing or undecodable cache bodies and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
|
||||
- Unavailable or malformed storage inventories mark `storage_keys` unavailable, even without fingerprints. Comparisons suppress inferred key additions and removals while preserving modifications proven by complete fingerprints on both sides. Previously exported captures are not retroactively corrected; recapture them to obtain these coverage markers.
|
||||
|
||||
@@ -9,7 +9,10 @@ liveness deadlines. `INPUT_JSON` may be inline JSON or a JSON file path.
|
||||
|
||||
The minimal request contains only browser launch/connect selection, `start_url`,
|
||||
and at least one `actions` entry. Launch `headless` defaults to `true` and can
|
||||
be set to `false` where the host supports a visible browser. Navigations follow
|
||||
be set to `false` where the host supports a visible browser. Launched Chromium
|
||||
keeps its sandbox; on hosts that cannot provide one, such as root in a
|
||||
container, set `REA_BROWSER_NO_SANDBOX=true` for the REA process. Termux on
|
||||
Android always launches without it. Navigations follow
|
||||
the destinations named by the scenario and the page's ordinary browser requests,
|
||||
and [request steps](#request-steps) send only to the start URL's origin;
|
||||
there is no separate origin allowlist. Request steps use a temporary dispatch
|
||||
|
||||
+49
-1
@@ -39,6 +39,17 @@ pass an absolute path. REA reads and authenticates the file, then projects it
|
||||
like retained Evidence, so a large file is bounded by the file reader rather
|
||||
than charged as inline input.
|
||||
|
||||
An argument that accepts inline JSON or a JSON file path also accepts `-`, which
|
||||
reads the request from standard input, so a generated request needs no
|
||||
temporary file:
|
||||
|
||||
```bash
|
||||
jq -nc '{source: {kind: "evidence-file", path: "app-evidence.json"}, view: {kind: "summary"}}' | rea inspect-analysis-view -
|
||||
```
|
||||
|
||||
Standard input is read once as a single UTF-8 JSON document without a
|
||||
byte-order mark, and it is limited to 8 MiB. Put a larger request in a file.
|
||||
|
||||
Local file inspection commands reject empty paths and NUL bytes before resolving
|
||||
relative inputs, auxiliary files or output destinations. An empty string never
|
||||
selects the working directory; use an explicit `.` when selecting that directory
|
||||
@@ -270,6 +281,17 @@ Format versions 6 through 8 retain header, block and directory facts with
|
||||
none/LZ4/LZ4HC block decompression. See [UnityFS](unityfs.md) for encryption
|
||||
and LZMA coverage boundaries.
|
||||
|
||||
Inspect a WeChat mini-program package without WeChat or an unpacker:
|
||||
|
||||
```sh
|
||||
rea inspect-artifact app.wxapkg --detail summary --evidence_output evidence.json --json
|
||||
rea extract-artifact app.wxapkg --json
|
||||
```
|
||||
|
||||
Plaintext packages retain header, table and member facts with verbatim member
|
||||
bytes. `V1MMWX` packages are detected as encrypted coverage and refused on
|
||||
extraction. See [wxapkg](wxapkg.md) for layout and coverage boundaries.
|
||||
|
||||
Inspect an ESP32-family app or bootloader image without esptool:
|
||||
|
||||
```sh
|
||||
@@ -569,6 +591,17 @@ prints a failure, so it omits the same-session reuse and
|
||||
`parameter`) for a new invocation and rerun. For example, a Ghidra decompile
|
||||
deadline names `REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS`.
|
||||
|
||||
`rea debug-with-gdb` and `rea debug-with-rizin` hold their debugger only for the
|
||||
command's own process. If cleanup cannot confirm the debugger stopped, they print
|
||||
`cleanup_incomplete` and exit nonzero without stopping it. `remediation.action`
|
||||
names the process, as does `retained_debugger_pids` in `details.diagnostics`, or
|
||||
in `details.diagnostics.cleanup_error.details.diagnostics` when the command also
|
||||
failed or was interrupted. Exiting closes the debugger's command and output pipes.
|
||||
REA does not establish what the debugger or its target does next, so check that
|
||||
process. An MCP server that shuts down likewise exits with status 1 instead of
|
||||
waiting on a debugger it could not stop. It names the process on stderr and as
|
||||
`retained_debugger_pids` in its error log.
|
||||
|
||||
If one-shot analysis fails and closing its session also fails, the original
|
||||
error details remain inline and `details.cleanup_error` describes the later
|
||||
close failure. Each diagnostic retains its own reported resources; the later
|
||||
@@ -601,6 +634,9 @@ Saved snapshots include only cache entries whose subject identifies the selected
|
||||
target. Unbound observations and observations of child artifacts remain available
|
||||
in the open session and its Evidence bundle; they are excluded from the target's
|
||||
saved replay entries without changing their subjects.
|
||||
The snapshot also retains available Evidence linked by selected records, including
|
||||
transitive sources with an unbound or different subject. Those sources are not
|
||||
replay entries for the selected target. Unrelated records remain excluded.
|
||||
|
||||
Snapshots retain eligible target-scoped question histories in full, including
|
||||
mutation Evidence recorded while another target was active. Observations and
|
||||
@@ -615,6 +651,11 @@ rea analyze /absolute/path/to/program --provider ghidra --snapshot /absolute/pat
|
||||
```
|
||||
|
||||
An exact CLI cache hit is normally read before starting a provider process.
|
||||
`decompile --provider ghidra --snapshot ...` also retains its single selected
|
||||
Program's pseudocode result, even without an explicit document selector. Old
|
||||
snapshots that contain only the Evidence need one new query to add that replay
|
||||
entry. This does not retain every body from `decompile-all` or make a persistent
|
||||
session's implicit document replayable.
|
||||
On Windows, Ghidra first admits the selected source through its native snapshot
|
||||
owner, so replay can start that provider.
|
||||
In MCP, `open_binary` accepts `snapshot_path` to import a snapshot atomically
|
||||
@@ -667,6 +708,10 @@ results and Evidence. These are cached observations, not a saved provider
|
||||
database: only eligible exact queries can reuse a result, and new or live
|
||||
queries can still require provider startup. The CLI uses the same snapshot
|
||||
format and preserves these records when loading and updating it.
|
||||
When an MCP session replays a provider observation, its delivered Evidence identifies
|
||||
the original observation through `evidence_links` and reports that the
|
||||
provider was not rerun. The original and its available source records remain
|
||||
in the investigation bundle.
|
||||
|
||||
## Import, export and compare Evidence
|
||||
|
||||
@@ -1174,9 +1219,12 @@ treated as malformed.
|
||||
exact array path with `--filter-output` and provide comma-separated direct
|
||||
scalar columns with `--columns`; column order is the output column order:
|
||||
|
||||
For an artifact inventory request that selects the `occurrences` collection
|
||||
and includes `logical_path` and `artifact_id` in its view fields:
|
||||
|
||||
```bash
|
||||
rea inspect-analysis-view request.json --format csv \
|
||||
--filter-output normalized_result.rows --columns name,address
|
||||
--filter-output normalized_result.items --columns logical_path,artifact_id
|
||||
```
|
||||
|
||||
The first line is a REA metadata record,
|
||||
|
||||
@@ -45,6 +45,10 @@ the client's configured command or reproduce the client's environment. If doctor
|
||||
passes but the client disconnects, inspect the client's startup error and registered
|
||||
launcher; see [verification scope](client-verification.md#launch-environment-verification).
|
||||
|
||||
`rea doctor` has the same limit for provider checks, and reports a provider setting
|
||||
the registration's file does not carry as a
|
||||
[non-blocking unknown](installation.md#check-readiness-for-your-task).
|
||||
|
||||
## Runtime settings and client deadlines
|
||||
|
||||
`update_analysis_settings` changes curated defaults inside the running REA
|
||||
@@ -306,6 +310,22 @@ Kimi Code also discovers personal skills in `~/.agents/skills` and project-local
|
||||
and verifies the matching skill files through the normal shared-skill flow.
|
||||
Start a new Kimi Code session to pick up MCP and skill changes.
|
||||
|
||||
## LM Studio
|
||||
|
||||
For [LM Studio](https://lmstudio.ai/docs/app/mcp), setup writes `mcpServers.rea`
|
||||
into `mcp.json` in the LM Studio home directory. Setup finds that directory as
|
||||
LM Studio does: the path stored in `~/.lmstudio-home-pointer`, otherwise
|
||||
`~/.cache/lm-studio` when it exists, otherwise `~/.lmstudio`. Setup only reads
|
||||
the pointer file. LM Studio parses `mcp.json` as strict JSON, so setup rejects
|
||||
a file with comments, trailing commas, or a byte order mark without a backup
|
||||
or a write. The entry carries `command`, `args`, and `env`, and setup keeps
|
||||
every other server. Local models often have small context windows, so setup
|
||||
selects `REA_MCP_INPUT_SCHEMA_PROFILE=compact` unless the stored entry or the
|
||||
setup environment selects a profile explicitly. Doctor reports an entry with no
|
||||
explicit profile as stale, as for Kimi Code. LM Studio 0.4.26 on macOS loaded
|
||||
this registration, and a local model called an REA tool through it. Linux and
|
||||
Windows remain unverified.
|
||||
|
||||
## Pi
|
||||
|
||||
For [Pi](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/mcp.md),
|
||||
@@ -628,21 +648,20 @@ writing a registration ZCode cannot load.
|
||||
|
||||
## FreeBuff
|
||||
|
||||
For FreeBuff, the proposed integration writes `mcpServers.rea` into the user-scope
|
||||
`~/.agents/mcp.json` shared-agent file. Entries carry `command`, `args`, and
|
||||
`env`; stdio is the default transport. The proposed personal skill location is
|
||||
`~/.agents/skills`, so setup installs its skill copy there. Because the same
|
||||
For FreeBuff, `rea setup --client freebuff` writes `mcpServers.rea` into the
|
||||
user-scope `~/.agents/mcp.json` shared-agent file. Entries carry `command`,
|
||||
`args`, and `env`; stdio is the default transport. The personal skill location
|
||||
is `~/.agents/skills`, so setup installs its skill copy there. Because the same
|
||||
file doubles as ZCode's user-scope fallback (see above), a `freebuff`
|
||||
registration is also in the file ZCode uses until ZCode defines its own native server.
|
||||
Setup and doctor detect FreeBuff from its own profile directory
|
||||
(`~/.config/freebuff-desktop`), not from the shared `.agents` folder that
|
||||
REA's skill installs create for every client.
|
||||
registration is also in the file ZCode uses until ZCode defines its own native
|
||||
server. Setup and doctor detect FreeBuff from its own profile directory
|
||||
(`~/.config/freebuff-desktop`), not from the shared `.agents` folder that REA's
|
||||
skill installs create for every client.
|
||||
|
||||
The configuration write and personal skill locations are proposed integration
|
||||
boundaries. FreeBuff MCP loading and skill discovery have not been verified
|
||||
with an installed native FreeBuff client/version; filesystem tests cover REA's
|
||||
editing, discovery, and ownership behavior only. Automatic applicability
|
||||
requires the FreeBuff profile directory even when the shared MCP file exists.
|
||||
FreeBuff MCP loading and skill discovery have not been verified with an
|
||||
installed native FreeBuff client/version; filesystem tests cover REA's editing,
|
||||
discovery, and ownership behavior only. Automatic applicability requires the
|
||||
FreeBuff profile directory even when the shared MCP file exists.
|
||||
When uninstall cannot establish FreeBuff or another shared-storage reader from
|
||||
product discovery, a malformed or unreadable shared MCP file is retained
|
||||
unchanged and does not stop other REA removals, including `--purge-data`. When
|
||||
@@ -650,8 +669,9 @@ FreeBuff or ZCode product applicability is established or unresolved, the same
|
||||
malformed shared file stops uninstall before removals. See
|
||||
[Diagnose, update, and remove](installation.md#diagnose-update-and-remove).
|
||||
|
||||
FreeBuff setup support requires `rea-agents@6.8.0` or later. Older versions
|
||||
reject `rea setup --client freebuff` as an unsupported client. See the
|
||||
FreeBuff setup support requires `rea-agents@6.8.0` or later (including the
|
||||
current npm `latest` release). Older versions reject
|
||||
`rea setup --client freebuff` as an unsupported client. See the
|
||||
[installation guide](installation.md#supported-agents) for client availability.
|
||||
|
||||
## DeepSeek Harness
|
||||
|
||||
@@ -121,9 +121,9 @@ launch environment affects the next REA process.
|
||||
is the byte-budget shape: a decimal safe integer with a 10 MiB floor
|
||||
(`MINIMUM_RESPONSE_BYTES`), the same shared schema, and no silent fallback. A
|
||||
change needs a REA restart and a matching client receive buffer. It does not
|
||||
raise REA's reserved string limit or Node's string ceiling. The setup
|
||||
allowlist does not copy this setting. A new headroom setting should, following
|
||||
the regex deadline.
|
||||
raise REA's reserved string limit or Node's string ceiling. Like the regex
|
||||
deadline, it is in `SELECTED_REGISTRATION_SETTINGS`, so setup forwards an
|
||||
explicit selection into the client registration.
|
||||
|
||||
A per-call budget can have a runtime default without having an environment
|
||||
setting. JavaScript `max_heap_mb`
|
||||
|
||||
@@ -110,6 +110,15 @@ not establish full metadata validity or correspondence to a native binary;
|
||||
version 24 inspection still applies only to supported header layouts. See
|
||||
[IL2CPP metadata](il2cpp-metadata.md).
|
||||
|
||||
In a managed directory with a Unity Mono witness, `Assembly-CSharp.dll` and
|
||||
`Assembly-CSharp-firstpass.dll` get an `inspect_managed_members` probe listed
|
||||
first; project-named asmdef assemblies keep only the ordinary managed probes.
|
||||
The same probe is offered for those names in `Managed/` of a
|
||||
`<Product>_BackUpThisFolder_ButDontShipItWithYourGame` folder (or when that
|
||||
folder is the root); whether its IL matches the shipped IL2CPP code is unknown.
|
||||
Point the scan at the game folder: a root that is the `<Product>_Data` or
|
||||
`Managed` folder itself has no Mono witness path and gets no member probe.
|
||||
|
||||
Candidates are derived or inferred from these facts and do not prove loading or
|
||||
execution. BepInEx, MelonLoader, Easy Anti-Cheat and BattlEye labels identify
|
||||
path conventions only; runtime presence stays unknown. No bypass is provided.
|
||||
|
||||
+7
-1
@@ -38,6 +38,12 @@ were examined; an unavailable section name does not erase those note facts.
|
||||
For GNU notes, `gnu.build_id_hex`, `gnu.abi_tag`, or `gnu.gold_version` are
|
||||
filled when the descriptor matches that type. Other owners keep `gnu` null.
|
||||
|
||||
For Linux `CORE` notes, `core.prpsinfo`, `core.file`, `core.siginfo`, or
|
||||
`core.auxv` are decoded for process arguments and status (`NT_PRPSINFO`), mapped
|
||||
file memory regions and paths (`NT_FILE`), fault signal and code (`NT_SIGINFO`), and
|
||||
auxiliary vectors (`NT_AUXV`) across 32-bit and 64-bit images. Other owners or unparsed
|
||||
notes keep `core` null.
|
||||
|
||||
`PT_NOTE` and `SHT_NOTE` are examined independently. The same file range may
|
||||
appear twice when both tables declare it.
|
||||
|
||||
@@ -48,7 +54,7 @@ the image never carried notes in another container or overlay.
|
||||
|
||||
The selected file is limited to 64 MiB by default (512 MiB maximum). At most
|
||||
32 note entries are examined by default across every table. Retained note
|
||||
names, section names, and GNU descriptor text share a scan budget based on
|
||||
names, section names, GNU descriptor text, and core dump string representations share a scan budget based on
|
||||
the source bytes plus any decoded section-name table, capped at
|
||||
`max_file_bytes`. The raw source and decoded table must also fit together
|
||||
within that ceiling. If admission fails, the error reports the required
|
||||
|
||||
@@ -28,6 +28,15 @@ The host must already provide util-linux `prlimit`, normally `/usr/bin/prlimit`;
|
||||
no system tools and changes no user configuration. [Upstream provenance and
|
||||
license](https://github.com/morluto/rea/blob/main/third_party/evmole/README.md) are packaged.
|
||||
|
||||
Limiter discovery validates the selected path without launching it. A relative
|
||||
path or directory is reported as `not_configured`, a missing file as
|
||||
`executable_missing`, and denied access or another filesystem-probe failure as
|
||||
`runtime_unavailable`. Recovery names the selected path and
|
||||
`REA_EVM_PRLIMIT_COMMAND`; denied access asks for execute permission and
|
||||
filesystem access. The original admission or system code stays in diagnostics.
|
||||
An available filesystem probe does not verify the selected tool's version or
|
||||
util-linux compatibility, and REA does not substitute another command.
|
||||
|
||||
## Evidence semantics
|
||||
|
||||
- `encoding` is required: raw bytes or UTF-8 hex, with optional `0x` prefix and
|
||||
|
||||
@@ -26,6 +26,9 @@ REA does not install these tools, their dependencies or system packages. It
|
||||
checks the tool's `--version` banner when executing a request. Binwalk must
|
||||
print `binwalk <version>` and Unblob must print the version alone. Inspection and
|
||||
extraction have independent prerequisites; one does not require the other.
|
||||
Capability discovery recommends `REA_BINWALK_COMMAND` for missing inspection
|
||||
and `REA_UNBLOB_COMMAND` for missing extraction. Configuring the other engine
|
||||
does not enable the unavailable operation.
|
||||
Both require util-linux `prlimit`, normally `/usr/bin/prlimit`; override its
|
||||
absolute path with `REA_FIRMWARE_PRLIMIT_COMMAND` if needed. `rea doctor` does
|
||||
not currently inspect firmware readiness. Engine selection/version checks run
|
||||
@@ -242,9 +245,12 @@ tree semantics, vulnerability scanning, emulation or hardware runtime capture.
|
||||
3, allowed range 1–10.
|
||||
|
||||
Staging budgets are polled and rechecked after exit; short-lived overshoot is
|
||||
possible. The per-file process limit is the larger of the selected payload budget
|
||||
and the fixed report/diagnostic file allowances, so a small payload selection
|
||||
still permits the mandatory report. A tighter inherited host limit still applies.
|
||||
possible. The per-file process limit is one byte above the larger of the selected
|
||||
payload budget and the fixed report/diagnostic file allowances, so a small payload
|
||||
selection still permits the mandatory report, and a staged file the kernel stops
|
||||
at the limit exceeds its budget instead of passing as complete. A tighter
|
||||
inherited host limit still applies and can remove that byte, so a staged file
|
||||
that reaches the installed limit is rejected as possibly truncated.
|
||||
The inherited file-size limit bounds each file, not total storage;
|
||||
address-space limits are per process, not an aggregate memory quota. Worker
|
||||
count is not CPU affinity for every external extractor. These limits do not
|
||||
|
||||
@@ -109,9 +109,13 @@ server; older servers keep the complete request after schema discovery.
|
||||
header check does not establish valid table contents or exported symbols.
|
||||
- **`libflutter` facts** are per-ABI observations: byte size, SHA-256
|
||||
digest, GNU build IDs from ELF-declared `PT_NOTE` / `SHT_NOTE` ranges,
|
||||
and any _labeled_ strings the build
|
||||
carries. Release engine builds often strip the `Dart VM version` string —
|
||||
absence is reported as `null`, not guessed. Android clang/LLVM toolchain
|
||||
and Dart/toolchain strings the build carries. `dart_version` accepts the
|
||||
labeled `Dart VM version:` form and the unlabeled Dart
|
||||
`Version::String()` form (`<version> (<channel>) (<commit time>)` with an
|
||||
optional ` on "<os>_<arch>"` suffix) that release engines embed. An
|
||||
unlabeled match is a candidate based on its shape; it is not bound to a
|
||||
Dart symbol and another string can match. `null` means no supported pattern
|
||||
was found, not proof that no version exists. Android clang/LLVM toolchain
|
||||
lines are reported verbatim as build provenance.
|
||||
`build_id_candidates` preserves distinct IDs. `build_id` is populated only
|
||||
when all declared note ranges were examined and one ID was found;
|
||||
@@ -122,10 +126,13 @@ server; older servers keep the complete request after schema discovery.
|
||||
Compressed `SHT_NOTE` sections are skipped with an unsupported-compression
|
||||
limitation and partial coverage; IDs from other note ranges remain candidates.
|
||||
Encoded note storage is never interpreted as note headers or inflated.
|
||||
- Labeled strings must fit their complete grammar. `dart_version_capped`
|
||||
and `toolchain_capped` independently report labels omitted by grammar or
|
||||
projection bounds; either makes identification coverage partial. Values
|
||||
split across read chunks are retained without publishing truncated prefixes.
|
||||
- Version and toolchain strings must fit their complete grammar.
|
||||
`dart_version_capped` and `toolchain_capped` independently report labels
|
||||
omitted by grammar or projection bounds; either makes identification
|
||||
coverage partial. Values split across read chunks are retained without
|
||||
publishing truncated prefixes. Unlabeled runs that miss the
|
||||
`Version::String()` grammar are ignored and do not set
|
||||
`dart_version_capped`.
|
||||
- **`coverage: "partial"`** also appears when an ABI carries `libapp.so`
|
||||
without `libflutter.so` (or vice versa).
|
||||
- The APK itself is digested before parsing, so `target.sha256` binds the
|
||||
|
||||
@@ -330,9 +330,11 @@ Without bounds, loaded memory blocks are eligible, in Ghidra address order.
|
||||
Blocks that are not loaded (file parts such as `.symtab`) and overlay blocks are
|
||||
listed in `searched.skipped_not_loaded` and are not searched; pass `start` or
|
||||
`end` in their address space to search them.
|
||||
Overlapping matches are included; matches never cross block boundaries.
|
||||
Each match reports its address, segment, unique source file offset (or null),
|
||||
and containing procedure (or null).
|
||||
Overlapping matches are included, including across adjacent eligible initialized
|
||||
blocks in the same address space. Gaps and excluded or uninitialized blocks break
|
||||
a search run. Each match reports its address, starting segment, unique source file
|
||||
offset for that starting address (or null), and containing procedure (or null).
|
||||
A starting offset does not imply the full match is contiguous in the file.
|
||||
|
||||
Pages default to 100 matches and accept 1 to 1000. Pass `next_cursor` as `cursor`
|
||||
with the same target, profile, pattern, mask and bounds to continue. `complete`
|
||||
|
||||
@@ -25,6 +25,33 @@ by default; `--integrity-policy record-and-continue` retains the observed bytes
|
||||
and an explicit untrusted contradiction. An all-zero MD5 is an unknown claim,
|
||||
not a verified digest.
|
||||
|
||||
Artifact comparison pairs members by `logical_path`, the stored path without
|
||||
`res://`, and compares their bytes and occurrence facts. It does not compare
|
||||
`godot-pck-entry` metadata. Godot 4.3
|
||||
[stores `res://` paths](https://github.com/godotengine/godot/blob/77dcf97d82cbfe4e4615475fa52ca03da645dbd8/editor/export/editor_export_platform.cpp#L228)
|
||||
and [keys members by them](https://github.com/godotengine/godot/blob/77dcf97d82cbfe4e4615475fa52ca03da645dbd8/core/io/file_access_pack.cpp#L50-L52).
|
||||
Godot 4.4.1
|
||||
[stores paths without the prefix](https://github.com/godotengine/godot/blob/49a5bc7b616bd04689a2c89e89bda41f50241464/editor/export/editor_export_platform.cpp#L257-L260)
|
||||
and [removes it before keying](https://github.com/godotengine/godot/blob/49a5bc7b616bd04689a2c89e89bda41f50241464/core/io/file_access_pack.cpp#L48-L50).
|
||||
Each version finds `res://scenes/main.gd` in its own export, but comparing the
|
||||
stored `source_path` would mark every member changed between such exports.
|
||||
`logical_path` names the member Godot's lookup finds, except in two cases.
|
||||
REA normalizes Unicode to NFC, so a member renamed only between Unicode forms
|
||||
compares unchanged, although Godot treats it as another path. Godot reads `\`
|
||||
as `/` and REA keeps it literal, so a member spelled with backslashes compares
|
||||
as removed and added. Directory order and offsets are layout. The encryption
|
||||
bit, the only member flag REA admits, is the occurrence's `encrypted` field. A
|
||||
member whose encryption changes compares `unknown` in `metadata` and
|
||||
`availability`: the change is known, but REA does not read encrypted bytes,
|
||||
so whether the script changed is unknown. Godot's exporter
|
||||
[stores the MD5 of a member's original bytes](https://github.com/godotengine/godot/blob/a13da4feb8d8aefc283c3763d33a2f170a18d541/editor/export/editor_export_platform.cpp#L465-L468),
|
||||
as the 4.3 and 4.4.1 exporters do, so the declared MD5 does not depend on the
|
||||
encryption IV. Encrypted members whose declared MD5s differ therefore also
|
||||
compare `unknown` in `metadata` and `availability`: the MD5s declare
|
||||
different scripts, but REA cannot verify them, and equal MD5s add nothing for
|
||||
the same reason. An encrypted member with an MD5 on only one side stays
|
||||
`unknown`.
|
||||
|
||||
## CLI
|
||||
|
||||
Start with a compact summary and keep the complete Evidence bundle:
|
||||
|
||||
@@ -36,6 +36,8 @@ The MCP tool `project_harmony_application_graph` takes the same
|
||||
|
||||
- exact component paths, artifact IDs, and hashes (manifests, Ark bytecode,
|
||||
resources, native libraries, JavaScript, signing)
|
||||
- `unclassified_components` with the same paths and content identities for
|
||||
authenticated files outside those categories and the App Pack child list
|
||||
- a packaging-model hint: Stage is claimed only from an exactly cased `module.json` entry
|
||||
outside a classified resource path, FA only from `config.json` outside a
|
||||
classified resource path, otherwise `unknown`
|
||||
@@ -55,6 +57,24 @@ being classified as code files. Ark and JavaScript runtime hints summarize the
|
||||
classified bytecode and JavaScript lists; supported source extensions share the
|
||||
same classification rule as their component rows.
|
||||
|
||||
`unclassified_components` preserves files such as `pkgContextInfo.json`,
|
||||
`.pages.info`, a HarmonyOS `.har` shared library, and a non-certificate
|
||||
`META-INF` entry without interpreting their contents. The `.har` suffix also
|
||||
names HTTP Archive JSON, so identifying one needs verified ZIP bytes rather than
|
||||
a path rule. Different paths sharing the same bytes remain separate entries. App
|
||||
Pack children of an App Pack root are already reported in `app_pack_children`,
|
||||
so they are not repeated as unclassified files. A nested `.hap` or `.hsp` member
|
||||
of a HAP or HSP root is disclosed in `unclassified_components` instead, because
|
||||
child-package inventory is not recursive. Unreadable or encrypted content remains
|
||||
in `unavailable_components` with unknown identity instead.
|
||||
|
||||
`coverage.status: "complete-within-inventory"` describes the authenticated
|
||||
source inventory and bounded bridge projection, not semantic understanding of
|
||||
every file. Unclassified files do not make a complete inventory partial; missing
|
||||
inventory pages, source coverage limits, unavailable content and bridge limits
|
||||
still qualify coverage independently. An empty unclassified list on a partial
|
||||
inventory does not establish that the whole package has no unclassified files.
|
||||
|
||||
Every claim is path-presence only. Manifest semantics (module identity,
|
||||
abilities, entry points, permissions, dependencies), Ark bytecode contents and
|
||||
versions, resource resolution, signing verification, and observed native
|
||||
|
||||
+4
-1
@@ -36,7 +36,10 @@ regions and are never filled.
|
||||
|
||||
Region `format_metadata` (`hex-record-region`) records `start_address`,
|
||||
`end_address`, `length`, `record_count`, and `first_line`/`last_line`: the
|
||||
1-based line span of the contributing data records.
|
||||
1-based line span of the contributing data records. `compare_artifacts`
|
||||
compares none of these fields: a region's name encodes its start and end
|
||||
addresses, its length follows from them, and record and line counts are
|
||||
layout.
|
||||
|
||||
Root `format_metadata` (`hex-records`) records:
|
||||
|
||||
|
||||
+13
-14
@@ -47,20 +47,19 @@ For runtime instrumentation, see the [Frida guide](./frida-instrumentation.md).
|
||||
|
||||
## Choose a guide
|
||||
|
||||
| Task | Guides |
|
||||
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Identify a desktop installation | [Runtime candidates, evidence and follow-up files](desktop-applications.md) |
|
||||
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [CPython pyc files](python-pyc.md), [PE certificates](pe-certificates.md), [PE imports](pe-imports.md), [PE TLS/load config](pe-load-config.md), [ELF notes](elf-notes.md), [Linux kernel modules](kernel-modules.md), [Apple IPS](apple-ips.md), [Erlang BEAM](erlang-beam.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
|
||||
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [CPython pyc files](python-pyc.md), [PE certificates](pe-certificates.md), [PE imports](pe-imports.md), [PE TLS/load config](pe-load-config.md), [ELF notes](elf-notes.md), [Apple IPS](apple-ips.md), [Erlang BEAM](erlang-beam.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
|
||||
| Select a native provider or import format | [Ghidra semantics](ghidra-provider.md), [Windows](windows-ghidra-p0.md), [DOS](ghidra-dos.md), [raw bytes](raw-binary.md), [GameCube DOL](ghidra-dol.md), [MIPS](ghidra-mips.md), [PSP](ghidra-psp.md), [NativeAOT](ghidra-nativeaot.md), [IDA](ida-provider.md), [Kuna](kuna-provider.md), [JEB](jeb-analysis.md) |
|
||||
| Analyze JavaScript or Electron artifacts | [Static analysis](javascript-artifact-reconstruction.md), [tracing and comparisons](javascript-application-workflows.md), [application graph](javascript-application-graph.md), [source recovery](javascript-recovery.md) |
|
||||
| Compare static JavaScript with runtime observations | [Runtime attribution](javascript-runtime-observation.md), [reconciliation](javascript-runtime-reconciliation.md), [Electron observation](electron-observation.md), [Electron fuses](electron-fuses.md), [Release exposure](release-exposure.md) |
|
||||
| Inspect assemblies, packages, or firmware | [Godot PCK](godot-pck.md), [UnityFS](unityfs.md), [U-Boot uImage](uimage.md), [.NET](managed-code-analysis.md), [Python packages](python-packaged-applications.md), [Android](android-analysis.md), [APK signatures](apk-signature.md), [Apktool resources](apktool-resource-analysis.md), [ADB acquisition](adb-device-analysis.md), [Apple](apple-application-analysis.md), [Harmony](harmony-analysis.md), [firmware](firmware-analysis.md), [ESP images](esp-image.md), [ESP flash images](esp-flash-image.md), [WASM](wasm-artifacts.md), [EVM](evm-bytecode.md) |
|
||||
| Observe or compare a browser scenario | [CDP observation](browser-observation.md), [scenarios](browser-scenario-contract.md), [runtime evidence](web-runtime.md), [historical captures](web-network-captures.md) |
|
||||
| Serve recorded behavior as an agent environment | [Agent environments](agent-environments.md) |
|
||||
| Trace captured website source | [Source locations](web-source-location.md), [module traces](website-module-trace.md), [script export](website-script-export.md) |
|
||||
| Capture and compare a command | [Process capture](process-capture.md) |
|
||||
| Use CLI/MCP and retain evidence | [CLI](cli.md), [MCP contracts](mcp-contracts.md), [agent prompts](mcp-prompts.md), [obligation ledgers](reconstruction-obligation-ledgers.md) |
|
||||
| Task | Guides |
|
||||
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Identify a desktop installation | [Runtime candidates, evidence and follow-up files](desktop-applications.md) |
|
||||
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [CPython pyc files](python-pyc.md), [PE certificates](pe-certificates.md), [PE imports](pe-imports.md), [PE TLS/load config](pe-load-config.md), [ELF notes](elf-notes.md), [Linux kernel modules](kernel-modules.md), [Apple IPS](apple-ips.md), [Erlang BEAM](erlang-beam.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
|
||||
| Select a native provider or import format | [Ghidra semantics](ghidra-provider.md), [Windows](windows-ghidra-p0.md), [DOS](ghidra-dos.md), [raw bytes](raw-binary.md), [GameCube DOL](ghidra-dol.md), [MIPS](ghidra-mips.md), [PSP](ghidra-psp.md), [NativeAOT](ghidra-nativeaot.md), [IDA](ida-provider.md), [Kuna](kuna-provider.md), [JEB](jeb-analysis.md) |
|
||||
| Analyze JavaScript or Electron artifacts | [Static analysis](javascript-artifact-reconstruction.md), [tracing and comparisons](javascript-application-workflows.md), [application graph](javascript-application-graph.md), [source recovery](javascript-recovery.md) |
|
||||
| Compare static JavaScript with runtime observations | [Runtime attribution](javascript-runtime-observation.md), [reconciliation](javascript-runtime-reconciliation.md), [Electron observation](electron-observation.md), [Electron fuses](electron-fuses.md), [Release exposure](release-exposure.md) |
|
||||
| Inspect assemblies, packages, or firmware | [Godot PCK](godot-pck.md), [UnityFS](unityfs.md), [wxapkg](wxapkg.md), [U-Boot uImage](uimage.md), [.NET](managed-code-analysis.md), [Python packages](python-packaged-applications.md), [Android](android-analysis.md), [APK signatures](apk-signature.md), [Apktool resources](apktool-resource-analysis.md), [ADB acquisition](adb-device-analysis.md), [Apple](apple-application-analysis.md), [Harmony](harmony-analysis.md), [firmware](firmware-analysis.md), [ESP images](esp-image.md), [ESP flash images](esp-flash-image.md), [WASM](wasm-artifacts.md), [EVM](evm-bytecode.md) |
|
||||
| Observe or compare a browser scenario | [CDP observation](browser-observation.md), [scenarios](browser-scenario-contract.md), [runtime evidence](web-runtime.md), [historical captures](web-network-captures.md) |
|
||||
| Serve recorded behavior as an agent environment | [Agent environments](agent-environments.md) |
|
||||
| Trace captured website source | [Source locations](web-source-location.md), [module traces](website-module-trace.md), [script export](website-script-export.md) |
|
||||
| Capture and compare a command | [Process capture](process-capture.md) |
|
||||
| Use CLI/MCP and retain evidence | [CLI](cli.md), [MCP contracts](mcp-contracts.md), [agent prompts](mcp-prompts.md), [obligation ledgers](reconstruction-obligation-ledgers.md) |
|
||||
|
||||
For development, use [testing](testing.md), [client verification](client-verification.md),
|
||||
[tool design](tool-design.md), [admission limits and recovery](design/limits-and-recovery.md),
|
||||
|
||||
+57
-9
@@ -378,18 +378,32 @@ a particular caller-supplied input will be valid. See
|
||||
|
||||
Select the part you want to check:
|
||||
|
||||
| Task | Readiness check |
|
||||
| ----------------------------------- | ------------------------------------------------------------------------ |
|
||||
| Static JavaScript/Electron analysis | Run `rea analyze-javascript-application PATH --json` directly. |
|
||||
| One analysis engine | `rea doctor --provider ghidra --json` (or `hopper`, `ida`, `kuna`) |
|
||||
| One agent registration | `rea doctor --client codex --json` (see [client IDs](#supported-agents)) |
|
||||
| Installed workflow instructions | `rea doctor --skill --json` |
|
||||
| Task | Readiness check |
|
||||
| ----------------------------------- | -------------------------------------------------------------------------- |
|
||||
| Static JavaScript/Electron analysis | Run `rea analyze-javascript-application PATH --json` directly. |
|
||||
| One analysis engine | `rea doctor --provider ghidra --json` (or `hopper`, `ida`, `jadx`, `kuna`) |
|
||||
| One agent registration | `rea doctor --client codex --json` (see [client IDs](#supported-agents)) |
|
||||
| Installed workflow instructions | `rea doctor --skill --json` |
|
||||
|
||||
A scoped report has `scope.mode: "explicit"`. Its `scope_checks` determine
|
||||
`healthy` and the exit status. Other checks appear in `informational_checks`;
|
||||
`environment_healthy` summarizes the full audit. `--target` adds a target check,
|
||||
but the report remains audit-wide unless a scope option is also supplied.
|
||||
|
||||
Provider checks read the terminal running doctor. A client-started server may
|
||||
receive a different environment, and doctor cannot observe it. When an available
|
||||
provider relies on a setting that a registration's file does not record, or
|
||||
records with other text, doctor adds a non-blocking
|
||||
`registration_environment:<client>` check with `classification: "unknown"`. The
|
||||
setting names appear in that registration's `provider_settings`. Selecting the
|
||||
client or an affected provider places the check in `scope_checks`; it never
|
||||
changes `healthy`, because the client may still pass the setting on. Preview
|
||||
`rea setup --client <client-id> --skill=false --dry-run` from that terminal and
|
||||
approve it to record the settings, then restart the client. To see what the
|
||||
running server can use, call `binary_session` in the client and read
|
||||
`result.tool_availability`. For Hopper, only an explicit `HOPPER_LAUNCHER_PATH`
|
||||
is compared.
|
||||
|
||||
An invalid `REA_LOG_LEVEL` fails every scope, including one client or the
|
||||
installed skill, because the executable rejects it before any command. A
|
||||
selected client also fails for every other setting that MCP startup rejects,
|
||||
@@ -640,6 +654,7 @@ hosted connector; see [its configuration](client-configuration.md#other-client-s
|
||||
| Factory Droid | `droid` |
|
||||
| Trae | `trae` |
|
||||
| WorkBuddy | `workbuddy` |
|
||||
| LM Studio | `lm_studio` |
|
||||
| ZCode | `zcode` |
|
||||
| DeepSeek Harness | `dsh` |
|
||||
| FreeBuff | `freebuff` |
|
||||
@@ -649,6 +664,8 @@ hosted connector; see [its configuration](client-configuration.md#other-client-s
|
||||
`freebuff` requires `rea-agents@6.8.0` or later. Older releases reject
|
||||
`rea setup --client freebuff` as an unsupported client.
|
||||
|
||||
`lm_studio` requires a release after 6.8.0.
|
||||
|
||||
See [client configuration and troubleshooting](client-configuration.md) for
|
||||
configuration paths, profiles, trust policies, and schema-limit workarounds.
|
||||
|
||||
@@ -1146,7 +1163,11 @@ selected formula's `libexec/openjdk.jdk/Contents/Home`, for example
|
||||
Java version bounds declared by your Ghidra installation; REA does not select
|
||||
another JDK when `JAVA_HOME` is explicit or the PATH JVM runs. On macOS,
|
||||
when the PATH launcher cannot start a JVM, it tries executable Homebrew
|
||||
`opt/openjdk` candidates, preferring the host architecture's prefix. Failed
|
||||
`opt/openjdk` and installed `opt/openjdk@<major>` candidates, preferring the host
|
||||
architecture's prefix and the unversioned formula within each prefix. Versioned
|
||||
aliases are considered in ascending major order within Ghidra's declared Java
|
||||
bounds; the selected executable still undergoes the normal Java probe and
|
||||
admission checks. REA does not invoke Homebrew or install a JDK. Failed
|
||||
discovery probes remain available in installation diagnostics. Doctor distinguishes an absent Java
|
||||
command from a command that runs and fails, and retains bounded probe output
|
||||
and any parsed properties when the selected runtime cannot be admitted.
|
||||
@@ -1408,6 +1429,20 @@ in the project root; when omitted it defaults to the selected target's filename.
|
||||
Paths, globs and command flags are not program names. The matching `.rep`
|
||||
directory must be present; restore `.grez` archives with Ghidra first.
|
||||
|
||||
For a `.gzf` packed program, first import it into a new Ghidra project in an
|
||||
existing, caller-selected directory. A packed program is not a `.grez` project
|
||||
archive:
|
||||
|
||||
```bash
|
||||
"$GHIDRA_INSTALL_DIR/support/analyzeHeadless" "/absolute/new-project-directory" "new-project" \
|
||||
-import "/absolute/program.gzf" -noanalysis
|
||||
```
|
||||
|
||||
Select the resulting `new-project.gpr` with `REA_GHIDRA_PROJECT_PATH`. If the
|
||||
imported program name differs from the selected target's filename, set
|
||||
`REA_GHIDRA_PROGRAM_NAME` to the name reported by Ghidra's import. REA then
|
||||
opens its own read-only project copy; it does not import the packed program.
|
||||
|
||||
```bash
|
||||
export REA_ANALYSIS_PROVIDER=ghidra
|
||||
export REA_GHIDRA_PROJECT_PATH=/absolute/projects/app-analysis.gpr
|
||||
@@ -1429,18 +1464,31 @@ the original database; REA reads selected files only for fingerprinting and copy
|
||||
This mode does not copy unrelated sibling projects, rerun repair scripts, or
|
||||
admit bridge database edits. Cleanup removes the owned copy.
|
||||
The bridge checks the program's recorded import SHA-256 against the admitted
|
||||
target digest; mismatches and project changes fail explicitly. Reopen the target
|
||||
to resolve a new profile after intentionally changing the source project.
|
||||
target digest. An unavailable or malformed recorded digest is reported as an
|
||||
unverified identity, distinct from a known mismatch; neither admits the program
|
||||
or triggers a fresh import. Select a matching target or explicitly configure a
|
||||
different project or program. Changes to the source project also fail admission;
|
||||
reopen the target to resolve a new profile after intentionally changing it.
|
||||
|
||||
The profile records the caller path, resolved project/name, program and content
|
||||
fingerprint. Original analysis settings and completeness remain **unknown**;
|
||||
prior annotations or edits are caller-project state, not fresh REA discoveries.
|
||||
For supported header-selected MIPS ELF targets, the profile also binds the
|
||||
selected target's ELF metadata; it does not claim a fresh-import MIPS lane was
|
||||
used for the saved project.
|
||||
A matching recorded import digest does not claim that the database is unedited.
|
||||
Saved REA seed/analyzer reports remain historical facts in retained source Evidence;
|
||||
they are not interpreted as overrides applied by this session.
|
||||
Project content participates in snapshot identity, so different project states
|
||||
cannot reuse the same profile's cached observations.
|
||||
|
||||
If a function selector is missing, use `list_procedures` or `search_procedures`
|
||||
to inspect the selected project's observed inventory. A missing entry does not
|
||||
prove the function is absent from the executable. To explicitly compare with a
|
||||
fresh import, start another REA process without `REA_GHIDRA_PROJECT_PATH` and
|
||||
`REA_GHIDRA_PROGRAM_NAME`; the fresh analysis will not include the selected
|
||||
project's annotations.
|
||||
|
||||
To prepare a new project explicitly, use your existing Ghidra installation in a
|
||||
new directory you own (this writes that project; REA does not save it for you):
|
||||
|
||||
|
||||
@@ -876,8 +876,22 @@ and Evidence. If that expansion does not fit, the file node retains
|
||||
`static_analysis` and any admitted `semantic_module_analysis` observations,
|
||||
including their exact values and source locations. Use the public modules item
|
||||
view to read them. The graph marks those unexpanded relationships as unknown.
|
||||
If cumulative result capacity is exhausted, analysis stops; inventoried but
|
||||
unexamined files remain present with explicit unknown scopes.
|
||||
|
||||
The parent charges everything it retains against the heap that was free when
|
||||
analysis started, less a transfer reserve. Semantic facts and expanded
|
||||
relationships stop at half of that capacity, and a source refused there keeps
|
||||
its static facts with the refusal reported. Static facts can continue to three
|
||||
quarters of it while the live heap also has room for another copy of what it
|
||||
holds, so results charged for earlier sources do not cost later sources their
|
||||
static facts when the heap can still hold them. When that share runs short,
|
||||
relationships that were reserved but not yet expanded are given up before any
|
||||
static facts, and each affected file reports the omission.
|
||||
|
||||
If static facts themselves exhaust that share, or the parent's live heap cannot
|
||||
hold the next checkpoint, analysis stops; inventoried but unexamined files remain
|
||||
present with explicit unknown scopes. The failure reports the limit that applied
|
||||
in bytes, what was still available, the live heap in use and what the checkpoint
|
||||
needed.
|
||||
|
||||
### Large local JSON inputs
|
||||
|
||||
|
||||
@@ -93,6 +93,10 @@ coverage. It does not print captured source text.
|
||||
Runtime targets, frames, scripts, and workers become capture-scoped graph
|
||||
nodes. REA evaluates compatible static candidates conservatively:
|
||||
|
||||
V8 Inspector targets whose protocol type is `node` use JavaScript asset/module
|
||||
candidates for their verified entry location. A target-only correspondence does
|
||||
not mark that script loaded; load-state evidence requires a script observation.
|
||||
|
||||
1. A captured-source SHA-256 plus one matching mapped location yields
|
||||
a content-and-location match.
|
||||
2. When no static candidate exists at a mapped location, one unique exact
|
||||
|
||||
+25
-3
@@ -1,9 +1,27 @@
|
||||
# Kuna provider
|
||||
|
||||
[Kuna](https://github.com/Noelo-Lab/kuna) is an Apache-2.0 decompiler distributed as one `kuna` executable. For a new installation, use Kuna 1.988, REA's recommended verified release. REA uses a `kuna` you supply; it does not download, install, update or bundle Kuna. REA accepts other releases, which it reports as unverified in `rea doctor` and in each analysis profile. The verified-release flag compares the reported version; it does not authenticate the executable. The 1.988 real-provider checks ran on macOS arm64; Linux and other hosts have not been verified with that release.
|
||||
[Kuna](https://github.com/Noelo-Lab/kuna) is an Apache-2.0 decompiler distributed as one `kuna` executable. For a new installation, use Kuna 1.990, REA's recommended verified release. REA uses a `kuna` you supply; it does not download, install, update or bundle Kuna. REA accepts other releases; `rea doctor` and analysis Evidence report them as unverified. Verification compares the reported version; it does not authenticate the executable. Analysis profiles retain the observed version and executable digest, independently of REA's current verified-release policy. The 1.990 real-provider checks ran on macOS arm64; Linux and other hosts have not been verified with that release.
|
||||
|
||||
Kuna is static: it reads the selected image and never executes it.
|
||||
|
||||
This profile change makes earlier Kuna analysis snapshots and resumable directory
|
||||
exports incompatible, even with the same Kuna executable. Keep the old files:
|
||||
their recorded Evidence remains historical. To continue analysis, open the
|
||||
binary without the old snapshot and save a new one, or choose a new export
|
||||
directory. Existing snapshot Evidence can be extracted from its
|
||||
`evidence_bundle` JSON member and imported as a separate Evidence bundle; that
|
||||
does not restore cached workflow entries or resume the old directory export.
|
||||
For example, using `jq` and a new output path:
|
||||
|
||||
```bash
|
||||
jq -e '.evidence_bundle | select(type == "object")' old-snapshot.json > history.json
|
||||
rea evidence-import history.json --json
|
||||
```
|
||||
|
||||
The CLI command validates the bundle in a one-shot session. To use the records
|
||||
in an MCP session, call `import_evidence_bundle` with the extracted file path.
|
||||
This validates the embedded bundle, not the rest of the snapshot container.
|
||||
|
||||
## Configure
|
||||
|
||||
```bash
|
||||
@@ -30,6 +48,10 @@ completes. Cleanup retries keep both the observations and the exact file owner.
|
||||
| `REA_KUNA_TIMEOUT_MS` | 120 s | Deadline for one single-function decompile, and for the inventory run REA makes when the export ends without one. |
|
||||
| `REA_KUNA_JOBS` | 1 | `decompile-project --jobs`. 1 is Kuna's reference answer; more jobs finish sooner but lose callee-first prototypes, and change about 40–60% of function bodies on the measured binaries. |
|
||||
|
||||
Pseudocode read from a parallel project export includes a prototype-context
|
||||
limitation in its Evidence, including in function dossiers and composed
|
||||
workflows. Early single-function results have their own source limitation.
|
||||
|
||||
A single-function decompile, inventory, string, graph or xref run that exceeds its deadline fails as `provider_timeout`, naming the setting that owns the deadline and that raising it takes a restart. A project export that exceeds `REA_KUNA_PROJECT_TIMEOUT_MS` is an export failure instead: requests that need the export report `capability_unavailable` with the export's own retry guidance.
|
||||
|
||||
Windows is not supported yet. Supported targets are ELF and thin Mach-O executables for x86-64, AArch64 and ARM; PE and universal Mach-O are rejected with a typed reason.
|
||||
@@ -61,7 +83,7 @@ The first text or assembly request in a session looks for this target's export i
|
||||
- Meanwhile, pseudocode comes from one `kuna decompile --addr` run. Name and address lookups, and listings, use the inventory the export takes with `kuna functions --json` before it decompiles. The session runs `kuna functions` itself, under `REA_KUNA_TIMEOUT_MS`, only when no export is building (an inventory-only request does not start one) or the export ends without an inventory.
|
||||
- Requests for the same function share one run; it stops when its last waiting request is cancelled. Each run analyzes the whole image again in its own process, so one REA process runs at most as many at once as the host has cores beyond `REA_KUNA_JOBS` (at least one). Further requests wait in order, and a waiting request is answered from the export if it finishes first. A run whose stop REA cannot prove keeps its slot until `close_binary` proves it stopped.
|
||||
- The session keeps each completed single-function text, up to 16 MiB in all with the least recently used dropped first, and answers repeats from it until the export can. After a failed export that lasts for the session.
|
||||
- Assembly and data names wait for the export; progress notifications report the wait every ten seconds. A session does not rebuild a failed export: requests that need it fail with `retryable: false`, the export's failure, its captured Kuna output and what to change before reopening the target. A setting such as `REA_KUNA_PROJECT_TIMEOUT_MS` or `REA_KUNA_JOBS` is read when REA starts, so changing it also needs a restart. Pseudocode keeps using single-function runs.
|
||||
- Assembly and data names wait for the export; progress notifications report the wait every ten seconds. A session does not rebuild a failed export: requests that need it fail with `retryable: false`, the export's failure, its captured Kuna output and what to change before reopening the target. REA does not expose per-function records from a failed project export, even if the retained Kuna diagnostic refers to them. Single-function fallback Evidence reports the failed state and this limitation. If cleanup remains incomplete, its error takes precedence and `close_binary` retries the retained resources. A setting such as `REA_KUNA_PROJECT_TIMEOUT_MS` or `REA_KUNA_JOBS` is read when REA starts, so changing it also needs a restart. Pseudocode keeps using single-function runs.
|
||||
- A single-function answer says so in its limitations. It lacks the export's whole-program prototypes, so a later request can return different text for the same function.
|
||||
|
||||
Pseudocode, assembly, `analyze_function` and data-name answers record where their text came from in `raw_result.source`: `single-function`, or `project-export` with the cache directory. `procedure_pseudo_code` and `procedure_assembly` answers from the export also name the file, byte span and the file's SHA-256. Combined `analyze_function` answers retain both facet records in `raw_result.source.files`, each with its file, digest and byte span. Both texts come from one export; losing that project retries the complete report. Listings and procedure lookups carry no `raw_result`; omitted [Mach-O data entries](#mach-o-data-entries) are reported in their limitations. Agents can open or search the export's `.c` and `.asm` there directly when that is cheaper than calling a tool per function. Text read that way is target-derived, like any decompiler output, but it bypasses the untrusted-content marker that REA's tool results carry.
|
||||
@@ -72,7 +94,7 @@ The assembly label index is scanned once per export reader. Cancelling a request
|
||||
|
||||
Regex procedure and string search uses ECMAScript Unicode matching in a cancellable worker with a matching deadline and a worker old-generation heap. Literal matching remains the default. Cancellation or a matching deadline leaves the session available. The deadline is `REA_REGEX_MATCHING_DEADLINE_MS`, a positive decimal integer from 1 to 2147483647 ms. Unset uses 5000 ms. A larger value is refused because Node schedules a delay past that signed 32-bit bound as 1 ms. Invalid values are configuration errors and do not fall back to the default. The setting is read from the environment that starts REA, so a change needs a restart. A deadline failure names the effective milliseconds, whether they came from the default or the setting, the default, and the maximum. The worker old-generation heap does not raise it. The worker heap request is `REA_REGEX_WORKER_HEAP_REQUEST_MIB`, a decimal integer from 7 to 16384 MiB. Unset uses 128 MiB. A request below 7 MiB aborts the worker during startup, and a larger value is refused. Invalid values are configuration errors and do not fall back to 128. The setting is read from the environment that starts REA, so a change needs a restart. A heap failure names the effective request, whether it came from the default or the setting, the default, the floor, and the maximum. A positive Node.js `--max-old-space-size` on the process that starts REA, including `NODE_OPTIONS`, overrides that request and sets the effective heap. `--max-old-space-size-percentage` takes precedence over that flag in either order. REA does not turn the percentage into a MiB, so the effective heap stays unestablished instead of the worker heap request or the absolute flag. The reported `NODE_OPTIONS` value is the process initial environment when that block can be read. A later edit to the environment is not the value Node applied, and an unreadable initial value leaves the effective heap unestablished. The launch Node options are the initial command line on Linux when that buffer still has a program entry, not a later edit to `process.execArgv`. A preload can clear that array after V8 has applied the flags. Setting `process.title` or `--title` overwrites the command line, so a buffer without a program entry leaves the effective heap unestablished. `--env-file` or `--env-file-if-exists` can supply `NODE_OPTIONS` when the initial environment has none. That file is not part of the initial environment block, so the effective heap stays unestablished instead of the worker heap request. An initial `NODE_OPTIONS` is still used, because Node does not let the file replace it. A positive `--max-heap-size`, including the underscore form and launch `NODE_OPTIONS` on Node versions that allow it, sets the total heap rather than the old generation and can be lower than the worker heap request. The effective old generation stays unestablished while that value is in effect. A zero or empty value clears it. `--experimental-config-file` or `--experimental-default-config-file` can set `nodeOptions`, including a heap percentage, from a file that is not read back. The effective heap stays unestablished when either flag is present, including when an absolute `--max-old-space-size` is also on the command line or in the recovered `NODE_OPTIONS`. Node accepts the config path as `=` or the next argument.
|
||||
|
||||
Single-function subprocess output and each cached source facet share a 16 MiB buffered-text capacity. Cached spans are checked before allocation; larger text remains in the local export and the error identifies the file and range. `analyze_function` with `detail: "complete"` (the CLI default) returns the complete dossier inline, which can still be large; later retained views do not remove that first transfer. The MCP default retains the dossier and returns its summary, so page the facets you need through `inspect_analysis_view`, or prefer the individual text tools for a focused question.
|
||||
Single-function subprocess output and each cached source facet share a 16 MiB buffered-text capacity. Cached spans are checked before allocation; larger project-export spans remain in the local export and the error identifies the file and range. A single-function run does not by itself retain a complete project export. `analyze_function` with `detail: "complete"` (the CLI default) returns the complete dossier inline, which can still be large; later retained views do not remove that first transfer. The MCP default retains the dossier and returns its summary, so page the facets you need through `inspect_analysis_view`, or prefer the individual text tools for a focused question.
|
||||
|
||||
## Mach-O data entries
|
||||
|
||||
|
||||
+131
-9
@@ -218,6 +218,12 @@ Malformed provider replies return `unreadable_output` for
|
||||
`resolve_containing_procedure` before successful-result projection;
|
||||
cancellation and incomplete cleanup remain errors.
|
||||
|
||||
For a procedure match, `procedure_references` with `direction: "incoming"` can
|
||||
provide the selected provider's reference kinds when supported. Its scope covers
|
||||
the procedure body, so compare target addresses when selecting references to the
|
||||
exact entry. It is a separate observation and does not establish runtime use or
|
||||
complete reference discovery.
|
||||
|
||||
REA prepares complete MCP results within the pinned stdio client's 10 MiB
|
||||
receive-buffer budget, including both text and structured representations and
|
||||
room for the JSON-RPC envelope. Tool errors omit `structuredContent` from the
|
||||
@@ -278,6 +284,17 @@ to `/absolute/view-request.json`, then run
|
||||
`rea inspect-analysis-view /absolute/view-request.json --json`. This also reads
|
||||
pre-summary source records without changing their identity; no live provider is started.
|
||||
|
||||
Unavailable or inapplicable native collections keep `items.rows: null` and
|
||||
report `coverage: {"status":"unavailable","examined":0,"total":null,"next_offset":null,"exhausted":false}`.
|
||||
This applies to unrecorded `native-load-image` checks/sections and inapplicable
|
||||
`native-context` result limitations. Their total is unknown; no continuation is
|
||||
offered. Stop paging that collection and inspect the returned unknowns rather
|
||||
than treating it as an observed empty set or retrying a null offset. Recorded
|
||||
empty collections, empty selections and offsets past the end retain numeric
|
||||
totals and exhausted page coverage. Clients upgrading from the earlier
|
||||
zero-total representation must handle the new `unavailable` status and nullable
|
||||
total; historical saved views remain valid.
|
||||
|
||||
`procedure_address`, `procedure_callees`, `procedure_callers`,
|
||||
`resolve_containing_procedure` and `get_call_graph` also accept `detail: "summary"`
|
||||
(complete remains the default). The summary includes the first 64 recorded
|
||||
@@ -292,6 +309,12 @@ limitation and reason IDs index that page's `items.messages` dictionary. Expand
|
||||
those references to recover exact text, repeated messages, and absent versus
|
||||
empty optional fields. Failed nodes retain their original error.
|
||||
|
||||
Retained `xrefs` Evidence also supports `native-observation` address pages and
|
||||
`native-context` source caveats. `xrefs` still returns complete Evidence by
|
||||
default and does not accept `detail`; when its MCP response exceeds the budget,
|
||||
follow the returned continuation to page the retained result. These addresses
|
||||
remain untyped; paging does not add reference kinds or establish completeness.
|
||||
|
||||
These views reference complete source context through `source_context.digest`
|
||||
and caveat counts. Read it using the same parent with
|
||||
`view: {"kind":"native-context","facet":"overview"}`. The other facets are
|
||||
@@ -371,6 +394,12 @@ is null. `total` counts all matching heads; each page preserves complete records
|
||||
in stable unknown ID order. Filters apply before paging. Pages reflect current
|
||||
session state, so keep the unknown ledger unchanged while traversing it.
|
||||
|
||||
`list_unknowns` only lists registered heads. Import restores the Unknown records
|
||||
present in a bundle; it does not turn residual question text embedded in Evidence
|
||||
into registry entries. An empty registry therefore does not establish that
|
||||
retained Evidence has no open questions. Inspect the relevant Evidence or its
|
||||
analysis view for its residual questions.
|
||||
|
||||
Automatically retained workflow questions include their immutable result
|
||||
Evidence ID. Distinct results retain independent questions and source links;
|
||||
repeating the same result does not create another unknown. Workflow Evidence
|
||||
@@ -380,6 +409,19 @@ Duplicate creation is a nonretryable `invalid_request`; use the existing head
|
||||
or update it with its current `expected_revision`. A stale update remains a
|
||||
retryable `revision_conflict`.
|
||||
|
||||
If automatic question publication fails after a workflow completes,
|
||||
`details.workflow_unknown_publication` identifies the Evidence and states
|
||||
whether its retention was confirmed. `created_questions` and
|
||||
`existing_questions` count processed question occurrences; duplicate questions
|
||||
may share an Unknown identity. `total_questions` counts string questions, and
|
||||
`next_question_index` is the zero-based position in the workflow's original
|
||||
`residual_unknowns` array where publication stopped. It is not a resume token.
|
||||
For a retained record on the same MCP connection, retrieve the Evidence by ID
|
||||
with `get_evidence_bundle` and inspect committed heads with `list_unknowns`.
|
||||
Unprocessed questions remain in the completed Evidence; they were not checked
|
||||
by this attempt and may already exist from earlier work. A one-shot invocation identifies session-only retention
|
||||
as `invocation-only` and carries the completed Evidence in its failure when available.
|
||||
|
||||
When `record_unknown` or `update_unknown` commits but its complete reply exceeds
|
||||
the response budget, `details.reported_limits.committed_unknown` identifies the
|
||||
exact unknown ID, committed revision and mutation Evidence IDs. Its retained
|
||||
@@ -390,9 +432,26 @@ that revision. If a list response overflows, retry on the same connection with
|
||||
the same filters and offset and a smaller limit; export if one head still cannot
|
||||
fit.
|
||||
|
||||
To find missing sources before a downstream analysis refuses them, read
|
||||
`missing_direct_links` from the summary, or call `get_evidence_bundle` with
|
||||
`detail: "links"`. The result (`kind: "evidence-link-inspection"`) transfers no
|
||||
record payload. `filters` select the starting records, while membership is
|
||||
checked against every record in the session or file, so a filtered-out source is
|
||||
`retained`, not missing. Rows (`evidence_id`, `linked_evidence_id`, `status`) are
|
||||
in canonical order, default to `link_status: "missing"` and page with `offset` and
|
||||
`limit` (default 64); `links` counts every inspected link regardless of that
|
||||
selection. `traversal: "direct"` (the default) reads the selected records' own
|
||||
links and sets `retained_closure` to null, because it makes no closure claim.
|
||||
`traversal: "retained-closure"` also follows retained links, as composed workflows
|
||||
such as `build_call_path` do, and reports `complete` or `incomplete`; anything
|
||||
beyond a missing link remains unknown. Missing means absent from this session or
|
||||
file, not nonexistent, and neither status describes analysis coverage. Importing
|
||||
a missing source changes this membership without changing any Evidence ID.
|
||||
|
||||
For a portable exported bundle, the CLI shares this projection:
|
||||
`rea inspect-evidence-bundle '{"path":"/tmp/evidence.json","detail":"summary","filters":{"operation":"analyze_function"}}' --format json`.
|
||||
`"detail":"records"` with an `evidence_id` filter returns that complete record.
|
||||
`"detail":"records"` with an `evidence_id` filter returns that complete record,
|
||||
and `"detail":"links"` reads link membership.
|
||||
The CLI validates and materializes that local bundle under its existing memory
|
||||
budget; summary delivery does not make arbitrary large files cheap to import.
|
||||
|
||||
@@ -477,10 +536,12 @@ result's `_meta`, whether the call succeeded or failed:
|
||||
```
|
||||
|
||||
The marker is declared once per tool contract (`returnsTargetContent`) and is
|
||||
on by default: `export_evidence_bundle`, `record_unknown`, `search_guidance`,
|
||||
and `read_guidance_section` omit it. The first two contain only REA session
|
||||
state or caller-supplied values; the guidance tools return REA's own installed
|
||||
documentation, not text from an analysis target. Tools whose results or errors
|
||||
on by default: `inspect_analysis_settings`, `update_analysis_settings`,
|
||||
`export_evidence_bundle`, `record_unknown`, `guided_workflows`,
|
||||
`search_guidance`, and `read_guidance_section` omit it. The first four contain
|
||||
only REA session state or caller-supplied values; the workflow and guidance
|
||||
tools return REA's own installed workflows and documentation, not text from an
|
||||
analysis target. Tools whose results or errors
|
||||
can include provider, launcher, or device output stay marked, including
|
||||
`binary_session` (provider failure
|
||||
reasons and availability diagnostics), `close_binary` (provider cleanup
|
||||
@@ -633,12 +694,19 @@ refuses the summary before analysis. Further views use the returned
|
||||
`parent_evidence_id` through `inspect_analysis_view`, or a saved Evidence file or
|
||||
portable bundle, and never repeat decompilation.
|
||||
|
||||
A null provider observation produces `status: "unavailable"` and
|
||||
`reason: "no_pseudocode"`; item limitations retain any provider explanation.
|
||||
This does not establish why pseudocode is absent or whether a retry would help.
|
||||
Operation failures and malformed non-null output remain `status: "error"`.
|
||||
The `failed` count includes both unavailable and error items. Existing retained
|
||||
records keep their original item statuses and error details.
|
||||
|
||||
A `batch-decompile` view selects one `collection` and repeats the batch counts
|
||||
and provider identities:
|
||||
|
||||
- `items`: rows in retained order with their original `index`, so duplicate
|
||||
selectors stay distinct. A row keeps the selector in `address`, its `procedure`
|
||||
identity and `status`, plus `pseudocode_code_units` or its exact `error` and
|
||||
identity and `status`, plus `pseudocode_code_units`, an unavailable `reason`, or its exact `error` and
|
||||
`limitations`. `where.indices`, `where.addresses`, `where.procedure_addresses`
|
||||
and `where.statuses` filter exactly and combine; an item with unknown identity
|
||||
never matches `procedure_addresses`. Omitted filters select all, and empty
|
||||
@@ -647,11 +715,11 @@ and provider identities:
|
||||
- `pseudocode`: one `index`'s text in UTF-16 code-unit pages. `offset` and `limit`
|
||||
default to 0 and 16,384, and `coverage` measures that body. An offset inside a
|
||||
surrogate pair is refused, and a limit ending inside one stops before it. An
|
||||
error item has no text and is refused with a pointer to `items`.
|
||||
unavailable or error item has no text and is refused with a pointer to `items`.
|
||||
|
||||
Page `coverage` counts the selection only: exhausting it establishes nothing
|
||||
about functions outside the batch. Evidence `limitations` aggregate every item's
|
||||
provider observations and are not attributed to one successful item; only error
|
||||
provider observations and are not attributed to one successful item; unavailable and error
|
||||
items keep their own. A page over the response budget offers a smaller selection
|
||||
from the same `source`: fewer rows, or one body's `pseudocode` pages when a single
|
||||
row does not fit. An inline source offers none, because the record cannot be
|
||||
@@ -921,6 +989,15 @@ concrete version, selection source, and complete analysis profile through
|
||||
choices return typed selection details. A selected provider is never replaced
|
||||
automatically after a runtime failure.
|
||||
|
||||
When an admitted `open_binary` attempt fails, `error.details.selection_at_failure`
|
||||
records the canonical binary path and SHA-256, resource root (including its
|
||||
caller-selected path), or absence of selection at
|
||||
the end of that serialized attempt. This is a past observation: a queued or
|
||||
later operation may already have changed the selection. No selection does not
|
||||
establish that cleanup is complete. Call `binary_session` for current state
|
||||
and cleanup status. Validation failures that do not enter the session have no
|
||||
such receipt. The original error and any cleanup recovery remain authoritative.
|
||||
|
||||
`open_binary` resolves an Apple bundle directory (`.app`, `.appex`,
|
||||
`.framework`, `.xpc`, `.systemextension`, `.bundle` or `.plugin`, compared
|
||||
case-insensitively) to the program file its `Info.plist` names in
|
||||
@@ -1539,6 +1616,49 @@ support for omitted historical values.
|
||||
known and differ. If only one value is known, storage comparison is unknown;
|
||||
when both are omitted, storage is not compared.
|
||||
|
||||
<!-- BEGIN:compare-artifacts:compared-format-metadata-kinds -->
|
||||
|
||||
`compare_artifacts` also compares the declarations in the `format_metadata`
|
||||
of UnityFS members (`unityfs-entry`), PyInstaller entries
|
||||
(`pyinstaller-entry`), Node SEA members (`node-sea-entry`),
|
||||
ESP image segments (`esp-image-segment`), DfuSe elements (`dfuse-element`),
|
||||
ESP flash partitions (`esp-flash-partition`) and FIT images (`fit-image`);
|
||||
each format's guide names the compared fields. A changed declaration is a
|
||||
metadata change, including when content is unavailable. A declaration that
|
||||
only one side records, or that the other side records as another kind, is
|
||||
unknown and adds a limitation. A field that either side could not read is
|
||||
unknown, and the other fields still compare. Other kinds are not compared:
|
||||
their fields record layout, come from the bytes of the artifact or its
|
||||
container or from caller options that identify the target, or repeat
|
||||
occurrence fields, except Godot PCK source paths, which
|
||||
[Godot PCK archives](godot-pck.md) explains.
|
||||
|
||||
<!-- END:compare-artifacts:compared-format-metadata-kinds -->
|
||||
|
||||
Every occurrence records `entry_kind`, `executable` and `encrypted`. A
|
||||
different entry kind, `encrypted` value or occurrence limitation text is a
|
||||
`metadata` difference, and a different `executable` value is an `executable`
|
||||
difference. A symlink's own permission bits never mark it `executable`,
|
||||
because running a symlink runs its target. When either side's content is
|
||||
unavailable, `compare_artifacts` lists the differences that it can still
|
||||
observe with `availability`, and the path compares `unknown` unless a
|
||||
declaration changed. REA does not read symlink targets or encrypted bytes,
|
||||
so a non-executable file that becomes a symlink compares `unknown` in
|
||||
`metadata` and `availability`, and so does a member that becomes encrypted.
|
||||
Declared sizes do not count without content: REA compares observed sizes.
|
||||
For a node-less metadata record with a compared declaration, only that
|
||||
declaration, these fields and any recorded ASAR storage decide the
|
||||
comparison, so a record that becomes encrypted compares `changed` in
|
||||
`metadata`. Such a record adds `availability` only when a declaration is
|
||||
unknown. REA does not compare the record's unread payload or its
|
||||
limitations, which describe the observation.
|
||||
|
||||
When either side has no content, `compare_artifacts` also compares the
|
||||
occurrences' `declared_integrity` claims, which Godot PCK and ASAR readers
|
||||
record. Claims of one algorithm that differ add `metadata`. REA cannot verify
|
||||
them, so they do not make the path `changed`, and equal claims add nothing.
|
||||
With content on both sides, the observed bytes decide.
|
||||
|
||||
POSIX letter-colon names such as `a:b` remain valid logical
|
||||
names; Windows extraction refuses names its filesystem cannot materialize.
|
||||
|
||||
@@ -1682,7 +1802,9 @@ 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.
|
||||
description because it states the accepted input groups. A root `detail`
|
||||
property keeps its description because it selects between complete Evidence
|
||||
and a bounded summary.
|
||||
- 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
|
||||
|
||||
@@ -46,6 +46,18 @@ fingerprint and the same known architecture. Extents are never guessed from the
|
||||
next entry, and relocations are not normalized. A larger mapped read can serve
|
||||
several observed bodies; only needed ranges need to be captured.
|
||||
|
||||
For a matched function, its `evidence_links` include the selected byte reads
|
||||
that covered its body. Request the `function-matches` view with
|
||||
`collection: "byte-sources"` to page the selected `read_bytes` source IDs,
|
||||
sides and limitations; intersect those IDs with the function's links. This
|
||||
distinguishes, for example, Ghidra initialized-memory bytes from file bytes.
|
||||
Identical overlapping reads retain the selected covering source, not every
|
||||
corroborating read. Pages preserve the original source limitations and may end
|
||||
early when the response budget is reached. If one source is too large for a
|
||||
page, the error identifies its Evidence and the next offset; inspect the
|
||||
original matching Evidence or export it if retained, then resume at that
|
||||
offset. Older matching Evidence may not record this source selection.
|
||||
|
||||
This first slice reuses existing range observations. Stripped builds can require
|
||||
many lightweight range queries when those facts are not retained yet. Matching
|
||||
does not hide that acquisition cost or silently start those queries. Full
|
||||
|
||||
@@ -128,6 +128,16 @@ rea inspect-native-data-type <binary> --type /MyStruct --provider ghidra
|
||||
rea inspect-native-data-type <binary> --address <typed-data-address> --provider ghidra
|
||||
```
|
||||
|
||||
Ghidra bit-field components may have a `type_id` that is not a registered
|
||||
database type. When `bit_field` is present, `base_type_id` is the provider's
|
||||
base type and the lookup fields report whether the manager resolves equivalent
|
||||
types at those exact paths. `bit_size` is the effective width;
|
||||
`declared_bit_size` can be larger. `bit_offset` is the least-significant-bit
|
||||
offset within the base type, not a byte-order-independent offset from the start
|
||||
of the containing structure. Account for the component's storage size and byte
|
||||
order before applying that offset to a larger word. Missing `bit_field` details
|
||||
are unreported, not proof that a component is not a bit-field.
|
||||
|
||||
Instruction facts include bytes, length, decoder text, ordered register/scalar/
|
||||
address tokens, typed references and flow destinations. Mid-instruction, data,
|
||||
outside-memory and unlisted addresses have separate outcomes. For Ghidra,
|
||||
@@ -227,7 +237,17 @@ caveats. Failure details retain at most 32 rows within 2,048 JSON-encoded
|
||||
UTF-8 bytes; names may be omitted to keep complete addresses and reasons. The
|
||||
index retains every failure and its complete observed name.
|
||||
Each function's `decompile-index.json` record, whatever its status, keeps the
|
||||
complete limitations of the observations it used. The returned Evidence keeps
|
||||
complete limitations of the observations it used.
|
||||
When available, `provider_source` records the source of that attempt's
|
||||
pseudocode observation: a file path, SHA-256 and byte range, or a
|
||||
single-function decompile. The latter describes the method, not a unique run. A file
|
||||
descriptor records the source as observed; the file may since have changed or
|
||||
been removed, so compare its current digest before using it. The descriptor
|
||||
does not authenticate the journal. `status: "unknown"` distinguishes
|
||||
an unavailable source from an existing matching output whose original producer
|
||||
is unknown. Older journal records can omit this field; omission is unknown.
|
||||
Failed attempts retain their source in the append-only journal when the
|
||||
provider supplied one, even if a later attempt succeeds. The returned Evidence keeps
|
||||
at most eight first-observed distinct per-function provider limitations within an 8,000-byte JSON-encoded UTF-8 text budget and counts omitted observations. Repeated omitted text is counted per observation, not as a distinct caveat. This summary does not retain every omitted string in memory. A fresh output directory must not already exist. `--resume`
|
||||
continues an export for the same target SHA-256, provider, analysis profile,
|
||||
and layout; it requires the directory to exist, and an export without a known
|
||||
@@ -300,6 +320,20 @@ relationships but reports their completeness as unknown. In particular, a resolv
|
||||
tail call observed in one direction does not prove that the reverse inventory is
|
||||
complete. An empty Hopper inventory cannot establish that no such call exists.
|
||||
No whole-binary reverse scan is performed.
|
||||
|
||||
For large native traces, `trace_feature` accepts `detail: "summary"`. It retains
|
||||
the complete trace and returns counts and its parent Evidence ID. Use
|
||||
`inspect_analysis_view` with that ID and `view: {kind: "native-feature-trace",
|
||||
collection: "references", offset: 0, limit: 64}` to page the original rows;
|
||||
`matches` and `unknowns` are separate collections. Matches accept an exact
|
||||
`address`; references accept exact `source_address` and `target_address`
|
||||
filters. Row indices refer to the retained collection, and page coverage does not establish provider discovery
|
||||
completeness. `list_unknowns` pages the independently managed question heads.
|
||||
In the one-shot CLI, `rea trace ... --detail summary --evidence_output PATH`
|
||||
stores the trace and its linked source Evidence for later
|
||||
`inspect-analysis-view` calls. This bundle does not include the CLI session's
|
||||
Unknown registry; its original question texts remain available in the trace's
|
||||
`unknowns` pages.
|
||||
Regex searches use ECMAScript Unicode syntax in a cancellable worker with a
|
||||
matching deadline and a worker old-generation heap. Deadline or cancellation
|
||||
stops matching while leaving the Hopper API available. The deadline is
|
||||
@@ -388,6 +422,11 @@ sites remain distinct. A static route does not establish runtime reachability.
|
||||
Swift/closure dispatch requires resolved static references; unavailable ABI
|
||||
forms remain unknown.
|
||||
|
||||
Encoded Objective-C method records retain their own locations in dispatch
|
||||
Evidence. A handler node uses the implementation address with an unknown file
|
||||
offset unless that function already has an independently located graph node;
|
||||
the method-record offset does not locate the handler's code.
|
||||
|
||||
Incomplete reference kinds do not discard known typed calls or partial
|
||||
provider call references. A `provider_call` with `observed` resolution retains
|
||||
the provider-reported call site and endpoint without claiming generic direct,
|
||||
@@ -429,6 +468,18 @@ nodes and edges. Node payloads are bounded to 8 MiB and serialized results to
|
||||
32 MiB. Pagination returns edges whose source is on the node page and preserves
|
||||
stable IDs for endpoints outside that page.
|
||||
|
||||
Procedure, node and edge `evidence_id` values identify the source function
|
||||
observations used by the trace. In an MCP session, retrieve them from the
|
||||
Evidence bundle for inspection or export. A replayed source keeps the original
|
||||
observation's identity; the trace also reports that replay occurred. Older
|
||||
cached value traces are not replayed under the revised workflow profile, while
|
||||
their historical Evidence remains available.
|
||||
|
||||
The one-shot CLI accepts `--evidence_output <new-path>` to save the trace and
|
||||
the transitive source Evidence closure named by its links. The destination is
|
||||
never overwritten. `--detail summary` requires `--evidence_output`; the bundle
|
||||
still retains the complete trace and source Evidence.
|
||||
|
||||
Within one value trace, each exact procedure selector is attempted at most once,
|
||||
including failed analyses. A failed seed returns the provider's original typed
|
||||
failure and corrective guidance; it cannot establish a trace or target identity.
|
||||
@@ -439,8 +490,10 @@ resources it owns stops the trace: the result is that provider's own
|
||||
`cleanup_incomplete` failure with its resources and any bound cleanup retry, and the work
|
||||
admitted before the stop is retained as partial Evidence with a located unknown
|
||||
at the stopping read. A successful seed without a native def-use
|
||||
model still returns its procedure and an explicit coverage unknown. A new trace
|
||||
can retry failed selectors.
|
||||
model still returns its procedure and an explicit coverage unknown. An unrecorded
|
||||
facet stays unknown; when the provider explicitly reports unavailability, the trace
|
||||
retains its reason. REA's Kuna provider explicitly reports that it does not supply
|
||||
native def-use observations. A new trace can retry failed selectors.
|
||||
|
||||
Paging and traversal completeness are separate facts. `page_truncated` means the
|
||||
page omits retained nodes, and the edges they source, before `offset` or after
|
||||
@@ -1004,7 +1057,9 @@ with a path-specific gap and incomplete capture coverage. Null alone does not
|
||||
establish that an accessibility value was successfully read or absent.
|
||||
|
||||
An initial PID or window mismatch is `invalid_request` on `pid` or
|
||||
`window_id` and leaves provider availability intact. An unavailable executable
|
||||
`window_id` and leaves provider availability intact. A selected window that
|
||||
exists off screen is not a mismatch: selection succeeds and the permission and
|
||||
capture checks report their own outcome. An unavailable executable
|
||||
URL or unresolved executable path leaves identity unknown and reports
|
||||
`capability_unavailable`. Identity or window loss
|
||||
after the initial capture remains an observation failure with collected facts.
|
||||
@@ -1392,7 +1447,7 @@ procedures. The CLI equivalent is
|
||||
Use `start`/`end` to constrain the inclusive address window, and continue with
|
||||
`next_cursor` until `complete` is true. Inspect `searched` for page coverage and
|
||||
skipped uninitialized blocks. A missing match does not establish absence from
|
||||
uninitialized memory or across block boundaries. See
|
||||
uninitialized or excluded memory. See
|
||||
[Ghidra byte search](ghidra-provider.md#masked-byte-search) for mask and coverage
|
||||
semantics. This query never executes or modifies target bytes.
|
||||
|
||||
|
||||
+6
-2
@@ -82,8 +82,12 @@ override. Asset and argument decoding, padding scans, and inventory construction
|
||||
yield in bounded batches and honor caller cancellation. Asset count and name
|
||||
budgets bound retained metadata independently of stored member body sizes. The PE
|
||||
name probe reads headers from the first 64 KiB of the carrier; a PE whose
|
||||
headers extend beyond that is left to native classification. Snapshot and
|
||||
code-cache bytes remain opaque.
|
||||
headers extend beyond that is left to native classification. ELF note tables
|
||||
are read for at most one note per 12 carrier bytes in total, the most that
|
||||
disjoint tables can hold, so program headers that repeat the same notes do not
|
||||
rescan them once per header. Past that count the remaining tables are
|
||||
unreadable: an ELF without a confirmed blob keeps its native classification,
|
||||
and one with a blob fails closed. Snapshot and code-cache bytes remain opaque.
|
||||
|
||||
## Verification
|
||||
|
||||
|
||||
@@ -358,9 +358,13 @@ sibling filename such as `tool-helper`; selected parent directories still
|
||||
normalize within descendant paths, including when the working directory or an
|
||||
observation path is spelled with a trailing `/` or `/.`. A selected filesystem
|
||||
root `/` also aliases descendant paths, without rewriting URL separators or
|
||||
HTML closing tags. The comparison contract
|
||||
records these rules as `path-component-boundaries-v3`, so captures made with the
|
||||
earlier rules cannot be compared as if the text had the same meaning.
|
||||
HTML closing tags. When no temporary root applies to an observation, no temporary-root
|
||||
path is selected: literal `<no-temporary-root>` input and sampled command text
|
||||
remain unchanged. The comparison contract
|
||||
records these rules as `path-component-boundaries-v4`, so captures made with the
|
||||
earlier rules cannot be compared as if the text had the same meaning. Historical
|
||||
captures remain readable; recapture both sides with the same path normalization
|
||||
version before comparing them.
|
||||
|
||||
Rendered frame `lines` hold the visible rows after normalization, with trailing
|
||||
U+0020 spaces removed. Blank rows and internal spaces remain. `columns` records
|
||||
|
||||
@@ -77,14 +77,17 @@ table index; original names remain available in the supplied artifact.
|
||||
Path failures use bounded escaped previews and report the original length;
|
||||
accepted paths and retained source names are not shortened.
|
||||
|
||||
`compare_artifacts` compares these node-less records as declarations using the
|
||||
container, container name, source name and type code. Equal option or dependency
|
||||
declarations can therefore compare unchanged without implying that an external
|
||||
dependency's bytes were found. For materialized PyInstaller members, the same
|
||||
fields participate in the metadata delta independently of content identity;
|
||||
archive offsets and table positions remain location facts, not declaration
|
||||
identity. Missing declaration facts and unavailable non-metadata content remain
|
||||
unknown.
|
||||
`compare_artifacts` compares node-less records, namely runtime options,
|
||||
dependencies and namespace packages, as declarations using the container,
|
||||
container name, source name and type code, and whether the record is
|
||||
encrypted; their limitations are not compared. Equal declarations can
|
||||
therefore compare unchanged without implying that an external dependency's
|
||||
bytes were found or that unread legacy namespace payloads match. For
|
||||
materialized PyInstaller members, the same fields participate in the metadata
|
||||
delta independently of content identity, including when the member's bytes
|
||||
are unavailable; archive offsets and table positions remain location facts,
|
||||
not declaration identity. Missing declaration facts remain unknown, as does
|
||||
unavailable non-metadata content whose declaration is unchanged.
|
||||
|
||||
Request `detail: "summary"` for a compact inspection, then query retained
|
||||
Evidence with `inspect_analysis_view`. An inventory view can select
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[الموقع](https://rea.tools/ar/) · [الأدلة](https://rea.tools/ar/guides/) · [دراسات الحالة](https://rea.tools/ar/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="انضم إلى مجتمع REA على Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[البدء السريع](#البدء-السريع) · [كيف يعمل REA](#كيف-يعمل-rea) · [ما الذي يمكنك تحليله](#ما-الذي-يمكنك-تحليله) · [دراسات الحالة](#دراسات-الحالة) · [إنشاء بيئات التعلم المعزز](#build-rl-environments) · [الأسئلة الشائعة](#الأسئلة-الشائعة) · [أدوات GNU objdump وRizin](../reverse-engineering-tools.md) · [التوثيق](#التوثيق)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[Website](https://rea.tools/de/) · [Anleitungen](https://rea.tools/de/guides/) · [Fallstudien](https://rea.tools/de/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Der REA-Community auf Discord beitreten" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[Schnellstart](#schnellstart) · [So funktioniert REA](#so-funktioniert-rea) · [Was du analysieren kannst](#was-du-analysieren-kannst) · [Fallstudien](#fallstudien) · [RL-Umgebungen erstellen](#build-rl-environments) · [Häufige Fragen](#häufige-fragen) · [Dokumentation](#dokumentation)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
**[Ιστότοπος](https://rea.tools/) · [Οδηγοί](https://rea.tools/guides/) · [Μελέτες περίπτωσης](https://rea.tools/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Έλα στην κοινότητα REA στο Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[Γρήγορη εκκίνηση](#γρήγορη-εκκίνηση) · [Πώς λειτουργεί το REA](#πώς-λειτουργεί-το-rea) · [Τι μπορείς να αναλύσεις](#τι-μπορείς-να-αναλύσεις) · [Μελέτες περίπτωσης](#μελέτες-περίπτωσης) · [Δημιουργία περιβαλλόντων RL](#build-rl-environments) · [Συχνές ερωτήσεις](#συχνές-ερωτήσεις) · [Τεκμηρίωση](#τεκμηρίωση)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[Sitio web](https://rea.tools/es/) · [Guías](https://rea.tools/es/guides/) · [Casos prácticos](https://rea.tools/es/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Únete a la comunidad de REA en Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[Inicio rápido](#inicio-rápido) · [Cómo funciona REA](#cómo-funciona-rea) · [Qué puedes analizar](#qué-puedes-analizar) · [Casos prácticos](#casos-prácticos) · [Crear entornos de RL](#build-rl-environments) · [Preguntas frecuentes](#preguntas-frecuentes) · [Documentación](#documentación)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[وبسایت](https://rea.tools/fa/) · [راهنماها](https://rea.tools/fa/guides/) · [نمونههای عملی](https://rea.tools/fa/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="به انجمن REA در دیسکورد بپیوندید" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[شروع سریع](#شروع-سریع) · [REA چگونه کار میکند؟](#rea-چگونه-کار-میکند) · [چه چیزهایی را میتوان تحلیل کرد؟](#چه-چیزهایی-را-میتوان-تحلیل-کرد) · [نمونههای عملی](#نمونههای-عملی) · [ساخت محیطهای RL](#build-rl-environments) · [پرسشهای متداول](#پرسشهای-متداول) · [مستندات](#مستندات)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[Site web](https://rea.tools/fr/) · [Guides](https://rea.tools/fr/guides/) · [Études de cas](https://rea.tools/fr/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Rejoignez la communauté REA sur Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[Démarrage rapide](#démarrage-rapide) · [Comment fonctionne REA](#comment-fonctionne-rea) · [Ce que vous pouvez analyser](#ce-que-vous-pouvez-analyser) · [Études de cas](#études-de-cas) · [Créer des environnements RL](#build-rl-environments) · [Questions fréquentes](#questions-fréquentes) · [Documentation](#documentation)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[Situs Web](https://rea.tools/id/) · [Panduan](https://rea.tools/id/guides/) · [Contoh Penggunaan](https://rea.tools/id/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Bergabunglah dengan komunitas REA di Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[Mulai Cepat](#mulai-cepat) · [Cara Kerja REA](#cara-kerja-rea) · [Hal yang Dapat Anda Analisis](#hal-yang-dapat-anda-analisis) · [Contoh Penggunaan](#contoh-penggunaan) · [Membangun lingkungan RL](#build-rl-environments) · [FAQ](#faq) · [Dokumentasi](#dokumentasi)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[Sito web](https://rea.tools/it/) · [Guide](https://rea.tools/it/guides/) · [Showcase](https://rea.tools/it/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Unisciti alla community REA su Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[Avvio rapido](#avvio-rapido) · [Come funziona REA](#come-funziona-rea) · [Cosa puoi analizzare](#cosa-puoi-analizzare) · [Showcase](#showcase) · [Creare ambienti RL](#build-rl-environments) · [FAQ](#faq) · [Documentazione](#documentazione)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[ウェブサイト](https://rea.tools/ja/) · [ガイド](https://rea.tools/ja/guides/) · [事例](https://rea.tools/ja/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="DiscordのREAコミュニティに参加" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[クイックスタート](#クイックスタート) · [REA の仕組み](#rea-の仕組み) · [分析できる対象](#分析できる対象) · [事例](#事例) · [RL 環境を構築する](#build-rl-environments) · [よくある質問](#よくある質問) · [ドキュメント](#ドキュメント)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[웹사이트](https://rea.tools/ko/) · [가이드](https://rea.tools/ko/guides/) · [사례](https://rea.tools/ko/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Discord에서 REA 커뮤니티에 참여하세요" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[빠른 시작](#빠른-시작) · [REA의 작동 방식](#rea의-작동-방식) · [분석할 수 있는 대상](#분석할-수-있는-대상) · [사례](#사례) · [RL 환경 만들기](#build-rl-environments) · [자주 묻는 질문](#자주-묻는-질문) · [문서](#문서)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[വെബ്സൈറ്റ്](https://rea.tools/) · [ഗൈഡുകൾ](https://rea.tools/guides/) · [ഉദാഹരണങ്ങൾ](https://rea.tools/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Discord-ൽ REA കമ്മ്യൂണിറ്റിയിൽ ചേരുക" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[ദ്രുതാരംഭം](#ദ്രുതാരംഭം) · [REA എങ്ങനെ പ്രവർത്തിക്കുന്നു](#rea-എങ്ങനെ-പ്രവർത്തിക്കുന്നു) · [എന്തൊക്കെ വിശകലനം ചെയ്യാം](#എന്തൊക്കെ-വിശകലനം-ചെയ്യാം) · [ഉദാഹരണങ്ങൾ](#ഉദാഹരണങ്ങൾ) · [RL പരിതസ്ഥിതികൾ നിർമ്മിക്കുക](#build-rl-environments) · [പതിവുചോദ്യങ്ങൾ](#പതിവുചോദ്യങ്ങൾ) · [ഡോക്യുമെന്റേഷൻ](#ഡോക്യുമെന്റേഷൻ)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[Strona internetowa](https://rea.tools/pl/) · [Przewodniki](https://rea.tools/pl/guides/) · [Przykłady zastosowań](https://rea.tools/pl/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Dołącz do społeczności REA na Discordzie" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[Szybki start](#szybki-start) · [Jak działa REA](#jak-działa-rea) · [Co możesz analizować](#co-możesz-analizować) · [Przykłady zastosowań](#przykłady-zastosowań) · [Tworzenie środowisk RL](#build-rl-environments) · [FAQ](#faq) · [Dokumentacja](#dokumentacja)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[Site](https://rea.tools/pt-BR/) · [Guias](https://rea.tools/pt-BR/guides/) · [Casos de uso](https://rea.tools/pt-BR/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Participe da comunidade REA no Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[Início rápido](#início-rápido) · [Como o REA funciona](#como-o-rea-funciona) · [O que você pode analisar](#o-que-você-pode-analisar) · [Casos de uso](#casos-de-uso) · [Criar ambientes de RL](#build-rl-environments) · [Perguntas frequentes](#perguntas-frequentes) · [Documentação](#documentação)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[Site web](https://rea.tools/) · [Ghiduri](https://rea.tools/guides/) · [Studii de caz](https://rea.tools/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Alătură-te comunității REA pe Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[Pornire rapidă](#pornire-rapidă) · [Cum funcționează REA](#cum-funcționează-rea) · [Ce poți analiza](#ce-poți-analiza) · [Studii de caz](#studii-de-caz) · [Construiește medii RL](#build-rl-environments) · [Întrebări frecvente](#întrebări-frecvente) · [Documentație](#documentație)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[Сайт](https://rea.tools/ru/) · [Руководства](https://rea.tools/ru/guides/) · [Примеры](https://rea.tools/ru/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Присоединяйтесь к сообществу REA в Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[Быстрый старт](#быстрый-старт) · [Как работает REA](#как-работает-rea) · [Что можно анализировать](#что-можно-анализировать) · [Примеры](#примеры) · [Создание RL-окружений](#build-rl-environments) · [Частые вопросы](#частые-вопросы) · [Документация](#документация)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[เว็บไซต์](https://rea.tools/th/) · [คู่มือ](https://rea.tools/th/guides/) · [ตัวอย่างการใช้งาน](https://rea.tools/th/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="เข้าร่วมชุมชน REA บน Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[เริ่มต้นอย่างรวดเร็ว](#เริ่มต้นอย่างรวดเร็ว) · [REA ทำงานอย่างไร](#rea-ทำงานอย่างไร) · [สิ่งที่วิเคราะห์ได้](#สิ่งที่วิเคราะห์ได้) · [ตัวอย่างการใช้งาน](#ตัวอย่างการใช้งาน) · [สร้างสภาพแวดล้อม RL](#build-rl-environments) · [คำถามที่พบบ่อย](#คำถามที่พบบ่อย) · [เอกสาร](#เอกสาร)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[Web sitesi](https://rea.tools/tr/) · [Rehberler](https://rea.tools/tr/guides/) · [Örnek çalışmalar](https://rea.tools/tr/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Discord'da REA topluluğuna katılın" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[Hızlı başlangıç](#hızlı-başlangıç) · [REA nasıl çalışır](#rea-nasıl-çalışır) · [Neleri analiz edebilirsiniz](#neleri-analiz-edebilirsiniz) · [Örnek çalışmalar](#örnek-çalışmalar) · [RL ortamları oluşturun](#build-rl-environments) · [Sık sorulan sorular](#sık-sorulan-sorular) · [GNU objdump ve Rizin araçları](../reverse-engineering-tools.md) · [Belgeler](#belgeler)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[Сайт](https://rea.tools/uk/) · [Посібники](https://rea.tools/uk/guides/) · [Приклади](https://rea.tools/uk/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Приєднуйтеся до спільноти REA в Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[Швидкий старт](#швидкий-старт) · [Як працює REA](#як-працює-rea) · [Що можна аналізувати](#що-можна-аналізувати) · [Приклади](#приклади) · [Створення RL-середовищ](#build-rl-environments) · [Поширені запитання](#поширені-запитання) · [Документація](#документація)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[Website](https://rea.tools/) · [Guides](https://rea.tools/guides/) · [Showcases](https://rea.tools/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Discord par REA community mein shamil hon" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[Jaldi shuru karein](#jaldi-shuru-karein) · [REA kaise kaam karta hai](#rea-kaise-kaam-karta-hai) · [Kya kuch analyze kar sakte hain](#kya-kuch-analyze-kar-sakte-hain) · [Amli misaalein](#amli-misaalein) · [RL environments banayein](#build-rl-environments) · [FAQ](#faq) · [Documentation](#documentation)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[Trang web](https://rea.tools/vi/) · [Hướng dẫn](https://rea.tools/vi/guides/) · [Các nghiên cứu điển hình](https://rea.tools/vi/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Tham gia cộng đồng REA trên Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[Bắt đầu nhanh](#bắt-đầu-nhanh) · [Cách REA hoạt động](#cách-rea-hoạt-động) · [Những gì bạn có thể phân tích](#những-gì-bạn-có-thể-phân-tích) · [Các nghiên cứu điển hình](#các-nghiên-cứu-điển-hình) · [Xây dựng môi trường RL](#build-rl-environments) · [Câu hỏi thường gặp](#câu-hỏi-thường-gặp) · [Tài liệu](#tài-liệu)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[網站](https://rea.tools/zh-TW/) · [指南](https://rea.tools/zh-TW/guides/) · [案例](https://rea.tools/zh-TW/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="加入 REA 的 Discord 社群" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[快速開始](#快速開始) · [REA 的工作原理](#rea-的工作原理) · [可以分析什麼](#可以分析什麼) · [案例](#案例) · [建立 RL 環境](#build-rl-environments) · [常見問題](#常見問題) · [文件](#文件)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
**[网站](https://rea.tools/zh-CN/) · [指南](https://rea.tools/zh-CN/guides/) · [案例](https://rea.tools/zh-CN/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="加入 REA 的 Discord 社区" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[快速开始](#快速开始) · [REA 的工作原理](#rea-的工作原理) · [可以分析什么](#可以分析什么) · [案例](#案例) · [构建 RL 环境](#build-rl-environments) · [常见问题](#常见问题) · [GNU objdump 和 Rizin 工具](../reverse-engineering-tools.md) · [文档](#文档)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -14,7 +14,13 @@ can run.
|
||||
|
||||
## GNU objdump
|
||||
|
||||
`inspect_with_objdump` and `rea inspect-with-objdump` capture stdout and stderr as inline Evidence. Artifact inspection Evidence records SHA-256 digests before and after the provider call and whether those observed digests differ; this cannot prove the exact bytes consumed if the artifact changes during execution. Choose one artifact operation: `file_headers`, `section_headers`, `symbols`, `relocations`, `dwarf`, `disassemble`, or `disassemble_all`. `disassemble` maps to `-d`; `disassemble_all` maps to `-D` and has different symbol-boundary semantics. The pathless `inspect_with_objdump {"operation":"architectures"}` MCP call or `rea list-objdump-architectures` CLI command maps to `-i` and produces subjectless provider-capability Evidence describing BFD targets compiled into the installed binary, not a guarantee that a target is supported.
|
||||
REA sends GNU-style arguments to the selected command but does not verify its
|
||||
implementation or version. Evidence identifies the command profile as
|
||||
`objdump.command` and retains the selected executable; it does not claim that
|
||||
the executable is GNU objdump. Other implementations may accept some commands
|
||||
while differing in supported options and output.
|
||||
|
||||
`inspect_with_objdump` and `rea inspect-with-objdump` capture stdout and stderr as inline Evidence. Artifact inspection Evidence records SHA-256 digests before and after the provider call and whether those observed digests differ; this cannot prove the exact bytes consumed if the artifact changes during execution. Choose one artifact operation: `file_headers`, `section_headers`, `symbols`, `relocations`, `dwarf`, `disassemble`, or `disassemble_all`. `disassemble` maps to `-d`; `disassemble_all` maps to `-D` and has different symbol-boundary semantics. The pathless `inspect_with_objdump {"operation":"architectures"}` MCP call or `rea list-objdump-architectures` CLI command maps to `-i` and produces subjectless Evidence containing the selected command’s output, not a guarantee that any target is supported. GNU objdump uses this option to list its compiled-in BFD targets.
|
||||
|
||||
Failure to launch the selected executable reports provider unavailability, the
|
||||
configured command and the host error under `boundary: "provider-launch"`.
|
||||
@@ -25,7 +31,7 @@ their rejected fields in input issues before artifact reads or provider launch.
|
||||
|
||||
For DWARF, REA does not follow local debug links by default. Set `follow_debug_links` to `true` to pass `-WK`; external debug-file identities are not bound to Evidence when following links is enabled. The CLI flag is `--follow-debug-links`; `--no-follow-debug-links` or `--follow-debug-links=false` keeps the default. A separate `true` or `false` word after the flag is not its value. REA clears `DEBUGINFOD_URLS` in the objdump child environment to disable debuginfod network access on GNU binutils versions that do not support `-WE`. The complete raw output is authoritative; REA does not normalize it into a version-independent disassembly schema.
|
||||
|
||||
For a known native symbol, use `operation: "disassemble", symbol: "my_function"` in MCP or `--symbol my_function` in CLI. REA passes `--disassemble=my_function` to objdump before collecting output, rather than producing the whole disassembly and filtering it afterward. Omit `symbol` to keep full code-section disassembly. Other operations reject this selector. Use a nonempty, NUL-free name with well-formed Unicode; unsupported values are rejected before reading the artifact or starting objdump. Symbol names are passed unchanged; matching, versioned symbols, aliases and the end of the disassembly follow the installed GNU objdump's semantics. Evidence records the selector and actual arguments. An empty match does not establish artifact-wide absence, and the selected observation does not contain the omitted disassembly; request the complete operation when needed.
|
||||
For a known native symbol, use `operation: "disassemble", symbol: "my_function"` in MCP or `--symbol my_function` in CLI. REA passes `--disassemble=my_function` to objdump before collecting output, rather than producing the whole disassembly and filtering it afterward. Omit `symbol` to keep full code-section disassembly. Other operations reject this selector. Use a nonempty, NUL-free name with well-formed Unicode; unsupported values are rejected before reading the artifact or starting objdump. Symbol names are passed unchanged; matching, versioned symbols, aliases and the end of the disassembly follow the selected executable's semantics. Evidence records the selector and actual arguments. An empty match does not establish artifact-wide absence, and the selected observation does not contain the omitted disassembly; request the complete operation when needed.
|
||||
|
||||
```sh
|
||||
rea inspect-with-objdump ./program disassemble
|
||||
@@ -40,7 +46,7 @@ Captured stdout and stderr are retained up to a 16 MiB combined limit; `output_t
|
||||
|
||||
## GDB
|
||||
|
||||
Persistent GDB sessions, including the MCP workflow and one-shot `rea debug-with-gdb` CLI, are available only on Linux. REA verifies a live inferior's output descriptors through `/proc` before detaching it; it does not claim equivalent descriptor verification on macOS. Windows is also unsupported because owned Job Object cleanup can terminate debugger inferiors. On supported Linux hosts, `start_gdb_session`, `gdb_console`, `gdb_session_status`, and `close_gdb_session` manage a persistent GDB/MI session in MCP. The one-shot CLI requires a caller-selected durable TTY: `rea debug-with-gdb --inferior-tty /dev/pts/7 'file ./program'`. The TTY must remain available after GDB exits so a detached inferior can continue writing. MCP starts GDB with `--nx --quiet --interpreter=mi3`, retries initialization with MI2 if MI3 initialization returns an error and cleanup succeeds, waits for its prompt, and disables auto-loading. Pass optional `inferior_tty` to select the terminal that future inferior runs use; REA configures it before the session becomes ready and checks the exact value with GDB/MI. `gdb_session_status` reports the selected path, whether REA still considers the configuration verified, and whether invalid UTF-8 has been observed in MI bytes (`mi_invalid_utf8`) or stderr (`stderr_invalid_utf8`). Commands that can change the inferior TTY or redirect a run revoke that verification permanently for the session, so close retains GDB ownership if a live inferior remains. The complete built-in inspections `show inferior-tty` and `help inferior-tty` preserve verification; compound commands still use the conservative mutation checks. Closing a session with one live inferior and a verified TTY automatically detaches the target, which can resume execution; MCP clients receive destructive-operation annotations for this behavior. The console remains unrestricted and can run shell commands, change target state, access files, and use network in the REA process user's security context. Raw MI records and console/target/log streams are returned with Evidence; command output is retained up to 16 MiB and explicitly marked when truncated. GDB startup waits 10000 ms and each MI command waits 120000 ms. This release has no runtime override for those deadlines, the 16 MiB command output budget, session capacity, or the pending-command queue. `command_timeout_ms` does not change them.
|
||||
Persistent GDB sessions, including the MCP workflow and one-shot `rea debug-with-gdb` CLI, are available only on Linux. REA verifies a live inferior's output descriptors through `/proc` before detaching it; it does not claim equivalent descriptor verification on macOS. Windows is also unsupported because owned Job Object cleanup can terminate debugger inferiors. On supported Linux hosts, `start_gdb_session`, `gdb_console`, `gdb_session_status`, and `close_gdb_session` manage a persistent GDB/MI session in MCP. The one-shot CLI requires a caller-selected durable TTY: `rea debug-with-gdb --inferior-tty /dev/pts/7 'file ./program'`. The TTY must remain available after GDB exits so a detached inferior can continue writing. MCP starts GDB with `--nx --quiet --interpreter=mi3`, retries initialization with MI2 if MI3 initialization returns an error and cleanup succeeds, waits for its prompt, and disables auto-loading. Pass optional `inferior_tty` to select the terminal that future inferior runs use; REA configures it before the session becomes ready and checks the exact value with GDB/MI. `gdb_session_status` reports the selected path, whether REA still considers the configuration verified, and whether invalid UTF-8 has been observed in MI bytes (`mi_invalid_utf8`) or stderr (`stderr_invalid_utf8`). Commands that can change the inferior TTY or redirect a run revoke that verification permanently for the session, so close retains GDB ownership if a live inferior remains. The complete built-in inspections `show inferior-tty` and `help inferior-tty` preserve verification; compound commands still use the conservative mutation checks. Closing a session with one live inferior and a verified TTY automatically detaches the target, which can resume execution; MCP clients receive destructive-operation annotations for this behavior. The console remains unrestricted and can run shell commands, change target state, access files, and use network in the REA process user's security context. Raw MI records and console/target/log streams are returned with Evidence; command output is retained up to 16 MiB and explicitly marked when truncated. A single console, target or log record over the 16 MiB MI line limit is dropped and the command is marked truncated, while an oversized result or async record still stops the session. GDB startup waits 10000 ms and each MI command waits 120000 ms. This release has no runtime override for those deadlines, the 16 MiB command output budget, session capacity, or the pending-command queue. `command_timeout_ms` does not change them.
|
||||
|
||||
With multiple live inferiors, or a live inferior without a verified TTY, manual
|
||||
close requests an explicit disposition and keeps the session ready. Use
|
||||
|
||||
@@ -86,7 +86,10 @@ credentials and resets it between sequences.
|
||||
environment refused the call), `not-served`, `no-tool` and `unmapped-id`, each with a JSON pointer and short values.
|
||||
- The report gives agreement overall, per probe and per tool, agreement ignoring item order beside it, the counts per
|
||||
category, the ids mapped and the values normalised, the records left uncompared, and each step's differences (at most
|
||||
20 per step).
|
||||
20 per step). Each step gives `differences_total` before that cap, `differences_omitted`, and
|
||||
`differences_truncated`, true when entries were omitted. Equality and categories use all differences.
|
||||
Omitted entries are not retained and `--full-output` cannot recover them; further investigation uses the original
|
||||
capture and probe Evidence.
|
||||
|
||||
## Non-goals
|
||||
|
||||
|
||||
+20
-7
@@ -269,8 +269,8 @@ APK for the negative check. No engine or external tool is involved. Record:
|
||||
Flutter Gallery 2.9.2 (112 MB, arm64-v8a / armeabi-v7a / x86_64) — every
|
||||
ABI reports the same agreeing snapshot hash
|
||||
(`65817c30a78bb44c3dc3771876b6010a`, two sections each) read 20 bytes after
|
||||
the Dart snapshot magic, per-ABI engine GNU build-ids, no labeled Dart
|
||||
version string (expected for a release engine build), and the Android
|
||||
the Dart snapshot magic, per-ABI engine GNU build-ids, an unlabeled
|
||||
Dart-shaped version candidate, and the Android
|
||||
clang/LLVM toolchain line; the AOT inspection locates all four snapshot
|
||||
symbols through the dynamic symbol table on both ELF64 and ELF32 images,
|
||||
validates the data-section headers, agrees with the identification hash,
|
||||
@@ -284,7 +284,7 @@ prefixes, with no private local-file dependency. The verifier now compares
|
||||
compiled CLI and stdio MCP Evidence on every ABI, including data-header kind
|
||||
and declared lengths. Add
|
||||
`--expectations scripts/fixtures/flutter-gallery-expectations.json` for the
|
||||
documented Gallery APK to assert its pinned digest, hash, exact engine labels
|
||||
documented Gallery APK to assert its pinned digest, hash, exact engine strings
|
||||
and per-ABI URI counts. The verifier requires complete structural snapshot
|
||||
coverage, reports bounded pool coverage as partial/unverified, and reports
|
||||
exact labels/counts on other APKs unverified without an expectations file.
|
||||
@@ -421,7 +421,10 @@ the first cold pseudocode answer comes from a single-function run or an export
|
||||
that already finished, assembly waits for and serves from that export, and
|
||||
pseudocode and both dossier facets match their recorded file spans and digests,
|
||||
a fresh server reuses the export without rebuilding it, staging is empty after
|
||||
a completed export, and no owned process survives. Pass
|
||||
a completed export, and no owned process survives. A separate forced 1 ms
|
||||
project deadline checks that a timed-out inventory recommends raising the
|
||||
project deadline without changing jobs; a fresh server retries with 60 seconds
|
||||
and the same two-worker setting. Pass
|
||||
`--targets DIR --functions a,b` to add caller-built binaries laid out as
|
||||
`DIR/<flavor>/<name>` beside `<name>.stripped`; addresses come from the
|
||||
symbol-bearing build. `--compare-ghidra` with `GHIDRA_INSTALL_DIR` runs those
|
||||
@@ -451,6 +454,16 @@ Mach-O fixture and caller-built symbol-bearing and stripped x86-64 ELF fixtures.
|
||||
Linux and other hosts have not been verified with 1.988; this is not a repeat
|
||||
of the larger 1.833 matrix.
|
||||
|
||||
Kuna 1.989 passed the real-provider lane on macOS arm64 with the generated
|
||||
symbol-bearing and stripped Mach-O fixtures, including cold and warm reads,
|
||||
references, and cleanup. Linux, other hosts, and the larger 1.833 target matrix
|
||||
were not verified with 1.989.
|
||||
|
||||
Kuna 1.990 passed the real-provider lane on macOS arm64 with the generated
|
||||
symbol-bearing and stripped Mach-O fixtures, including cold and warm reads,
|
||||
references, timeout recovery, and cleanup. Linux, other hosts, and the larger
|
||||
1.833 target matrix were not verified with 1.990.
|
||||
|
||||
## DOS Ghidra analysis
|
||||
|
||||
`npm run verify:ghidra:dos` requires the supported Ghidra and JDK installation
|
||||
@@ -1103,13 +1116,13 @@ coverage limits.
|
||||
### Offline binary layout
|
||||
|
||||
`npm run verify:binary:layout` requires Linux x64, GCC/binutils, the
|
||||
`i686-linux-gnu`, `arm-linux-gnueabi`, `mips-linux-gnu` and `mipsel-linux-gnu`
|
||||
cross `gcc`/`readelf` toolchains, absolute
|
||||
`aarch64-linux-gnu`, `i686-linux-gnu`, `arm-linux-gnueabi`, `mips-linux-gnu` and
|
||||
`mipsel-linux-gnu` cross `gcc`/`readelf` toolchains, absolute
|
||||
`REA_PWNTOOLS_PYTHON` with pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2 and
|
||||
absolute `REA_VERIFY_STRACE_COMMAND`. It compiles ephemeral source-owned ELF
|
||||
fixtures and checks public CLI/MCP, lossless addresses/names, file ranges,
|
||||
mitigation inferences, malformed/unsupported input, original file hashes and
|
||||
released process ownership. Cross-compiled ELF32 executables and relocatable
|
||||
released process ownership. Cross-compiled ELF64 AArch64 and ELF32 executables and relocatable
|
||||
objects check the reported class, machine and byte order, and every section
|
||||
name, address, offset and size against GNU `readelf`. Exec syscall tracing must identify only the declared
|
||||
Node/Python launchers; no target binary is executed. Core/debugger claims need
|
||||
|
||||
+3
-1
@@ -10,7 +10,9 @@ strings, declared size, dataflags, blocks-and-directory table bounds, the
|
||||
producer's 16-byte data hash as declared, and the resolved data-area offset.
|
||||
Members (`CAB-*` SerializedFiles, `.resS` resource streams, shared assets)
|
||||
retain declared source paths, virtual stream offsets, sizes and raw node flags.
|
||||
The directory flag identifies directories. Virtual offsets are not physical
|
||||
The directory flag identifies directories. `compare_artifacts` compares each
|
||||
member's declared `source_path` and raw `flags`; its index and offset are
|
||||
layout. Virtual offsets are not physical
|
||||
carrier locations: member `source_location` is null. Member reads only examine
|
||||
their covering blocks. Uncompressed ranges stream directly; compressed ranges
|
||||
reuse at most one completed decoded block. Independent 64 KiB output chunks
|
||||
|
||||
@@ -97,9 +97,13 @@ it does not invent an empty string or body bytes.
|
||||
The observed Playwright 1.58.2 profile can report a bracketed IPv6
|
||||
`serverIPAddress`, forwarded from Chromium's `remoteIPAddress` through
|
||||
[Playwright's HAR tracer](https://github.com/microsoft/playwright/blob/v1.58.2/packages/playwright-core/src/server/har/harTracer.ts).
|
||||
Chromium DevTools exports (declared as `WebInspector` `537.36`, the frozen
|
||||
AppleWebKit version in Chromium's user agent) report the same bracketed form:
|
||||
[its HAR writer](https://github.com/ChromeDevTools/devtools-frontend/blob/63739e8ec45ca01b9e4be9b8b05946acdadd8f1f/front_end/models/har/Log.ts)
|
||||
strips the port but not the brackets.
|
||||
REA preserves the original spelling in reported Evidence and removes brackets
|
||||
only from its validation copy, only for this declared producer and a valid IPv6
|
||||
literal. Invalid addresses, bracketed IPv4, ports and zone identifiers remain
|
||||
only from its validation copy, only for these declared producers and a valid
|
||||
IPv6 literal. Invalid addresses, bracketed IPv4, ports and zone identifiers remain
|
||||
validation failures. Producer declarations do not authenticate origin.
|
||||
|
||||
Native mitmproxy decoding requires a caller-supplied **mitmdump 12.2.3 on
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ Results retain:
|
||||
- Main-document script inventory, exact session script IDs, resource start coordinates, sourceURL/source-map declarations, producer hash and independently computed UTF-8 text digest.
|
||||
- Armed-window requests from the selected main frame and selected origins, complete reported initiator objects (including stack hierarchy/descriptions), derived callsites, exact script-ID associations and unresolved async parent IDs. Source association does not establish UI causality. Callsite and listener `line_number`/`column_number` values are CDP's zero-based lines and UTF-16 columns in the resource named by `url`. With a `script_id`, that is the script's enclosing resource: the script file for an external script, or the HTML document for an inline script. `resource_start` is where the script text starts in that resource. A callsite with an empty `script_id` comes from the request's reported initiator rather than a script call frame, such as markup that the parser read in a document or a module import in a script file. It still locates a position in `url`, but has no session-script association, so it cannot be joined to captured source or `resource_start`. `trace_web_source_location` takes one-based, script-relative lines and zero-based UTF-16 columns.
|
||||
|
||||
For an external script, where `url` is the script file, use `line = line_number + 1` and keep `column = column_number`; this also applies to an empty-`script_id` initiator position in a script file. For an inline script, which requires a `script_id` and its `resource_start`, use `line = line_number - resource_start.line_number + 1`. Subtract `resource_start.column_number` from `column_number` only when `line_number === resource_start.line_number`; keep the reported column on later lines. Columns remain zero-based. This conversion requires known source-start and column coordinates; preserve unavailable coordinates as unknown.
|
||||
For an external script, where `url` is the script file, use `line = line_number + 1` and keep `column = column_number`; this also applies to an empty-`script_id` initiator position in a script file. For an inline script, which requires a `script_id` and its `resource_start`, use `line = line_number - resource_start.line_number + 1`. Subtract `resource_start.column_number` from `column_number` only when `line_number === resource_start.line_number`; keep the reported column on later lines. Columns remain zero-based. This conversion requires known source-start and column coordinates; preserve unavailable coordinates as unknown. Chrome reports a negative start column for some inline scripts, such as one in a helper frame, and positions on that script's first line include it. REA keeps those columns as reported rather than failing the observation; a negative column does not locate a column in `url`, but the subtraction above still gives the script-relative column.
|
||||
|
||||
The coverage counter interval starts at backend acceptance before the armed receipt and ends at the resetting sample after the local timer. Counts may include execution during these command intervals; request events are restricted to the locally armed window. The result retains both backend timestamps and the distinct local clock.
|
||||
|
||||
|
||||
@@ -86,8 +86,10 @@ and import map have 32 MiB, 16 MiB and 4 MiB input limits. Native resolution has
|
||||
one 20-second deadline; cleanup may extend that deadline. One renderer is
|
||||
requested, and there is no aggregate host memory-limit claim. Cancellation
|
||||
closes only the owned browser; cleanup failure cannot return a successful trace.
|
||||
`REA_BROWSER_NO_SANDBOX=true` passes Chromium's corresponding launch option when
|
||||
the host requires it; it does not enable an REA security boundary.
|
||||
Chromium keeps its sandbox, except under Termux on Android, which always
|
||||
launches it without one. On hosts that cannot provide one, such as root in a
|
||||
container, `REA_BROWSER_NO_SANDBOX=true` launches it without the sandbox; the
|
||||
variable does not enable an REA security boundary.
|
||||
|
||||
See [testing](testing.md) for the native loading oracle and CLI/MCP real lane.
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# WeChat mini-program wxapkg archives
|
||||
|
||||
`inspect-artifact` and `extract-artifact` read plaintext WeChat mini-program
|
||||
package (`.wxapkg`) containers without running WeChat or an unpacker. The
|
||||
plaintext form starts with a `0xBE` mark and an 18-byte big-endian header:
|
||||
an unknown `info` field producers leave zero, the member-table byte length,
|
||||
the member-data byte length, a `0xED` mark and the member count. Producers
|
||||
either count only the name/offset/size rows in that table length or include
|
||||
the four-byte member count as well; both declarations are accepted. The table
|
||||
region then carries one name/offset/size row per member; member bytes are
|
||||
stored verbatim at absolute file offsets.
|
||||
|
||||
Inspection retains the header facts: `info`, index and data lengths, member
|
||||
count and directory status. Members retain declared source paths, absolute
|
||||
carrier offsets and declared sizes, so `source_location` reports real file
|
||||
positions. Member reads only examine their declared range in bounded 64 KiB
|
||||
chunks. Carrier freshness is checked when a member is opened and after its
|
||||
stream completes. Reader close cancels and joins outstanding streams before
|
||||
releasing its descriptor.
|
||||
|
||||
Encrypted PC-client packages start with the `V1MMWX` magic instead of the
|
||||
plain header. They keep their format identity but report `encrypted`
|
||||
directory status with unknown coverage: member identities and bytes remain
|
||||
unknown and no decryption is attempted, so extraction refuses with a typed
|
||||
error rather than producing ciphertext. Declared tables or data regions
|
||||
that exceed the file, member extents outside the declared data region,
|
||||
truncated rows, invalid UTF-8 names, and counts that cannot fit their table
|
||||
are malformed-input errors.
|
||||
|
||||
## Resource bounds
|
||||
|
||||
These are resource policies, not format maxima. The encoded member table is
|
||||
limited to 16 MiB and its buffer, parsed rows, retained entries and escaped
|
||||
path projections share a conservative 64 MiB admission budget, reserved
|
||||
before allocation; a declared member name is limited to 64 KiB. A reader
|
||||
admits four active member streams. The shared archive plaintext-work ceiling
|
||||
is the larger of 256 MiB and 500 times carrier size: every declared member
|
||||
extent is charged, including aliases that overlap one stored range. A
|
||||
separate member-I/O budget allows four times the larger of 1,024 and the sum
|
||||
of the member count and declared 64 KiB plaintext chunks, bounding repeated
|
||||
tiny-member reads independently of output bytes.
|
||||
|
||||
## CLI
|
||||
|
||||
```sh
|
||||
rea inspect-artifact app.wxapkg --detail summary \
|
||||
--evidence_output evidence/package.json --json
|
||||
rea extract-artifact app.wxapkg --json
|
||||
```
|
||||
|
||||
Detection is byte-based on the `0xBE`/`0xED` marks or the `V1MMWX` magic, so
|
||||
`.wxapkg`, `__APP__.wxapkg` and extensionless cache files all classify as
|
||||
`wxapkg` containers. Compact delivery keeps complete Evidence in the
|
||||
requested bundle.
|
||||
|
||||
## Verification scope
|
||||
|
||||
Synthetic fixtures cover plaintext headers, multiple members, empty members,
|
||||
chunked reads, unicode and dotted names, malformed marks, truncated tables,
|
||||
invalid UTF-8, table and directory-budget admission, member extents inside
|
||||
the header, the index, or beyond the declared data region, a data region
|
||||
declared past the file, a one-byte member whose table length includes the
|
||||
member count, an overstated first row consuming its successor, V1MMWX
|
||||
packages, cancellation inside member reads, paused iteration, close during
|
||||
I/O, cleanup retry, stream capacity and carrier freshness. Compiled
|
||||
full/compact MCP tests cover inventory and extraction.
|
||||
|
||||
Real producer verification used plaintext `0xBE` packages from a local macOS
|
||||
WeChat mini-program cache (`__APP__.wxapkg` main packages and
|
||||
`__PLUGINCODE__.wxapkg` plugin packages across three distinct app
|
||||
directories): every member extracted byte-identical to its declared carrier
|
||||
range, including `__plugin__/`-namespaced members and the producer's
|
||||
root-prefixed (`/name`) member convention. A cached `V1MMWX` PC package
|
||||
reports the encrypted directory state with unknown member facts and refuses
|
||||
extraction.
|
||||
|
||||
`wxml`/`wxss` source recovery, compiled bundle semantics, subpackage
|
||||
relationships and runtime behavior are outside this container reader's
|
||||
verification scope.
|
||||
|
||||
Artifact comparison pairs members by their package-relative paths and
|
||||
compares their bytes. It does not compare `wxapkg` or `wxapkg-entry`
|
||||
metadata: header fields repeat the package bytes, `index` is layout, and
|
||||
`source_path` keeps a producer root prefix that the occurrence path strips.
|
||||
@@ -81,6 +81,51 @@
|
||||
// In this case, there is no squashed merge commit. This could be a simple
|
||||
// merge commit, a rebase merge commit, or a direct commit to the branch.
|
||||
// Fallback to fetching the list of commits from the REST API. In the future
|
||||
@@ -1154,15 +1154,40 @@
|
||||
* @returns {string} HTML URL of the new file
|
||||
*/
|
||||
async createFileOnNewBranch(filename, contents, newBranchName, baseBranchName) {
|
||||
- // create or update new branch to match base branch
|
||||
- await this.forkBranch(newBranchName, baseBranchName);
|
||||
- // use the single file upload API
|
||||
+ // Preserve existing notes until the replacement file is committed.
|
||||
+ // A failed write must not leave an open proposal pointing at a missing file.
|
||||
+ if (!(await this.getBranchSha(newBranchName))) {
|
||||
+ const baseSha = await this.getBranchSha(baseBranchName);
|
||||
+ if (!baseSha) {
|
||||
+ throw new Error(`Unable to find base branch: ${baseBranchName}`);
|
||||
+ }
|
||||
+ await this.createNewBranch(newBranchName, baseSha);
|
||||
+ }
|
||||
+ let existingSha;
|
||||
+ try {
|
||||
+ const { data } = await this.octokit.repos.getContent({
|
||||
+ owner: this.repository.owner,
|
||||
+ repo: this.repository.repo,
|
||||
+ path: filename,
|
||||
+ ref: newBranchName,
|
||||
+ });
|
||||
+ if (Array.isArray(data) || data.type !== 'file' || !data.sha) {
|
||||
+ throw new Error(`Expected a file at ${filename} on branch: ${newBranchName}`);
|
||||
+ }
|
||||
+ existingSha = data.sha;
|
||||
+ }
|
||||
+ catch (error) {
|
||||
+ if (!(error && error.status === 404)) {
|
||||
+ throw error;
|
||||
+ }
|
||||
+ }
|
||||
const { data: { content }, } = await this.octokit.repos.createOrUpdateFileContents({
|
||||
owner: this.repository.owner,
|
||||
repo: this.repository.repo,
|
||||
path: filename,
|
||||
// contents need to be base64 encoded
|
||||
- content: Buffer.from(contents, 'binary').toString('base64'),
|
||||
+ content: Buffer.from(contents, 'utf8').toString('base64'),
|
||||
+ ...(existingSha ? { sha: existingSha } : {}),
|
||||
message: 'Saving release notes',
|
||||
branch: newBranchName,
|
||||
});
|
||||
--- a/build/src/github.d.ts
|
||||
+++ b/build/src/github.d.ts
|
||||
@@ -47,6 +47,8 @@
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface ReadinessPullRequest {
|
||||
readonly state: "open" | "closed";
|
||||
readonly draft: boolean;
|
||||
readonly body: string | null;
|
||||
readonly user: { readonly id: number } | null;
|
||||
readonly head: { readonly sha: string };
|
||||
readonly base: { readonly ref: string };
|
||||
}
|
||||
@@ -53,7 +54,7 @@ export interface ReadinessGitHub {
|
||||
>;
|
||||
}
|
||||
export function evaluateReadiness(
|
||||
pull: Pick<ReadinessPullRequest, "body" | "draft">,
|
||||
pull: Pick<ReadinessPullRequest, "body" | "draft" | "user">,
|
||||
template: string,
|
||||
): ReadinessResult;
|
||||
export function runReadinessCheck(input: {
|
||||
|
||||
@@ -2,6 +2,14 @@ import { readFile } from "node:fs/promises";
|
||||
|
||||
const result = (state, description) => ({ state, description });
|
||||
|
||||
// GitHub account IDs remain stable when a login changes.
|
||||
const EXEMPT_AUTHOR_IDS = new Set([
|
||||
76467478, // morluto
|
||||
208281322, // kaoru0822-kitauji
|
||||
161784452, // N0zoM1z0
|
||||
342580529, // gensokyo-dev
|
||||
]);
|
||||
|
||||
const OPEN_HEADS_QUERY = `query ReadinessOpenHeads($owner: String!, $name: String!, $after: String) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
pullRequests(first: 100, after: $after, states: OPEN, orderBy: {field: CREATED_AT, direction: ASC}) {
|
||||
@@ -105,6 +113,11 @@ export function evaluateReadiness(pull, template) {
|
||||
"pending",
|
||||
"Draft PR: complete Author readiness before requesting review.",
|
||||
);
|
||||
if (EXEMPT_AUTHOR_IDS.has(pull.user?.id))
|
||||
return result(
|
||||
"success",
|
||||
"PR author is exempt from the readiness declaration.",
|
||||
);
|
||||
const expected = readinessLines(template).flatMap((line) => {
|
||||
const match = /^- \[ \] (.+)$/u.exec(line);
|
||||
return match ? [match[1]] : [];
|
||||
@@ -210,6 +223,7 @@ export async function runReadinessCheck({ github, context, core }) {
|
||||
latest.head.sha !== pull.head.sha ||
|
||||
latest.body !== pull.body ||
|
||||
latest.draft !== pull.draft ||
|
||||
latest.user?.id !== pull.user?.id ||
|
||||
latest.base.ref !== pull.base.ref
|
||||
)
|
||||
verdict = result(
|
||||
|
||||
@@ -76,6 +76,7 @@ const PATH_SCOPES = {
|
||||
"src/frida/frida-bindings.d.ts",
|
||||
"src/objdump/**",
|
||||
"src/cli/reverseEngineeringCommands.ts",
|
||||
"src/process/ownedDebuggerCleanupError.ts",
|
||||
"src/process/ownedDebuggerSession.ts",
|
||||
"src/rizin/**",
|
||||
"src/contracts/*ToolContracts.ts",
|
||||
@@ -103,6 +104,7 @@ const PATH_SCOPES = {
|
||||
"tests/boundary/providers/RizinInferiorTty.test.ts",
|
||||
"tests/boundary/providers/RizinTestInferiorTty.ts",
|
||||
"tests/boundary/process/ownedDebuggerSession.test.ts",
|
||||
"tests/boundary/cli/debuggerOneShotExit.test.ts",
|
||||
"tests/boundary/cli/gdbStartupCleanupCli.test.ts",
|
||||
"tests/boundary/cli/reverseEngineeringCommands.test.ts",
|
||||
"tests/boundary/mcp/cutter*.test.ts",
|
||||
@@ -163,6 +165,9 @@ const PATH_SCOPES = {
|
||||
"tests/boundary/mcp/nativeDemangleMcp.test.ts",
|
||||
"tests/boundary/mcp/nativeMachoSummaryMcp.test.ts",
|
||||
"tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts",
|
||||
"tests/boundary/providers/native/nativeUiOffscreenWindow.test.ts",
|
||||
"tests/boundary/process/nativeUiVerifierLane.test.ts",
|
||||
"tests/conformance/native/ui-offscreen.swift",
|
||||
"tests/boundary/cli/nativeSignatureCli.test.ts",
|
||||
"tests/boundary/cli/nativeMachoArm64_32Cli.test.ts",
|
||||
"tests/boundary/cli/nativeMachoEncryptionCli.test.ts",
|
||||
@@ -699,7 +704,9 @@ export const CI_LANES = {
|
||||
agentenv: sameJobs(PATH_SCOPES.agentenv, ["agentenv"]),
|
||||
nativeaot: sameJobs(PATH_SCOPES.nativeaot, ["nativeaot"]),
|
||||
ghidra_bridge: sameJobs(PATH_SCOPES.ghidra_bridge, ["ghidra_bridge"]),
|
||||
website: sameJobs(PATH_SCOPES.website, ["website"]),
|
||||
website: sameJobs(PATH_SCOPES.website, ["website"], {
|
||||
earlyInputs: [".agents/skills/reverse-engineer-anything/**"],
|
||||
}),
|
||||
};
|
||||
// An input owner omitted from the registry must never silently lose a lane.
|
||||
for (const [name, paths] of Object.entries(PATH_SCOPES))
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
"snapshot_hash": "65817c30a78bb44c3dc3771876b6010a",
|
||||
"engines": {
|
||||
"x86_64": {
|
||||
"dart_version": null,
|
||||
"dart_version": "2.14.0-151.0.dev (dev) (Tue May 25 10:58:39 2021 -0700)",
|
||||
"toolchain_lines": [
|
||||
"Android (5900059 based on r365631c) clang version 9.0.8 (https://android.googlesource.com/toolchain/llvm-project 207d7abc1a2abf3ef8d4301736d6a7ebc224a290) (based on LLVM 9.0.8svn)"
|
||||
]
|
||||
},
|
||||
"armeabi-v7a": {
|
||||
"dart_version": null,
|
||||
"dart_version": "2.14.0-151.0.dev (dev) (Tue May 25 10:58:39 2021 -0700)",
|
||||
"toolchain_lines": [
|
||||
"Android (5900059 based on r365631c) clang version 9.0.8 (https://android.googlesource.com/toolchain/llvm-project 207d7abc1a2abf3ef8d4301736d6a7ebc224a290) (based on LLVM 9.0.8svn)"
|
||||
]
|
||||
},
|
||||
"arm64-v8a": {
|
||||
"dart_version": null,
|
||||
"dart_version": "2.14.0-151.0.dev (dev) (Tue May 25 10:58:39 2021 -0700)",
|
||||
"toolchain_lines": [
|
||||
"Android (5900059 based on r365631c) clang version 9.0.8 (https://android.googlesource.com/toolchain/llvm-project 207d7abc1a2abf3ef8d4301736d6a7ebc224a290) (based on LLVM 9.0.8svn)"
|
||||
]
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
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 { parseEvidence } from "../../dist/domain/evidence.js";
|
||||
import { webSourceLocationResultSchema } from "../../dist/domain/webSourceLocation.js";
|
||||
import { PrivateRuntimeRoot } from "../../dist/process/PrivateRuntimeRoot.js";
|
||||
import { startBrowserSourceMapSite } from "../fixtures/browser-source-map-site.mjs";
|
||||
import { mcpTextValue } from "./mcp-verifier-results.mjs";
|
||||
import { withVerifierCleanup } from "./verifier-cleanup.mjs";
|
||||
import { createPackageVerifierCommands } from "./verify-package-commands.mjs";
|
||||
import { importVerifierSource } from "./verifier-source-loader.mjs";
|
||||
|
||||
const { BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS } = await importVerifierSource(
|
||||
new URL("./browser-process-startup.mts", import.meta.url).href,
|
||||
import.meta.url,
|
||||
);
|
||||
|
||||
/** Verify real Chrome capture → export → original compiler point through packaged CLI/MCP. */
|
||||
export async function verifyBrowserSourceMap(
|
||||
@@ -21,21 +26,35 @@ export async function verifyBrowserSourceMap(
|
||||
) {
|
||||
let site;
|
||||
let root;
|
||||
let commands;
|
||||
let transport;
|
||||
let client;
|
||||
const closeResource = async (owner, resource) => {
|
||||
if (owner === undefined) return;
|
||||
try {
|
||||
await owner.close();
|
||||
} catch (cause) {
|
||||
commands?.adopt(owner, resource);
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
return withVerifierCleanup(async () => {
|
||||
site = await startBrowserSourceMapSite();
|
||||
root = await mkdtemp(join(tmpdir(), "rea-browser-source-map-"));
|
||||
root = await PrivateRuntimeRoot.create({
|
||||
prefix: "rea-browser-source-map-",
|
||||
});
|
||||
commands = await createPackageVerifierCommands({ workspace: root });
|
||||
const env = {
|
||||
...process.env,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
|
||||
};
|
||||
const cli = async (...args) => {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
const { stdout } = await commands.exec(
|
||||
process.execPath,
|
||||
[entrypoint, ...args, "--json"],
|
||||
{ env, timeout: 45000, maxBuffer: 16 * 1024 * 1024 },
|
||||
// Allow the shared cold-start budget plus capture, cleanup and delivery.
|
||||
{ env, timeout: 120_000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
return parseEvidence(JSON.parse(stdout));
|
||||
};
|
||||
@@ -50,6 +69,7 @@ export async function verifyBrowserSourceMap(
|
||||
"capture-browser-scenario",
|
||||
JSON.stringify({
|
||||
browser: { mode: "launch", executable_path: executable },
|
||||
command_timeout_ms: BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS,
|
||||
start_url: { url: site.origin },
|
||||
actions: [
|
||||
{
|
||||
@@ -66,12 +86,12 @@ export async function verifyBrowserSourceMap(
|
||||
capture: { network: { response_body: true } },
|
||||
}),
|
||||
);
|
||||
const capturePath = join(root, "capture.json");
|
||||
const capturePath = join(root.path, "capture.json");
|
||||
await writeFile(capturePath, JSON.stringify(capture));
|
||||
const exported = await cli(
|
||||
"export-web-scripts",
|
||||
capturePath,
|
||||
join(root, "export"),
|
||||
join(root.path, "export"),
|
||||
);
|
||||
const manifest = exported.normalized_result;
|
||||
const manifestBytes = await readFile(manifest.manifest.path);
|
||||
@@ -86,7 +106,7 @@ export async function verifyBrowserSourceMap(
|
||||
scriptIndex >= 0,
|
||||
"Actual browser capture omitted compiled source",
|
||||
);
|
||||
const mapPath = join(root, "app.js.map");
|
||||
const mapPath = join(root.path, "app.js.map");
|
||||
await writeFile(mapPath, site.map);
|
||||
const before = site.mapRequests();
|
||||
await client.connect(transport);
|
||||
@@ -132,13 +152,11 @@ export async function verifyBrowserSourceMap(
|
||||
public_point_cases: 4,
|
||||
};
|
||||
}, [
|
||||
() => client?.close(),
|
||||
() => transport?.close(),
|
||||
() => site?.close(),
|
||||
() =>
|
||||
root === undefined
|
||||
? undefined
|
||||
: rm(root, { recursive: true, force: true }),
|
||||
() => closeResource(client, "source-map MCP client"),
|
||||
() => closeResource(transport, "source-map stdio transport"),
|
||||
() => closeResource(site, "source-map fixture site"),
|
||||
() => commands?.close(),
|
||||
() => (commands === undefined ? root?.close() : undefined),
|
||||
]);
|
||||
}
|
||||
function assertPoint(evidence, { site, input, capture, identity }) {
|
||||
|
||||
@@ -114,6 +114,46 @@ export const verifyKunaReferences = async ({
|
||||
);
|
||||
}
|
||||
assert.equal(trace.value.truncated, false);
|
||||
const summary = await call("trace_feature", {
|
||||
query: name,
|
||||
detail: "summary",
|
||||
});
|
||||
assert.equal(
|
||||
summary.value.parent_evidence_id,
|
||||
trace.structured.evidence_id,
|
||||
);
|
||||
assert.equal(
|
||||
summary.value.summary.references,
|
||||
trace.value.references.length,
|
||||
);
|
||||
assert.equal(
|
||||
summary.value.summary.unknowns,
|
||||
trace.value.residual_unknowns.length,
|
||||
);
|
||||
for (const [collection, values] of [
|
||||
["matches", trace.value.matches],
|
||||
["references", trace.value.references],
|
||||
["unknowns", trace.value.residual_unknowns],
|
||||
]) {
|
||||
const view = await call("inspect_analysis_view", {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: trace.structured.evidence_id,
|
||||
},
|
||||
view: {
|
||||
kind: "native-feature-trace",
|
||||
collection,
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
},
|
||||
});
|
||||
assert.equal(view.value.coverage.total, values.length);
|
||||
assert.deepEqual(
|
||||
view.value.items.rows.map(({ value }) => value),
|
||||
values.slice(0, 1),
|
||||
);
|
||||
assert.equal(view.value.items.collection, collection);
|
||||
}
|
||||
}
|
||||
composed = {
|
||||
graph_ms: graph.ms,
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
requireMcpOutput,
|
||||
} from "./mcp-verifier-results.mjs";
|
||||
import { verifyGhidraBoundaries } from "./real-ghidra-boundaries.mjs";
|
||||
import { verifyGhidraProjectIdentity } from "./real-ghidra-project-identity.mjs";
|
||||
|
||||
const defaultEntrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
|
||||
const environment = () => ({
|
||||
@@ -214,6 +215,19 @@ async function verifyNativeValueAdapters(
|
||||
);
|
||||
assert.deepEqual(mcpEvidence.normalized_result, mcpGraph);
|
||||
assert.deepEqual(mcpGraph, cliGraph);
|
||||
const warmReply = await client.callTool(
|
||||
{
|
||||
name: "trace_native_values",
|
||||
arguments: { procedure, max_depth: 2, max_functions: 8, limit: 3 },
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
const warmEvidence = parseEvidence(
|
||||
requireMcpOutput(warmReply, "trace_native_values"),
|
||||
);
|
||||
const warmGraph = nativeValueTraceSchema.parse(
|
||||
warmEvidence.normalized_result,
|
||||
);
|
||||
const analyzed = await client.callTool(
|
||||
{
|
||||
name: "analyze_function",
|
||||
@@ -253,20 +267,9 @@ async function verifyNativeValueAdapters(
|
||||
functionEvidence.analysis_profile,
|
||||
);
|
||||
assert.deepEqual(functionProfile, upstreamProfile);
|
||||
const comparison = await client.callTool(
|
||||
{
|
||||
name: "compare_functions",
|
||||
arguments: {
|
||||
left: analyzed.structuredContent,
|
||||
right: analyzed.structuredContent,
|
||||
},
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(comparison.isError, true, JSON.stringify(comparison));
|
||||
assert.notEqual(comparison.structuredContent, undefined);
|
||||
const comparisonEvidence = parseEvidence(
|
||||
requireMcpOutput(comparison, "compare_functions"),
|
||||
const comparisonEvidence = await compareRetainedFunctionEvidence(
|
||||
client,
|
||||
functionEvidence,
|
||||
);
|
||||
assert.equal(
|
||||
comparisonEvidence.normalized_result.status,
|
||||
@@ -286,6 +289,25 @@ async function verifyNativeValueAdapters(
|
||||
"MCP omitted structured Evidence bundle",
|
||||
);
|
||||
const bundle = requireMcpEvidenceBundle(bundleReply);
|
||||
for (const [evidence, graph] of [
|
||||
[mcpEvidence, mcpGraph],
|
||||
[warmEvidence, warmGraph],
|
||||
]) {
|
||||
for (const source of [
|
||||
...graph.procedures,
|
||||
...graph.nodes,
|
||||
...graph.edges,
|
||||
]) {
|
||||
assert.ok(evidence.evidence_links.includes(source.evidence_id));
|
||||
assert.equal(
|
||||
bundle.records.find(
|
||||
(record) => record.evidence_id === source.evidence_id,
|
||||
)?.operation,
|
||||
"analyze_function",
|
||||
`Missing source Evidence ${source.evidence_id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const evidence of [
|
||||
mcpEvidence,
|
||||
functionEvidence,
|
||||
@@ -328,6 +350,11 @@ async function verifyNativeValueAdapters(
|
||||
await client.close();
|
||||
await transport.close();
|
||||
}
|
||||
const projectIdentity = await verifyGhidraProjectIdentity({
|
||||
target,
|
||||
entrypoint,
|
||||
env,
|
||||
});
|
||||
return {
|
||||
mocked: false,
|
||||
cli: true,
|
||||
@@ -335,6 +362,7 @@ async function verifyNativeValueAdapters(
|
||||
inline_evidence: true,
|
||||
direct_function_comparison: true,
|
||||
boundaries,
|
||||
project_identity: projectIdentity,
|
||||
nodes: cliGraph.total_nodes,
|
||||
edges: cliGraph.total_edges,
|
||||
decompilations: cliGraph.decompilations,
|
||||
@@ -395,21 +423,27 @@ async function verifyUnresolvedCallEvidence(client) {
|
||||
assert.equal(trace.goal_status, "unknown");
|
||||
assert.equal(trace.truncated, true);
|
||||
assert.ok(trace.residual_unknowns.length > 0);
|
||||
const compared = await compareRetainedFunctionEvidence(client, evidence);
|
||||
assert.equal(compared.normalized_result.status, "unknown");
|
||||
}
|
||||
|
||||
/** Compare an already admitted function record without retransmitting its payload. */
|
||||
export async function compareRetainedFunctionEvidence(client, evidence) {
|
||||
const reference = {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: evidence.evidence_id,
|
||||
};
|
||||
const compared = await client.callTool(
|
||||
{
|
||||
name: "compare_functions",
|
||||
arguments: {
|
||||
left: analyzed.structuredContent,
|
||||
right: analyzed.structuredContent,
|
||||
left: reference,
|
||||
right: reference,
|
||||
},
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(compared.isError, true, JSON.stringify(compared));
|
||||
assert.notEqual(compared.structuredContent, undefined);
|
||||
assert.equal(
|
||||
parseEvidence(requireMcpOutput(compared, "compare_functions"))
|
||||
.normalized_result.status,
|
||||
"unknown",
|
||||
);
|
||||
return parseEvidence(requireMcpOutput(compared, "compare_functions"));
|
||||
}
|
||||
@@ -504,6 +504,9 @@ export async function verifyGhidraBoundaries(
|
||||
// Prime both inventories before editing so stale names cannot hide in caches.
|
||||
await call("list_names");
|
||||
await call("list_procedures");
|
||||
const traceInput = { procedure: address, max_depth: 0, max_functions: 1 };
|
||||
const traceBefore = await evidence("trace_native_values", traceInput);
|
||||
const sourceBefore = traceBefore.normalized_result.procedures[0].evidence_id;
|
||||
const changes = {
|
||||
procedure: address,
|
||||
name: "dead",
|
||||
@@ -517,6 +520,16 @@ export async function verifyGhidraBoundaries(
|
||||
comment: changes.comment,
|
||||
inline_comment: changes.inline_comment,
|
||||
});
|
||||
const traceAfter = await evidence("trace_native_values", traceInput);
|
||||
const sourceAfter = traceAfter.normalized_result.procedures[0].evidence_id;
|
||||
assert.notEqual(sourceBefore, sourceAfter);
|
||||
const sourceProcedure = (evidenceId) =>
|
||||
call("inspect_analysis_view", {
|
||||
source: { kind: "retained-evidence", evidence_id: evidenceId },
|
||||
view: { kind: "native", facet: "procedure", offset: 0, limit: 1 },
|
||||
});
|
||||
assert.equal((await sourceProcedure(sourceBefore)).item.name, name);
|
||||
assert.equal((await sourceProcedure(sourceAfter)).item.name, changes.name);
|
||||
const regexPattern = "^(a|aa)*b$";
|
||||
const regexFailure = await client.callTool(
|
||||
{
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
appendFile,
|
||||
copyFile,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { requireMcpToolError } from "./mcp-verifier-results.mjs";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
|
||||
/** Verify selected-project identity refusal through real CLI and MCP adapters. */
|
||||
export async function verifyGhidraProjectIdentity({ target, entrypoint, env }) {
|
||||
if (process.platform === "win32")
|
||||
return {
|
||||
status: "not-applicable",
|
||||
reason: "Existing Ghidra projects are not admitted on Windows.",
|
||||
};
|
||||
assert.ok(env.GHIDRA_INSTALL_DIR, "Ghidra installation is required");
|
||||
const root = await mkdtemp(join(env.TMPDIR, "rea-project-identity-"));
|
||||
const targetPath = target.path;
|
||||
const different = join(root, "different-target");
|
||||
const headless = join(env.GHIDRA_INSTALL_DIR, "support", "analyzeHeadless");
|
||||
const fixtureEnv = {
|
||||
...env,
|
||||
HOME: root,
|
||||
XDG_CONFIG_HOME: join(root, "config"),
|
||||
XDG_CACHE_HOME: join(root, "cache"),
|
||||
XDG_DATA_HOME: join(root, "data"),
|
||||
JDK_JAVA_OPTIONS: `-Duser.home="${root}"`,
|
||||
};
|
||||
const program = basename(targetPath);
|
||||
const known = join(root, "known.gpr");
|
||||
const unknown = join(root, "unknown.gpr");
|
||||
const runtimeRootsBefore = (await readdir(env.TMPDIR))
|
||||
.filter((name) => name.startsWith("rea-ghidra-"))
|
||||
.sort();
|
||||
const sha = async (path) =>
|
||||
createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex");
|
||||
try {
|
||||
await copyFile(targetPath, different);
|
||||
await appendFile(different, "\nREA_IDENTITY_VERIFIER\n");
|
||||
assert.notEqual(await sha(different), await sha(targetPath));
|
||||
await writeFile(
|
||||
join(root, "ClearImportDigest.java"),
|
||||
`import ghidra.app.script.GhidraScript;
|
||||
public class ClearImportDigest extends GhidraScript {
|
||||
@Override public void run() {
|
||||
currentProgram.setExecutableSHA256(null);
|
||||
println("REA_DIGEST_AFTER=" + currentProgram.getExecutableSHA256());
|
||||
}
|
||||
}\n`,
|
||||
);
|
||||
await exec(
|
||||
headless,
|
||||
[root, "known", "-import", targetPath, "-noanalysis"],
|
||||
{ env: fixtureEnv, timeout: 180000, maxBuffer: 8 * 1024 * 1024 },
|
||||
);
|
||||
const cleared = await exec(
|
||||
headless,
|
||||
[
|
||||
root,
|
||||
"unknown",
|
||||
"-import",
|
||||
targetPath,
|
||||
"-noanalysis",
|
||||
"-scriptPath",
|
||||
root,
|
||||
"-postScript",
|
||||
"ClearImportDigest.java",
|
||||
],
|
||||
{ env: fixtureEnv, timeout: 180000, maxBuffer: 8 * 1024 * 1024 },
|
||||
);
|
||||
assert.match(cleared.stdout, /REA_DIGEST_AFTER=unknown/u);
|
||||
const before = await projectState([known, unknown, targetPath, different]);
|
||||
|
||||
const knownEnv = {
|
||||
...env,
|
||||
REA_GHIDRA_PROJECT_PATH: known,
|
||||
REA_GHIDRA_PROGRAM_NAME: program,
|
||||
};
|
||||
const unknownEnv = {
|
||||
...env,
|
||||
REA_GHIDRA_PROJECT_PATH: unknown,
|
||||
REA_GHIDRA_PROGRAM_NAME: program,
|
||||
};
|
||||
const mismatch = await selectedProjectMcp({
|
||||
entrypoint,
|
||||
env: knownEnv,
|
||||
first: different,
|
||||
matching: targetPath,
|
||||
expected: "import_identity_mismatch",
|
||||
});
|
||||
const unavailable = await selectedProjectMcp({
|
||||
entrypoint,
|
||||
env: unknownEnv,
|
||||
first: targetPath,
|
||||
expected: "import_identity_unavailable",
|
||||
});
|
||||
for (const [label, selectedEnv, input, expected] of [
|
||||
["mismatch", knownEnv, different, "import_identity_mismatch"],
|
||||
["unavailable", unknownEnv, targetPath, "import_identity_unavailable"],
|
||||
]) {
|
||||
let failed;
|
||||
try {
|
||||
await exec(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"decompile",
|
||||
input,
|
||||
"0x0",
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
{ env: selectedEnv, timeout: 180000, maxBuffer: 8 * 1024 * 1024 },
|
||||
);
|
||||
} catch (cause) {
|
||||
assert.equal(cause.code, 1, `${label} CLI exit`);
|
||||
failed = JSON.parse(cause.stdout);
|
||||
}
|
||||
assert.ok(failed, `${label} CLI must refuse`);
|
||||
assert.equal(failed.code, "configuration_invalid");
|
||||
assert.equal(failed.details.diagnostics.remote_code, expected);
|
||||
}
|
||||
assert.deepEqual(
|
||||
await projectState([known, unknown, targetPath, different]),
|
||||
before,
|
||||
);
|
||||
assert.deepEqual(
|
||||
(await readdir(env.TMPDIR))
|
||||
.filter((name) => name.startsWith("rea-ghidra-"))
|
||||
.sort(),
|
||||
runtimeRootsBefore,
|
||||
"Selected-project checks left a Ghidra runtime directory",
|
||||
);
|
||||
return {
|
||||
status: "verified",
|
||||
mismatch,
|
||||
unavailable,
|
||||
cli_refusals: true,
|
||||
source_files_unchanged: true,
|
||||
};
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function selectedProjectMcp({
|
||||
entrypoint,
|
||||
env,
|
||||
first,
|
||||
matching,
|
||||
expected,
|
||||
}) {
|
||||
const client = new Client({
|
||||
name: "rea-project-identity-verifier",
|
||||
version: "1",
|
||||
});
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const tools = new Map(
|
||||
(await client.listTools()).tools.map((tool) => [tool.name, tool]),
|
||||
);
|
||||
const call = (name, arguments_) =>
|
||||
client.callTool(
|
||||
{ name, arguments: arguments_ },
|
||||
{ timeout: 180000, toolDefinition: tools.get(name) },
|
||||
);
|
||||
assert.notEqual(
|
||||
(await call("open_binary", { path: first, provider_id: "ghidra" }))
|
||||
.isError,
|
||||
true,
|
||||
);
|
||||
const rejected = requireMcpToolError(await call("list_documents", {}));
|
||||
assert.equal(
|
||||
rejected.code,
|
||||
"configuration_invalid",
|
||||
JSON.stringify({
|
||||
code: rejected.code,
|
||||
message: rejected.message,
|
||||
failure_kind: rejected.details?.diagnostics?.failure_kind,
|
||||
failure_message: rejected.details?.diagnostics?.failure_message,
|
||||
stdout: rejected.details?.diagnostics?.stdout?.text?.slice(-2500),
|
||||
stderr: rejected.details?.diagnostics?.stderr?.text?.slice(-1000),
|
||||
}),
|
||||
);
|
||||
assert.equal(rejected.details.diagnostics.remote_code, expected);
|
||||
if (matching !== undefined) {
|
||||
assert.notEqual(
|
||||
(await call("open_binary", { path: matching, provider_id: "ghidra" }))
|
||||
.isError,
|
||||
true,
|
||||
);
|
||||
assert.notEqual((await call("list_documents", {})).isError, true);
|
||||
}
|
||||
assert.notEqual((await call("close_binary", {})).isError, true);
|
||||
return {
|
||||
refused: expected,
|
||||
matching_target_reopened: matching !== undefined,
|
||||
};
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function projectState(paths) {
|
||||
const files = [];
|
||||
const walk = async (dir) => {
|
||||
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) await walk(path);
|
||||
else if (entry.isFile()) files.push(path);
|
||||
}
|
||||
};
|
||||
for (const path of paths) {
|
||||
files.push(path);
|
||||
if (path.endsWith(".gpr")) await walk(`${path.slice(0, -4)}.rep`);
|
||||
}
|
||||
files.sort();
|
||||
return Promise.all(
|
||||
files.map(async (path) => [
|
||||
path,
|
||||
createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex"),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
export function applyReleasePleasePatch(directory?: string): Promise<void>;
|
||||
|
||||
export function describeReleaseProposalError(error: unknown): string;
|
||||
|
||||
export function releaseProposalSearchDepth(
|
||||
run: (command: string, args: string[]) => Promise<string>,
|
||||
sourceSha: string,
|
||||
|
||||
@@ -13,6 +13,15 @@ const patch = fileURLToPath(
|
||||
new URL("../../patches/release-please+17.3.0.patch", import.meta.url),
|
||||
);
|
||||
|
||||
export function describeReleaseProposalError(error) {
|
||||
if (!(error instanceof Error)) return String(error);
|
||||
const description = error.message || error.name || "Unknown error";
|
||||
const status = error.status;
|
||||
return Number.isInteger(status) && status >= 400 && status <= 599
|
||||
? `${description} (HTTP ${status})`
|
||||
: description;
|
||||
}
|
||||
|
||||
export async function applyReleasePleasePatch(
|
||||
directory = dirname(require.resolve("release-please/package.json")),
|
||||
) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import xml.etree.ElementTree as ET
|
||||
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
||||
|
||||
from website import PageMetadata, page_url
|
||||
from website_agents import prepare_discovery, prepare_markdown
|
||||
|
||||
|
||||
EXAMPLE_FILES = (
|
||||
@@ -81,6 +82,8 @@ def prepare_assets(site):
|
||||
prepare_sharing_image(site)
|
||||
prepare_sitemap(site)
|
||||
prepare_notes_example(site)
|
||||
prepare_discovery(Path(__file__).resolve().parent.parent, site)
|
||||
prepare_markdown(site)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+2
-9
@@ -70,15 +70,8 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
|
||||
guardClosedStdout();
|
||||
// Doctor runs outside Incur, so handle its global timing flag here. Keep a
|
||||
// token consumed by --format as a value, even when it looks like a flag.
|
||||
const outputValues = new Set(
|
||||
builtinArguments
|
||||
.splitIncurBuiltinArguments(doctorArguments)
|
||||
.outputValues.map(({ index }) => index),
|
||||
);
|
||||
const doctorCliArguments = doctorArguments.filter(
|
||||
(argument, index) => argument !== "--timings" || outputValues.has(index),
|
||||
);
|
||||
const timingRequested = doctorCliArguments.length !== doctorArguments.length;
|
||||
const { arguments: doctorCliArguments, enabled: timingRequested } =
|
||||
builtinArguments.splitCliTimingArguments(doctorArguments);
|
||||
const started = performance.now();
|
||||
try {
|
||||
const { runProductionMcpDoctorCli } =
|
||||
|
||||
@@ -15,12 +15,16 @@ const [
|
||||
{ parseConfig },
|
||||
{ silentLogger },
|
||||
{ PROMPT_CONTRACTS },
|
||||
{ TOOL_CONTRACTS },
|
||||
{ getEvidenceInputContract },
|
||||
] = await Promise.all([
|
||||
read("server/createServer.js"),
|
||||
read("composition/binary.js"),
|
||||
read("config/parseConfig.js"),
|
||||
read("logger.js"),
|
||||
read("contracts/promptContracts.js"),
|
||||
read("contracts/toolContracts.js"),
|
||||
read("domain/evidenceInputContract.js"),
|
||||
]);
|
||||
const recordCatalog = async (profile) => {
|
||||
// Catalog generation is independent of the host account and user configuration.
|
||||
@@ -71,6 +75,11 @@ const recordCatalog = async (profile) => {
|
||||
instructions: client.getInstructions(),
|
||||
identity: server.catalogIdentity,
|
||||
tools,
|
||||
evidenceReferenceAlternatives: TOOL_CONTRACTS.filter(
|
||||
(contract) =>
|
||||
getEvidenceInputContract(contract.inputSchema)
|
||||
.hasEvidenceReferenceAlternative,
|
||||
).map((contract) => contract.name),
|
||||
prompts,
|
||||
};
|
||||
const encoded = JSON.stringify(catalog);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { appendFile } from "node:fs/promises";
|
||||
import { parseArgs, promisify } from "node:util";
|
||||
import {
|
||||
applyReleasePleasePatch,
|
||||
describeReleaseProposalError,
|
||||
releaseProposalSearchDepth,
|
||||
} from "./lib/release-proposal.mjs";
|
||||
|
||||
@@ -63,7 +64,7 @@ async function main() {
|
||||
|
||||
await main().catch((error) => {
|
||||
process.stderr.write(
|
||||
`Release proposal failed: ${error instanceof Error ? error.message : String(error)}\n`,
|
||||
`Release proposal failed: ${describeReleaseProposalError(error)}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Check the public build artifacts and Markdown content contract."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from zipfile import ZipFile
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
import yaml
|
||||
|
||||
from website_agents import prepare_discovery, prepare_markdown
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
class AgentPublishingChecks(unittest.TestCase):
|
||||
def test_published_skill_digest_covers_every_reference_and_catalogs_resolve(self):
|
||||
site = ROOT / "website/_site"
|
||||
index = json.loads((site / ".well-known/agent-skills/index.json").read_text())
|
||||
self.assertEqual(index["$schema"], "https://schemas.agentskills.io/discovery/0.2.0/schema.json")
|
||||
skill, = index["skills"]
|
||||
self.assertEqual(skill["name"], "reverse-engineer-anything")
|
||||
self.assertEqual(skill["type"], "archive")
|
||||
artifact = site / skill["url"].removeprefix("https://rea.tools/")
|
||||
self.assertEqual(skill["digest"], "sha256:" + hashlib.sha256(artifact.read_bytes()).hexdigest())
|
||||
source = ROOT / ".agents/skills/reverse-engineer-anything"
|
||||
with ZipFile(artifact) as archive:
|
||||
expected = {f"{source.name}/{p.relative_to(source).as_posix()}": p.read_bytes()
|
||||
for p in source.rglob("*.md")}
|
||||
self.assertEqual(set(archive.namelist()), set(expected))
|
||||
for name, data in expected.items():
|
||||
self.assertEqual(archive.read(name), data)
|
||||
manifest = archive.read(f"{source.name}/SKILL.md").decode()
|
||||
metadata = yaml.safe_load(manifest[4:].split("\n---\n", 1)[0])
|
||||
self.assertEqual(skill["description"], metadata["description"])
|
||||
catalog = json.loads((site / ".well-known/ai-catalog.json").read_text())
|
||||
self.assertEqual(catalog["specVersion"], "1.0")
|
||||
entry, = catalog["entries"]
|
||||
self.assertEqual(entry["identifier"], "urn:air:rea.tools:skill:reverse-engineer-anything")
|
||||
self.assertEqual(entry["url"], skill["url"])
|
||||
self.assertNotIn("data", entry)
|
||||
ard = json.loads((site / ".well-known/ard.json").read_text())
|
||||
resource, = ard["entries"]
|
||||
self.assertEqual(resource["url"], skill["url"])
|
||||
self.assertTrue(2 <= len(resource["representativeQueries"]) <= 5)
|
||||
|
||||
def test_reference_changes_invalidate_digest_and_yaml_scalars_are_parsed(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
source = root / ".agents/skills/reverse-engineer-anything"
|
||||
source.mkdir(parents=True)
|
||||
(source / "SKILL.md").write_text(
|
||||
'---\nname: reverse-engineer-anything\ndescription: >-\n Inspect programs:\n preserve evidence.\n---\n# REA\n'
|
||||
)
|
||||
(source / "references").mkdir()
|
||||
reference = source / "references/evidence.md"
|
||||
reference.write_text("First revision")
|
||||
site = root / "site"
|
||||
def build():
|
||||
prepare_discovery(root, site)
|
||||
return json.loads((site / ".well-known/agent-skills/index.json").read_text())["skills"][0]
|
||||
before = build()
|
||||
self.assertEqual(before["description"], "Inspect programs: preserve evidence.")
|
||||
self.assertEqual(build(), before)
|
||||
reference.write_text("Second revision")
|
||||
self.assertNotEqual(build()["digest"], before["digest"])
|
||||
|
||||
def test_markdown_preserves_content_and_resources_without_publishing_excluded_pages(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
site = Path(directory)
|
||||
page = site / "zh-CN/guides/test/index.html"
|
||||
page.parent.mkdir(parents=True)
|
||||
page.write_text('''<html><head><title>REA: test</title></head><body>
|
||||
<nav>Outside navigation</nav><main><aside>Shortened display paths.</aside>
|
||||
<h1>Inspect a program</h1><nav>Contents navigation</nav>
|
||||
<ol><li><span class="flow-label">Read</span><span class="flow-node"><code>input.exe</code></span><span class="flow-note">Keep evidence.</span></li></ol>
|
||||
<p><a href="../other/?q=a%2Fb#code">Another guide</a></p>
|
||||
<pre><code>const value = a < b ? "x_y" : "&";\n keepIndent();</code></pre>
|
||||
<details><summary>Evidence</summary><p>Hidden evidence remains available.</p></details>
|
||||
<img src="../../../assets/diagram.svg" alt="Investigation diagram">
|
||||
<button>Copy code</button><script>doNotPublish()</script></main></body></html>''')
|
||||
fixture = site / "examples/notes-web/index.html"
|
||||
fixture.parent.mkdir(parents=True)
|
||||
fixture.write_text("<main>Target application</main>")
|
||||
fallback = site / "zh-CN/fallback/index.html"
|
||||
fallback.parent.mkdir(parents=True)
|
||||
fallback.write_text('<head><meta name="robots" content="noindex, follow"></head><main>English fallback notice.</main>')
|
||||
prepare_markdown(site)
|
||||
markdown = page.with_suffix(".md").read_text()
|
||||
self.assertIn('# Inspect a program', markdown)
|
||||
self.assertIn('const value = a < b ? "x_y" : "&";\n keepIndent();', markdown)
|
||||
self.assertIn("Hidden evidence remains available.", markdown)
|
||||
self.assertIn("Shortened display paths.", markdown)
|
||||
self.assertNotIn("Read`input.exe`", markdown)
|
||||
self.assertIn("https://rea.tools/zh-CN/guides/other/?q=a%2Fb#code", markdown)
|
||||
self.assertIn("https://rea.tools/assets/diagram.svg", markdown)
|
||||
for text in ("Outside navigation", "Contents navigation", "Copy code", "doNotPublish"):
|
||||
self.assertNotIn(text, markdown)
|
||||
self.assertFalse(fixture.with_suffix(".md").exists())
|
||||
self.assertFalse(fallback.with_suffix(".md").exists())
|
||||
html = BeautifulSoup(page.read_text(), "html.parser")
|
||||
self.assertEqual(html.find("link", rel="alternate")["href"], "https://rea.tools/zh-CN/guides/test/index.md")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -38,15 +38,16 @@ if (
|
||||
!isAbsolute(strace ?? "")
|
||||
)
|
||||
throw new Error(
|
||||
"verify:binary:layout requires Linux x64, absolute REA_PWNTOOLS_PYTHON (pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2) and REA_VERIFY_STRACE_COMMAND. gcc, ld, strip and the i686/ARM/MIPS/MIPSel linux-gnu cross gcc and readelf must be available. No target is executed.",
|
||||
"verify:binary:layout requires Linux x64, absolute REA_PWNTOOLS_PYTHON (pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2) and REA_VERIFY_STRACE_COMMAND. gcc, ld, strip and the AArch64/i686/ARM/MIPS/MIPSel linux-gnu cross gcc and readelf must be available. No target is executed.",
|
||||
);
|
||||
const run = createVerifierRun();
|
||||
const execute = promisify(execFile);
|
||||
const ELF32_TOOLCHAINS = [
|
||||
["i686-linux-gnu", "EM_386", "little"],
|
||||
["arm-linux-gnueabi", "EM_ARM", "little"],
|
||||
["mips-linux-gnu", "EM_MIPS", "big"],
|
||||
["mipsel-linux-gnu", "EM_MIPS", "little"],
|
||||
const CROSS_TOOLCHAINS = [
|
||||
["aarch64-linux-gnu", "EM_AARCH64", 64, "little"],
|
||||
["i686-linux-gnu", "EM_386", 32, "little"],
|
||||
["arm-linux-gnueabi", "EM_ARM", 32, "little"],
|
||||
["mips-linux-gnu", "EM_MIPS", 32, "big"],
|
||||
["mipsel-linux-gnu", "EM_MIPS", 32, "little"],
|
||||
];
|
||||
for (const command of [
|
||||
"gcc",
|
||||
@@ -54,7 +55,7 @@ for (const command of [
|
||||
"strip",
|
||||
"/usr/bin/prlimit",
|
||||
strace,
|
||||
...ELF32_TOOLCHAINS.flatMap(([prefix]) => [
|
||||
...CROSS_TOOLCHAINS.flatMap(([prefix]) => [
|
||||
`${prefix}-gcc`,
|
||||
`${prefix}-readelf`,
|
||||
]),
|
||||
@@ -347,14 +348,15 @@ try {
|
||||
cases++;
|
||||
}
|
||||
}
|
||||
// ELF32 profiles: cross-compiled, never executed, checked against GNU readelf.
|
||||
// Cross profiles: cross-compiled, never executed, checked against GNU readelf.
|
||||
for (const [
|
||||
prefix,
|
||||
machine,
|
||||
bits,
|
||||
byteOrder,
|
||||
kind,
|
||||
flags,
|
||||
] of ELF32_TOOLCHAINS.flatMap((toolchain) => [
|
||||
] of CROSS_TOOLCHAINS.flatMap((toolchain) => [
|
||||
[...toolchain, "executable", ["-fPIE", "-pie"]],
|
||||
[...toolchain, "relocatable", ["-c", "-fPIC"]],
|
||||
])) {
|
||||
@@ -423,16 +425,18 @@ try {
|
||||
`readelf relocation count: ${name}`,
|
||||
);
|
||||
for (const [entryIndex, [, offset, info]] of rows.entries()) {
|
||||
// ELF32 r_info uses the low byte for type and the upper 24 bits for
|
||||
// the symbol index. readelf has already decoded the file's byte order.
|
||||
// ELF32 r_info packs an 8-bit type under a 24-bit symbol index;
|
||||
// ELF64 packs a 32-bit type under a 32-bit index. readelf has already
|
||||
// decoded the file's byte order.
|
||||
const decodedInfo = BigInt(`0x${info}`);
|
||||
const typeBits = bits === 32 ? 8n : 32n;
|
||||
expectedRelocations.push({
|
||||
section_index: section.index,
|
||||
entry_index: entryIndex,
|
||||
reported_offset: BigInt(`0x${offset}`),
|
||||
type: Number(decodedInfo & 0xffn),
|
||||
type: Number(decodedInfo & ((1n << typeBits) - 1n)),
|
||||
symbol_table_index: section.link,
|
||||
symbol_index: Number(decodedInfo >> 8n),
|
||||
symbol_index: Number(decodedInfo >> typeBits),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -445,7 +449,7 @@ try {
|
||||
const value = await inspect(mode, path);
|
||||
assert.deepEqual(value.architecture, {
|
||||
machine,
|
||||
bits: 32,
|
||||
bits,
|
||||
byte_order: byteOrder,
|
||||
});
|
||||
assert.equal(
|
||||
@@ -1312,7 +1316,8 @@ try {
|
||||
}
|
||||
}
|
||||
}
|
||||
unsupported.writeUInt16LE(183, 18);
|
||||
// EM_RISCV: AArch64 (183) is now an admitted profile.
|
||||
unsupported.writeUInt16LE(243, 18);
|
||||
const core = Buffer.from(object);
|
||||
core.writeUInt16LE(4, 16);
|
||||
for (const [name, bytes, category] of [
|
||||
@@ -1337,7 +1342,7 @@ try {
|
||||
"invalid_input",
|
||||
],
|
||||
["undersized-program-header", undersizedProgramHeader, "invalid_input"],
|
||||
["arm64", unsupported, "unsupported_target"],
|
||||
["riscv64", unsupported, "unsupported_target"],
|
||||
["core", core, "unsupported_target"],
|
||||
]) {
|
||||
const path = join(root.path, name);
|
||||
|
||||
@@ -245,7 +245,13 @@ try {
|
||||
scope,
|
||||
{ environment: process.env },
|
||||
);
|
||||
if (mismatch.ok || !mismatch.error.message.includes("target-mismatch"))
|
||||
// A selection failure carries its helper code in the issue, not in its message.
|
||||
if (
|
||||
mismatch.ok ||
|
||||
!mismatch.error.issues?.some(({ message }) =>
|
||||
message.startsWith("target-mismatch:"),
|
||||
)
|
||||
)
|
||||
throw new Error("Real helper did not reject changed target bytes");
|
||||
report = {
|
||||
ok: true,
|
||||
|
||||
@@ -161,6 +161,38 @@ try {
|
||||
fromNumericOffset.normalized_result,
|
||||
fromOffset.normalized_result,
|
||||
);
|
||||
const missingProcedure = "rea_missing_procedure_fixture";
|
||||
const missingName = await client.callTool(
|
||||
{
|
||||
name: "procedure_pseudo_code",
|
||||
arguments: { procedure: missingProcedure },
|
||||
},
|
||||
{ timeout: 240000 },
|
||||
);
|
||||
assert.equal(missingName.isError, true);
|
||||
const missingNameError = JSON.parse(
|
||||
missingName.content.find((item) => item.type === "text").text,
|
||||
).error;
|
||||
assert.equal(missingNameError.code, "invalid_request");
|
||||
assert.match(missingNameError.message, /rea_missing_procedure_fixture/u);
|
||||
assert.match(missingNameError.remediation.action, /list_procedures/u);
|
||||
assert.match(missingNameError.remediation.action, /does not prove/u);
|
||||
assert.equal(missingNameError.recovery, undefined);
|
||||
const unmappedOffset = await client.callTool(
|
||||
{
|
||||
name: "procedure_pseudo_code",
|
||||
arguments: { procedure: "file-offset:0x1fffffffffffff" },
|
||||
},
|
||||
{ timeout: 240000 },
|
||||
);
|
||||
assert.equal(unmappedOffset.isError, true);
|
||||
const unmappedError = JSON.parse(
|
||||
unmappedOffset.content.find((item) => item.type === "text").text,
|
||||
).error;
|
||||
assert.equal(unmappedError.code, "invalid_request");
|
||||
assert.match(unmappedError.message, /no loaded source mapping/u);
|
||||
assert.match(unmappedError.remediation.action, /inspect_native_load_image/u);
|
||||
assert.equal(unmappedError.recovery, undefined);
|
||||
const missingAddress = "ram:0xffffffff";
|
||||
const rejected = await client.callTool(
|
||||
{ name: "procedure_pseudo_code", arguments: { procedure: missingAddress } },
|
||||
|
||||
@@ -276,6 +276,67 @@ try {
|
||||
assert.equal(updated.annotations.inline_comment, changes.inline_comment);
|
||||
assert.equal(updated.dossier.procedure.name, changes.name);
|
||||
assert.ok(updated.dossier.pseudocode.includes(changes.name));
|
||||
// Exercise the documented recovery through the public MCP catalog on real
|
||||
// Ghidra, rather than assuming the provider exposes direct comment methods.
|
||||
const contextCall = async (name, args) => {
|
||||
const definition = tools.find((tool) => tool.name === name);
|
||||
assert.ok(definition?.outputSchema, `${name} must be advertised`);
|
||||
const response = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
{ timeout: 240000, toolDefinition: definition },
|
||||
);
|
||||
return requireMcpOutput(response, name);
|
||||
};
|
||||
const context = (
|
||||
await contextCall("inspect_address_context", {
|
||||
address: fixture.entry,
|
||||
})
|
||||
).result;
|
||||
assert.equal(context.procedure.state, "available");
|
||||
assert.equal(context.procedure.value.found, true);
|
||||
for (const facet of [context.comment, context.inline_comment]) {
|
||||
assert.equal(facet.state, "unavailable");
|
||||
assert.match(
|
||||
facet.remediation,
|
||||
/analyze_function.*parent_evidence_id.*inspect_analysis_view/u,
|
||||
);
|
||||
}
|
||||
assert.match(context.bookmarks.remediation, /binary_session/u);
|
||||
const summary = parseEvidence(
|
||||
await contextCall("analyze_function", {
|
||||
procedure: context.procedure.value.procedure.address,
|
||||
}),
|
||||
).normalized_result;
|
||||
const comments = parseEvidence(
|
||||
await contextCall("inspect_analysis_view", {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: summary.parent_evidence_id,
|
||||
},
|
||||
view: { kind: "native", facet: "comments" },
|
||||
}),
|
||||
).normalized_result;
|
||||
for (const [kind, text] of [
|
||||
["comment", changes.comment],
|
||||
["inline", changes.inline_comment],
|
||||
])
|
||||
assert.ok(
|
||||
comments.item.some(
|
||||
(item) =>
|
||||
item.address === context.procedure.value.query_address &&
|
||||
item.kind === kind &&
|
||||
item.text === text,
|
||||
),
|
||||
);
|
||||
const outside = (
|
||||
await contextCall("inspect_address_context", {
|
||||
address: unlistedAddress,
|
||||
})
|
||||
).result;
|
||||
assert.equal(outside.procedure.state, "available");
|
||||
assert.equal(outside.procedure.value.found, false);
|
||||
assert.equal(outside.comment.state, "unavailable");
|
||||
assert.match(outside.comment.remediation, /binary_session/u);
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure: changes.name }),
|
||||
fixture.entry,
|
||||
@@ -386,6 +447,7 @@ try {
|
||||
fresh_decompiler: true,
|
||||
clear_comments: true,
|
||||
cli_mcp_parity: true,
|
||||
address_context_comment_recovery: true,
|
||||
},
|
||||
context_input_errors_preserved: true,
|
||||
original_unchanged: true,
|
||||
|
||||
@@ -619,7 +619,35 @@ export async function verifyNativeTypeLayout(client, typeNames) {
|
||||
)
|
||||
)
|
||||
throw new Error("Nested union or enum layout drifted");
|
||||
return { layout, payload, state };
|
||||
const bitsSymbol = typeNames.find((item) =>
|
||||
matchesSymbol(item.value, "rea_ghidra_inventory_bits"),
|
||||
);
|
||||
if (bitsSymbol === undefined)
|
||||
throw new Error("Bit-field fixture symbol unavailable");
|
||||
const bits = await functionCall(client, "inspect_native_data_type", {
|
||||
document: null,
|
||||
address: bitsSymbol.address,
|
||||
});
|
||||
const low = bits.fields.find((field) => field.name === "low");
|
||||
if (
|
||||
bits.kind !== "struct" ||
|
||||
low?.bit_size !== 3 ||
|
||||
low.bit_field?.declared_bit_size !== 3 ||
|
||||
low.bit_field.component_type_lookup !== "not-found" ||
|
||||
low.bit_field.base_type_lookup !== "equivalent"
|
||||
)
|
||||
throw new Error(`Bit-field identity unavailable: ${JSON.stringify(bits)}`);
|
||||
const component = await functionCall(client, "inspect_native_data_type", {
|
||||
document: null,
|
||||
type: low.type_id,
|
||||
});
|
||||
const base = await functionCall(client, "inspect_native_data_type", {
|
||||
document: null,
|
||||
type: low.bit_field.base_type_id,
|
||||
});
|
||||
if (component.status !== "unavailable" || base.status !== "available")
|
||||
throw new Error("Bit-field component or base lookup drifted");
|
||||
return { layout, payload, state, bits, base };
|
||||
}
|
||||
|
||||
export async function verifyRelativeSwitch(client, procedures, entrySize) {
|
||||
|
||||
@@ -46,8 +46,40 @@ async function expectRemoteFailure(client, operation, parameters, code) {
|
||||
return { operation, code };
|
||||
}
|
||||
|
||||
async function verifyBridgeRejections(client) {
|
||||
async function verifyBridgeRejections(client, names) {
|
||||
const global = names.find((item) =>
|
||||
matchesSymbol(item.value, "rea_ghidra_inventory_global"),
|
||||
);
|
||||
if (global === undefined)
|
||||
throw new Error("Ghidra initialized global probe is unavailable");
|
||||
const mapping = await call(client, "address_to_file_offset", {
|
||||
document: null,
|
||||
address: global.address,
|
||||
});
|
||||
if (!Number.isSafeInteger(mapping.file_offset))
|
||||
throw new Error("Ghidra initialized global has no exact file offset");
|
||||
return {
|
||||
unknownProcedure: await expectRemoteFailure(
|
||||
client,
|
||||
"procedure_address",
|
||||
{ document: null, procedure: "rea_missing_procedure_fixture" },
|
||||
"procedure_not_found",
|
||||
),
|
||||
mappedDataOffset: await expectRemoteFailure(
|
||||
client,
|
||||
"procedure_pseudo_code",
|
||||
{
|
||||
document: null,
|
||||
procedure: `file-offset:0x${mapping.file_offset.toString(16)}`,
|
||||
},
|
||||
"procedure_not_found",
|
||||
),
|
||||
unmappedFileOffset: await expectRemoteFailure(
|
||||
client,
|
||||
"procedure_pseudo_code",
|
||||
{ document: null, procedure: "file-offset:0x1fffffffffffff" },
|
||||
"file_offset_unmapped",
|
||||
),
|
||||
unknownDocument: await expectRemoteFailure(
|
||||
client,
|
||||
"list_segments",
|
||||
@@ -86,7 +118,7 @@ export async function verifyInventoryOperations({
|
||||
});
|
||||
|
||||
const rejections =
|
||||
variant === "debug" ? await verifyBridgeRejections(client) : null;
|
||||
variant === "debug" ? await verifyBridgeRejections(client, names) : null;
|
||||
const stringSearch = await call(client, "search_strings", {
|
||||
pattern: String.raw`^REA_GHIDRA_(?:INVENTORY_ENTRY|LEAF_VALUE)$`,
|
||||
mode: "regex",
|
||||
|
||||
@@ -6,12 +6,13 @@ import {
|
||||
mkdtemp,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
realpath,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { basename, join, relative } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
@@ -41,8 +42,10 @@ import {
|
||||
import {
|
||||
requireMcpToolError,
|
||||
requireMcpOperationResult,
|
||||
requireMcpOutput,
|
||||
} from "./lib/mcp-verifier-results.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { runOwnedCommand } from "../dist/process/OwnedCommand.js";
|
||||
|
||||
// Optional cross-target lane. No tool acquisition and no target execution.
|
||||
const mode = process.argv[2];
|
||||
@@ -90,6 +93,114 @@ for (const setting of ["GHIDRA_HEADLESS_MAXMEM", "GHIDRA_MAXMEM"]) {
|
||||
if (process.env[setting] !== undefined) env[setting] = process.env[setting];
|
||||
}
|
||||
|
||||
/** Observe an existing project, and verify REA never alters the caller's copy. */
|
||||
async function verifyExistingMipsProject(fixture, byteOrder) {
|
||||
const projectDir = join(workspace, `${byteOrder}-existing-project`);
|
||||
await mkdir(projectDir);
|
||||
assert.ok(installation.analyzeHeadlessPath);
|
||||
await runOwnedCommand(
|
||||
{
|
||||
command: installation.analyzeHeadlessPath,
|
||||
arguments: [
|
||||
projectDir,
|
||||
"ExistingMips",
|
||||
"-import",
|
||||
fixture.path,
|
||||
"-noanalysis",
|
||||
],
|
||||
env,
|
||||
runId: run.run_id,
|
||||
expectedCommand: null,
|
||||
},
|
||||
{ timeoutMs: 180000, diagnosticBytes: 1024 * 1024 },
|
||||
);
|
||||
const projectPath = join(projectDir, "ExistingMips.gpr");
|
||||
const digestTree = async () => {
|
||||
const paths = [projectPath];
|
||||
const walk = async (dir) => {
|
||||
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) await walk(path);
|
||||
else if (entry.isFile()) paths.push(path);
|
||||
}
|
||||
};
|
||||
await walk(join(projectDir, "ExistingMips.rep"));
|
||||
return Promise.all(
|
||||
paths.sort().map(async (path) => [
|
||||
relative(projectDir, path),
|
||||
createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex"),
|
||||
]),
|
||||
);
|
||||
};
|
||||
const before = await digestTree();
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env: {
|
||||
...env,
|
||||
REA_GHIDRA_PROJECT_PATH: projectPath,
|
||||
REA_GHIDRA_PROGRAM_NAME: basename(fixture.path),
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "rea-existing-mips-proof", version: "1" });
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const opened = await client.callTool(
|
||||
{
|
||||
name: "open_binary",
|
||||
arguments: { path: fixture.path, provider_id: "ghidra" },
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
requireMcpOperationResult(opened, "open_binary");
|
||||
const result = await client.callTool(
|
||||
{ name: "list_procedures", arguments: {} },
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
const observation = parseEvidence(
|
||||
requireMcpOutput(result, "list_procedures"),
|
||||
);
|
||||
assert.ok(Array.isArray(observation.normalized_result));
|
||||
const parameters = observation.analysis_profile?.parameters;
|
||||
assert.equal(parameters?.import_mode, "existing-project-private-copy-v1");
|
||||
assert.equal(parameters?.mips_elf?.byte_order, byteOrder);
|
||||
assert.equal(parameters?.mips_support_lane, undefined);
|
||||
assert.deepEqual(parameters?.original_analysis, {
|
||||
completeness: "unknown",
|
||||
settings: "unknown",
|
||||
});
|
||||
requireMcpOperationResult(
|
||||
await client.callTool({ name: "close_binary", arguments: {} }),
|
||||
"close_binary",
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
assert.deepEqual(
|
||||
await digestTree(),
|
||||
before,
|
||||
"REA changed the source project",
|
||||
);
|
||||
assert.equal(
|
||||
createHash("sha256")
|
||||
.update(await readFile(fixture.path))
|
||||
.digest("hex"),
|
||||
fixture.sha256,
|
||||
);
|
||||
return {
|
||||
byte_order: byteOrder,
|
||||
project_unchanged: true,
|
||||
target_unchanged: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed float signatures must use soft-float o32 storage, and callers must see
|
||||
* the source constants through it rather than FPU inputs.
|
||||
@@ -320,6 +431,10 @@ try {
|
||||
);
|
||||
}
|
||||
assert.equal(resolved.value.sha256, fixture.sha256);
|
||||
const existingProject =
|
||||
!psp && floatAbi === "hard"
|
||||
? await verifyExistingMipsProject(fixture, byteOrder)
|
||||
: undefined;
|
||||
const snapshotPath = join(
|
||||
workspace,
|
||||
`${byteOrder}-${floatAbi}.snapshot.json`,
|
||||
@@ -682,6 +797,9 @@ try {
|
||||
}
|
||||
reports.push({
|
||||
...fixture,
|
||||
...(existingProject === undefined
|
||||
? {}
|
||||
: { existing_project: existingProject }),
|
||||
independent_reader: independent,
|
||||
effective_language: image.observations.language_id,
|
||||
effective_compiler_spec: image.observations.compiler_spec_id,
|
||||
|
||||
Loaded 100 of 601 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user