mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
Compare commits
75
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5f9874cfe | ||
|
|
5f588485fe | ||
|
|
768a4c169d | ||
|
|
b55b0c317b | ||
|
|
7c1bd003ec | ||
|
|
22b5914c3d | ||
|
|
8c857ce56c | ||
|
|
28bffe92c8 | ||
|
|
4f7c4cc218 | ||
|
|
4b961e3715 | ||
|
|
8db538a7ca | ||
|
|
e01fcfb4b2 | ||
|
|
f5cfe09c07 | ||
|
|
f59d884f35 | ||
|
|
4f4e2552cc | ||
|
|
b3e22620a5 | ||
|
|
53fca98e3a | ||
|
|
5ed283d168 | ||
|
|
f251009e0a | ||
|
|
d892e7861a | ||
|
|
9ebbe67e05 | ||
|
|
b912ec184c | ||
|
|
193e01abb1 | ||
|
|
d496d803a4 | ||
|
|
6259ac41a4 | ||
|
|
79766ca96a | ||
|
|
22c493ad5c | ||
|
|
b7911b2d30 | ||
|
|
6442ea02cb | ||
|
|
eef8f44003 | ||
|
|
510e83eadc | ||
|
|
2d11fd92d2 | ||
|
|
9bea563e30 | ||
|
|
e749160ab0 | ||
|
|
920d634e83 | ||
|
|
55078b9f57 | ||
|
|
75c3cab540 | ||
|
|
3c05b4d3fe | ||
|
|
2cb29693c3 | ||
|
|
26c3e9e413 | ||
|
|
144843ccc2 | ||
|
|
4a01211f11 | ||
|
|
6a0ccf7eeb | ||
|
|
710fad8acf | ||
|
|
93b8b82db7 | ||
|
|
3f4aff2f22 | ||
|
|
8b89eee767 | ||
|
|
59f5502a40 | ||
|
|
2ac6d3efe2 | ||
|
|
365c3245e8 | ||
|
|
aba56df689 | ||
|
|
dec8d2d588 | ||
|
|
0e6943c9da | ||
|
|
20a282a181 | ||
|
|
f95bdbce76 | ||
|
|
8250bf2320 | ||
|
|
00c4f44a1c | ||
|
|
d3ab0faf52 | ||
|
|
6d60db1951 | ||
|
|
0354145a2a | ||
|
|
7bcefead69 | ||
|
|
ce9095d727 | ||
|
|
46c13b9981 | ||
|
|
85013da0ce | ||
|
|
cbc69537cd | ||
|
|
eb39a07966 | ||
|
|
4b840aa1fb | ||
|
|
423059a4de | ||
|
|
44b6384009 | ||
|
|
3f61303939 | ||
|
|
35ae7232bf | ||
|
|
76cd23d2e8 | ||
|
|
3e6a5cb20a | ||
|
|
1d343a88e5 | ||
|
|
df25bc8068 |
No files matched your search
@@ -377,8 +377,9 @@ unless the group overrides them. The grouped name union is the complete tool
|
||||
inventory; use `tools/list` for canonical tool schemas. The default summary keeps
|
||||
each provider candidate's availability and target-support status, code and
|
||||
reason; `omitted_fields` lists what it leaves out. Request `{"detail": "complete"}`
|
||||
only when you need provider diagnostics, candidate capability sets, or tool
|
||||
surface, effect and annotation flags; in those closed flag vocabularies, listed
|
||||
only when you need an unavailable candidate's remediation, provider
|
||||
diagnostics, candidate capability sets, or tool surface, effect and annotation
|
||||
flags; in those closed flag vocabularies, listed
|
||||
flags are true and omitted known flags are false. Provider candidate facts
|
||||
describe possible providers, while top-level session capabilities describe the
|
||||
current route and `analysis_provider_binding` names the selected provider. Do
|
||||
@@ -431,7 +432,10 @@ output also streams without a single MCP frame.
|
||||
When the retained parent ID is unknown, use `get_evidence_bundle` with
|
||||
`detail: "summary"` and exact filters for operation, target SHA-256,
|
||||
analysis-profile digest or canonical procedure address. Select its returned
|
||||
Evidence ID for the next view instead of requesting the complete bundle.
|
||||
Evidence ID for the next view instead of requesting the complete bundle. When
|
||||
that record has no supported view (for example an annotation), read it with
|
||||
`detail: "records"` and an `evidence_id` filter; `missing_evidence_links`
|
||||
names linked sources this session does not retain.
|
||||
`retention: "complete-record"` means the whole record is stored; native
|
||||
value-flow truncation and unknown metadata remain separate. For an exported
|
||||
portable bundle, use `rea inspect-evidence-bundle` with JSON containing `path`,
|
||||
|
||||
@@ -73,6 +73,14 @@ Parent Evidence ID plus source index preserves same-address
|
||||
alternatives. Follow `coverage.next_offset` only as far as the question needs;
|
||||
exhausting retained rows does not establish complete program discovery.
|
||||
|
||||
`batch_decompile` returns every body unless you select `detail: "summary"` when
|
||||
the connected tool advertises it. Use its parent Evidence ID with
|
||||
`view: {kind: "batch-decompile", collection: "items"}` for body-free rows and
|
||||
exact `where.indices`, `addresses`, `procedure_addresses` or `statuses`. Add
|
||||
`include_pseudocode: true` for several chosen bodies, or use
|
||||
`collection: "pseudocode"` with one row's `index` to page a large body. Batch
|
||||
limitations aggregate every item, so do not attribute them to one successful row.
|
||||
|
||||
Use `record_unknown` to track unresolved questions in the user's investigation
|
||||
and name the authority or environment still required. Supply supporting and
|
||||
contradicting evidence IDs. It updates the session registry directly.
|
||||
|
||||
@@ -169,7 +169,10 @@ leaving it, and `both` when the question needs both. Trace traversal includes
|
||||
containment relationships, so even a narrow seed can expand many nodes; a
|
||||
bidirectional trace can reach unrelated sibling modules. A module item or page
|
||||
is preferable when its observations already answer the question. Do not treat
|
||||
all reached nodes as participants in the feature.
|
||||
all reached nodes as participants in the feature. For a broad trace, pass
|
||||
`view: {"kind": "feature-trace", "collection": "paths"}` (or `summary`) to
|
||||
retain the complete trace and read terminal paths first; page `nodes` or `edges`
|
||||
with exact `where.node_ids` through `inspect_analysis_view` as needed.
|
||||
|
||||
Reuse retained parent Evidence when the server advertises it, or supply complete
|
||||
Evidence inline. Include Hopper or Ghidra Evidence only when its artifact digest
|
||||
|
||||
@@ -346,8 +346,9 @@ the complete tool inventory grouped by current status. Expand each group's
|
||||
overrides them; availability, reasons, remediation, and modes remain inline.
|
||||
Provider candidates keep availability and target-support status, code, and
|
||||
reason. `omitted_fields` names what the summary leaves out; `{"detail":
|
||||
"complete"}` adds provider diagnostics, candidate capability sets, and tool
|
||||
surface, `effects`, and `annotations`. Those arrays name true flags from fixed
|
||||
"complete"}` adds each unavailable candidate's `remediation`, provider
|
||||
diagnostics, candidate capability sets, and tool surface, `effects`, and
|
||||
`annotations`. Those arrays name true flags from fixed
|
||||
vocabularies, so an omitted known flag is false. `tools/list` remains the
|
||||
canonical source for full tool schemas when target or provider state changes.
|
||||
|
||||
|
||||
@@ -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.986/LICENSE), with its [NOTICE](https://github.com/Noelo-Lab/kuna/blob/v1.986/NOTICE) | Bring your own `kuna`; REA verified 1.986 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.989/LICENSE), with its [NOTICE](https://github.com/Noelo-Lab/kuna/blob/v1.989/NOTICE) | Bring your own `kuna`; REA verified 1.989 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. |
|
||||
|
||||
@@ -693,6 +693,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
row.addProperty("status", relocation.getStatus().toString());
|
||||
row.addProperty("type", relocation.getType());
|
||||
row.add("symbol", GSON.toJsonTree(relocation.getSymbolName()));
|
||||
row.add("values", GSON.toJsonTree(relocation.getValues() == null ? new long[0] : relocation.getValues()));
|
||||
byte[] original = relocation.getBytes();
|
||||
row.add("original_bytes_hex", original == null ? JsonNull.INSTANCE : GSON.toJsonTree(bytesHex(original)));
|
||||
@@ -2274,9 +2275,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
throw new RequestFailure("invalid_request", "mode must be literal or regex");
|
||||
}
|
||||
boolean caseSensitive = requireBoolean(params, "case_sensitive");
|
||||
List<InventoryItem> inventory = procedureSearch
|
||||
? procedures().stream().map(FunctionEntry::item).toList()
|
||||
: strings();
|
||||
ValueMatcher matcher;
|
||||
if (mode.equals("literal") && !caseSensitive) {
|
||||
if (!params.has("literal_ascii_candidate"))
|
||||
@@ -2290,7 +2288,26 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
: regexMatcher(expression, caseSensitive);
|
||||
}
|
||||
JsonArray items = new JsonArray();
|
||||
for (InventoryItem item : inventory) {
|
||||
if (procedureSearch) {
|
||||
// Search the names exact lookup accepts, keeping one row per
|
||||
// procedure under its primary name.
|
||||
for (FunctionEntry entry : procedures()) {
|
||||
monitor.checkCancelled();
|
||||
ProcedureNameMatch match = matchProcedureNames(entry, matcher);
|
||||
if (!match.matched()) {
|
||||
continue;
|
||||
}
|
||||
JsonObject item = resultItem(entry.item, null);
|
||||
if (!match.aliases().isEmpty()) {
|
||||
JsonArray aliases = new JsonArray();
|
||||
for (String alias : match.aliases()) aliases.add(alias);
|
||||
item.add("matched_aliases", aliases);
|
||||
}
|
||||
items.add(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
for (InventoryItem item : strings()) {
|
||||
monitor.checkCancelled();
|
||||
if (!matcher.matches(item.value)) {
|
||||
continue;
|
||||
@@ -2300,6 +2317,36 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* The one definition of the names that select a procedure, shared by exact
|
||||
* lookup and search: its primary name and every other symbol at its exact
|
||||
* entry, each in leaf or namespace-qualified spelling. A label inside the
|
||||
* body never names the procedure. Matching secondary symbols are reported
|
||||
* by their qualified names, sorted.
|
||||
*/
|
||||
private ProcedureNameMatch matchProcedureNames(FunctionEntry entry, ValueMatcher matcher) throws Exception {
|
||||
Function function = entry.function;
|
||||
boolean primary = spellingMatches(function.getName(), entry.item.value, matcher);
|
||||
Set<String> aliases = null;
|
||||
for (Symbol symbol : currentProgram.getSymbolTable().getSymbols(function.getEntryPoint())) {
|
||||
monitor.checkCancelled();
|
||||
String qualified = symbol.getName(true);
|
||||
if (!spellingMatches(symbol.getName(), qualified, matcher)) continue;
|
||||
if (qualified.equals(entry.item.value)) {
|
||||
primary = true;
|
||||
continue;
|
||||
}
|
||||
if (aliases == null) aliases = new TreeSet<>();
|
||||
aliases.add(qualified);
|
||||
}
|
||||
return new ProcedureNameMatch(primary, aliases == null ? List.of() : List.copyOf(aliases));
|
||||
}
|
||||
|
||||
/** A leaf is the suffix of its qualified spelling, so a literal found in it is found in both. */
|
||||
private static boolean spellingMatches(String leaf, String qualified, ValueMatcher matcher) {
|
||||
return matcher.matches(qualified) || (!leaf.equals(qualified) && matcher.matches(leaf));
|
||||
}
|
||||
|
||||
private List<InventoryItem> names() throws Exception {
|
||||
if (nameInventory != null) {
|
||||
return nameInventory;
|
||||
@@ -2402,26 +2449,13 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
// exact symbol name, even when every character is a hexadecimal digit.
|
||||
boolean explicitAddress = isExplicitReaAddress(value);
|
||||
if (!explicitAddress) {
|
||||
// Imported labels (for example Mach-O _main) can share a function
|
||||
// entry without becoming its primary symbol; search accepts the same.
|
||||
ValueMatcher selector = value::equals;
|
||||
List<Function> matches = new ArrayList<>();
|
||||
for (FunctionEntry entry : procedures()) {
|
||||
monitor.checkCancelled();
|
||||
Function function = entry.function;
|
||||
Symbol symbol = function.getSymbol();
|
||||
String qualified = symbol == null ? function.getName() : symbol.getName(true);
|
||||
boolean matchesName = function.getName().equals(value) || qualified.equals(value);
|
||||
if (!matchesName) {
|
||||
// Imported labels (for example Mach-O _main) can share a
|
||||
// function entry without becoming its primary symbol. Only
|
||||
// exact entries qualify; an interior label is not a selector.
|
||||
for (Symbol entrySymbol : currentProgram.getSymbolTable().getSymbols(function.getEntryPoint())) {
|
||||
monitor.checkCancelled();
|
||||
if (entrySymbol.getName().equals(value) || entrySymbol.getName(true).equals(value)) {
|
||||
matchesName = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (matchesName) matches.add(function);
|
||||
if (matchProcedureNames(entry, selector).matched()) matches.add(entry.function);
|
||||
}
|
||||
if (matches.size() > 1)
|
||||
throw new RequestFailure("ambiguous", "Ghidra procedure name is ambiguous: " + value +
|
||||
@@ -5138,6 +5172,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
private record AddressNameConflict(Namespace namespace, String leaf, List<Address> requesters, List<Address> holders) {}
|
||||
private record AddressNameFailure(String reason, AddressNameConflict conflict) {}
|
||||
private record FunctionEntry(Function function, InventoryItem item) {}
|
||||
private record ProcedureNameMatch(boolean primary, List<String> aliases) {
|
||||
boolean matched() {
|
||||
return primary || !aliases.isEmpty();
|
||||
}
|
||||
}
|
||||
private record FunctionAnalysisObservation(JsonObject dossier, List<String> limitations, JsonObject relationshipCoverage, boolean decompilerFailed) {}
|
||||
private record CallBoundaryObservation(List<String> limitations, Integer unrepresentedCalleeCalls, JsonArray unresolvedCalls) {}
|
||||
private record SwiftStringLiteralDecode(JsonArray literals, JsonArray omissions) {}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -15,6 +15,8 @@ struct ProcessIdentityObservation: Encodable {
|
||||
let effective_uid: UInt32?
|
||||
let lifecycle: String?
|
||||
let relation: ProcessRelationObservation?
|
||||
/** Display-only kernel accounting name (p_comm, at most MAXCOMLEN bytes). */
|
||||
let name: String?
|
||||
let reason: String?
|
||||
}
|
||||
|
||||
@@ -31,6 +33,7 @@ struct ProcessMetadata {
|
||||
let parentPid: Int32
|
||||
let processGroupId: Int32
|
||||
let pointerSize: Int
|
||||
let name: String
|
||||
}
|
||||
|
||||
struct ProcessSnapshot {
|
||||
@@ -92,7 +95,8 @@ func readProcessSnapshot(
|
||||
lifecycle: process.kp_proc.p_stat == SZOMB ? "zombie" : "non-zombie",
|
||||
parentPid: process.kp_eproc.e_ppid,
|
||||
processGroupId: process.kp_eproc.e_pgid,
|
||||
pointerSize: process.kp_proc.p_flag & P_LP64 != 0 ? 8 : 4
|
||||
pointerSize: process.kp_proc.p_flag & P_LP64 != 0 ? 8 : 4,
|
||||
name: processName(process)
|
||||
)
|
||||
)
|
||||
},
|
||||
@@ -103,6 +107,13 @@ func readProcessSnapshot(
|
||||
return ProcessSnapshot(metadata: [:], failure: "process_table_failed_\(ENOMEM)")
|
||||
}
|
||||
|
||||
/** Decode the NUL-terminated accounting name; invalid UTF-8 is replaced for display. */
|
||||
private func processName(_ process: kinfo_proc) -> String {
|
||||
withUnsafeBytes(of: process.kp_proc.p_comm) { bytes in
|
||||
String(decoding: bytes.prefix { $0 != 0 }, as: UTF8.self)
|
||||
}
|
||||
}
|
||||
|
||||
func readProcessIdentities(pids: [Int32]) -> [ProcessIdentityObservation] {
|
||||
readProcessIdentities(pids: pids, snapshot: readProcessSnapshot())
|
||||
}
|
||||
@@ -130,6 +141,7 @@ func readProcessIdentities(
|
||||
effective_uid: nil,
|
||||
lifecycle: nil,
|
||||
relation: nil,
|
||||
name: nil,
|
||||
reason: "not-in-snapshot"
|
||||
)
|
||||
}
|
||||
@@ -144,6 +156,7 @@ func readProcessIdentities(
|
||||
parent_pid: metadata.parentPid,
|
||||
process_group_id: metadata.processGroupId
|
||||
),
|
||||
name: metadata.name,
|
||||
reason: nil
|
||||
)
|
||||
}
|
||||
@@ -157,6 +170,7 @@ private func unavailableIdentity(pid: Int32, reason: String) -> ProcessIdentityO
|
||||
effective_uid: nil,
|
||||
lifecycle: nil,
|
||||
relation: nil,
|
||||
name: nil,
|
||||
reason: reason
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ PROFILE = "[email protected];[email protected];[email protected]"
|
||||
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):
|
||||
|
||||
@@ -257,12 +257,12 @@ beyond already-connected devices (`adb connect`) are not exposed yet.
|
||||
|
||||
## Real verification
|
||||
|
||||
`npm run verify:adb` exercises every operation against a live device. It
|
||||
`npm run verify:adb` exercises observation and transfer operations against a live device. It
|
||||
requires `adb` (or `REA_ADB_PATH`) and any attached emulator or phone;
|
||||
`--serial` selects one when several are attached, `--pull PACKAGE` pulls a
|
||||
real package and verifies the returned digests against the files on disk,
|
||||
and `--install-apk PATH` drives a full install → resolve → uninstall
|
||||
lifecycle with a lane-owned probe.
|
||||
real package and verifies the returned digests against the files on disk.
|
||||
The optional `--install-apk` lifecycle is unavailable until the selected APK
|
||||
identity and device cleanup ownership can be established safely.
|
||||
|
||||
Record: verified against adb 34.0.5 on Linux against an Android 14
|
||||
(API 34) x86_64 emulator — all operations above, a 224-package inventory,
|
||||
|
||||
@@ -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 |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -424,8 +426,10 @@ rea inspect-native-exports ./libexample.so --detail summary --evidence_output ./
|
||||
without an export directory is a complete empty inventory.
|
||||
- ELF rows list defined `.dynsym` symbols with global, weak or GNU-unique
|
||||
binding and default or protected visibility. Each row keeps `value`
|
||||
(`st_value` as stored; bit 0 marks Thumb code on 32-bit ARM), `size`, `type`
|
||||
and `binding`. ELF string tables do not declare a text encoding. Names that
|
||||
(`st_value` as stored; bit 0 marks Thumb code on 32-bit ARM), `size`, `type`,
|
||||
`binding` and `visibility` (`"default"` or `"protected"`; `"unknown"` in older
|
||||
historical Evidence where visibility was not recorded). ELF string tables do
|
||||
not declare a text encoding. Names that
|
||||
contain non-ASCII bytes are decoded as UTF-8 when those bytes form valid
|
||||
UTF-8; `name_utf8_decodable` records whether the non-ASCII name bytes are
|
||||
valid UTF-8 and `name_bytes_base64` retains those exact bytes (`null` for
|
||||
|
||||
@@ -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.
|
||||
|
||||
+31
@@ -561,6 +561,25 @@ be verified. See [MCP contracts](mcp-contracts.md) for session lifecycle and
|
||||
[Ghidra first-query deadlines](mcp-contracts.md#ghidra-first-query-deadlines-and-recovery)
|
||||
for import, client timeouts and recovery.
|
||||
|
||||
A one-shot command releases its provider session and REA runtime before it
|
||||
prints a failure, so it omits the same-session reuse and
|
||||
`update_analysis_settings` guidance an MCP connection receives. Its
|
||||
`details.limit` reports `scope: "rea-invocation"` and
|
||||
`applies_at: "next_invocation"`; set the named `environment` (or the per-call
|
||||
`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
|
||||
@@ -1286,6 +1305,18 @@ new file. A publication that has committed can win a later cancellation; the
|
||||
after commit, the `cleanup_incomplete` error reports the same file as
|
||||
`details.published_path` and `details.bytes`.
|
||||
|
||||
When a failure retains a completed observation and no bundle committed,
|
||||
`details.source_closure` states what that observation cites. `publication` is
|
||||
`not_published`. `records` holds authentic source Evidence this invocation
|
||||
captured, within 64 KiB. `omitted` lists up to 64 other identities and
|
||||
`omitted_count` the total: `not_captured` means the invocation holds no such
|
||||
record, so nothing is shown for it or its ancestors; `not_inlined` means it was
|
||||
captured but is not in this response. `complete` is true only when every cited
|
||||
record is in `records` or embedded in the observation. If examining the closure
|
||||
itself fails, `collection_failure` names the error type and the closure state is
|
||||
unknown; the original failure is still returned. Nothing is retained after exit,
|
||||
and running the command again is a new observation.
|
||||
|
||||
When the reader of piped output closes early, as `rea ... | head` does, the
|
||||
CLI stops writing, prints no error and exits `141` (`128 + 13`, the status of a
|
||||
SIGPIPE-terminated writer). The operation itself may have completed; only its
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -222,6 +222,25 @@ Error copying, attribution, redaction, and delivery must preserve these facts.
|
||||
Human-readable remediation explains the same failure; clients should not need
|
||||
to parse that sentence to identify an editable setting.
|
||||
|
||||
`scope` and `applies_at` describe a continuing caller, such as an MCP
|
||||
connection. A one-shot invocation, such as a CLI command, has released its
|
||||
provider session and exited its runtime before its caller reads the failure,
|
||||
so its adapter projects with an explicit `"invocation"` caller lifetime.
|
||||
`details.limit` then keeps every other field and reports
|
||||
`scope: "rea-invocation"`, `applies_at: "next_invocation"` and the launch
|
||||
`environment` seed when one exists. `agent_editable: true` still holds, because
|
||||
the caller chooses that environment value or per-call `parameter` for the new
|
||||
invocation; `rea_restart_required: false` means no running REA process needs a
|
||||
restart. A producer keeps guidance that holds only while its caller retains the
|
||||
session, such as "the Ghidra session remains usable" or a same-session runtime
|
||||
update, in `continuingCallerGuidance`; the shared presentation adds it only for
|
||||
a continuing caller. The producer's own message and remediation must stay true
|
||||
after the session is released. A composed error records each embedded child
|
||||
failure through `childFailureDiagnostics`, which snapshots it at composition
|
||||
for both lifetimes when they differ, so cleanup history keeps its state and
|
||||
the final projection selects the caller's form. Failures retained inside
|
||||
Evidence keep the continuing form, so CLI and MCP Evidence stay identical.
|
||||
|
||||
Cache policy is part of enforcement. Successful reusable analysis can survive
|
||||
a budget update. Transient Ghidra decompile failures remain uncached so a raised
|
||||
deadline can recover on the same Program. Admission checks still apply to reused
|
||||
@@ -286,7 +305,9 @@ whether live repair is possible.
|
||||
## What agents do
|
||||
|
||||
Use `details.limit` for a direct runtime update when it names an editable
|
||||
setting. Follow `remediation.action` for per-call, fixed and external controls.
|
||||
setting with `scope: "rea-runtime"`. For `scope: "rea-invocation"`, set its
|
||||
`environment` or `parameter` for a new invocation instead.
|
||||
Follow `remediation.action` for per-call, fixed and external controls.
|
||||
Inspect settings only when bounds or application details are missing. Restart only when it says the value is
|
||||
read at startup. A sentence that a control does not raise the limit is not a
|
||||
request to change it.
|
||||
|
||||
@@ -201,6 +201,9 @@ neither result means the requested host operation occurred. The existing
|
||||
`openExternal`, `openPath`, and `trashItem` blocks reject their promises.
|
||||
The `trashItem` block prevents that call from reaching the OS trash operation;
|
||||
it does not intercept other ways the application can remove files.
|
||||
Process rejection and uncaught-exception events are observed at dispatch without
|
||||
adding exception handlers. The runtime and application retain their warning,
|
||||
handler, and fatal-exit policies, including for rejected shell-block promises.
|
||||
CLI SIGINT/SIGTERM requests cancellation and awaits owned-process cleanup.
|
||||
Window IDs, URLs, and cached titles come from one main-process Electron snapshot,
|
||||
so a crashed renderer does not require a successful renderer-side title query.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -172,6 +172,11 @@ have no file backing. ELF mappings are observed
|
||||
without independent format-specific load-image verification, as the result's
|
||||
status and limitations state.
|
||||
|
||||
Relocation rows retain Ghidra's `symbol` value, including an empty string or
|
||||
null when returned by Ghidra. It is a provider observation, not proof of the
|
||||
runtime-resolved address. Older Evidence may not have recorded this field;
|
||||
a relocation page requesting it reports that uncertainty.
|
||||
|
||||
For large relocation inventories, select MCP `inspect_native_load_image` with
|
||||
`detail: "summary"`. It returns verification state, observed identity and counts,
|
||||
while retaining complete Evidence. Use `normalized_result.parent_evidence_id`
|
||||
|
||||
@@ -43,7 +43,8 @@ rea decompile /absolute/path/to/program main --json
|
||||
|
||||
If the registration cannot be read or validated, provider diagnostics retain its
|
||||
selected `registration_path` and the configuration owner's setting/constraint
|
||||
facts in `configuration_details.settings`, plus `configuration_remediation`.
|
||||
facts in `configuration_details.settings`. The availability's `remediation`, and
|
||||
each operation it makes unavailable, carry the configuration owner's repair.
|
||||
`configured` means a path was selected, not that it is valid or an IDA connection
|
||||
was established; failed registrations remain unavailable. Correct
|
||||
`REA_IDA_MCP_CONFIG` in the next CLI invocation's environment or the MCP server's
|
||||
|
||||
@@ -51,7 +51,6 @@ For runtime instrumentation, see the [Frida guide](./frida-instrumentation.md).
|
||||
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 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) |
|
||||
|
||||
+38
-6
@@ -390,6 +390,20 @@ A scoped report has `scope.mode: "explicit"`. Its `scope_checks` determine
|
||||
`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,
|
||||
@@ -666,7 +680,8 @@ Hopper is separate commercial software with its own license. Its free demo has
|
||||
vendor-defined limits, and a paid license is optional. REA reuses any detected
|
||||
installation and preserves Hopper during uninstall.
|
||||
|
||||
The automated Hopper installer supports macOS 12+ and x86_64 hosts running
|
||||
The automated Hopper installer supports macOS 12+, subject to the minimum
|
||||
macOS version of the current Hopper release, and x86_64 hosts running
|
||||
Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS, or Omarchy. The
|
||||
official Linux packages are x86_64-only, so setup does not offer Hopper on other
|
||||
Linux architectures; configure [Ghidra](#ghidra) there instead. REA itself runs on any
|
||||
@@ -689,12 +704,19 @@ for installation. Setup cannot prompt when its output is redirected or during a
|
||||
container image build. To approve Hopper installation without prompts, run
|
||||
`rea setup --yes --install-hopper`.
|
||||
|
||||
If the Linux package download receives an HTTP failure, setup includes the
|
||||
observed HTTP status in its `download_failed` diagnostic. The status does not
|
||||
establish the underlying cause or guarantee that retrying will succeed.
|
||||
If a Hopper request fails on macOS or Linux, the setup result's `download`
|
||||
field names the request (`release_feed`, `package` or `public_key`), its `url`,
|
||||
and the observed outcome `kind`: `http_status` with `httpStatus`, `timeout`,
|
||||
`network`, `response_too_large`, or `url_refused`. The remediation names the
|
||||
same URL and outcome. HTTP 404 or 410 means the file is missing, so an
|
||||
immediate retry is unlikely to help; 5xx, 408 and 429 indicate a temporary
|
||||
service failure. The status does not establish the underlying cause or guarantee
|
||||
that retrying will succeed. Doctor and Linux launches report the same request
|
||||
and status when publisher verification cannot download the current package.
|
||||
|
||||
Hopper metadata and package downloads require HTTPS throughout the redirect
|
||||
chain; a redirect to HTTP is refused before that destination is requested.
|
||||
chain; a redirect to HTTP or to a URL carrying credentials is refused before
|
||||
that destination is requested, and those credentials are not reported.
|
||||
|
||||
On macOS, approved setup downloads the official DMG, checks its published size
|
||||
and digest, validates the application bundle, and atomically installs it to
|
||||
@@ -702,6 +724,15 @@ and digest, validates the application bundle, and atomically installs it to
|
||||
operator can choose its demo mode or activate an existing license. Homebrew and
|
||||
administrator access are not used.
|
||||
|
||||
Each Hopper release declares its own minimum macOS version, so the setup plan
|
||||
cannot confirm support before approval. Setup compares this Mac's version with
|
||||
the release metadata's `deployment_target` before downloading the DMG, and with
|
||||
the signed application's `LSMinimumSystemVersion` before copying it. If this Mac
|
||||
is older, setup fails with `incompatible_host_version`, names both versions, and
|
||||
installs nothing. Update macOS, or install a Hopper release that supports this
|
||||
macOS version yourself and set `HOPPER_LAUNCHER_PATH`. An existing Hopper
|
||||
installation is not checked against the current release's minimum.
|
||||
|
||||
The Hopper launcher action used by REA creates a document from an executable;
|
||||
its supported command-line interface does not attach to an already-open
|
||||
document. While another live REA session owns the same target and loader
|
||||
@@ -1314,7 +1345,8 @@ fails with `provider_timeout` and that message; other Ghidra tools stay
|
||||
available. If the caller also cancels while the timeout settles, the session
|
||||
still closes and the message says so. Session reuse concerns an active MCP or
|
||||
library session; a standalone CLI command still closes its own session when it
|
||||
finishes. In MCP, `ghidra.decompile_timeout_seconds` can override the launch
|
||||
finishes, so its timeout names `REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS` for a new
|
||||
invocation instead. In MCP, `ghidra.decompile_timeout_seconds` can override the launch
|
||||
value for newly accepted operations on the existing Program; explicit `null`
|
||||
removes the deadline. A transient timeout remains uncached so the same function
|
||||
can be retried after an update. See the
|
||||
|
||||
@@ -173,7 +173,14 @@ Results follow retained node order and default to offset 0 and limit 64. Supply
|
||||
to examine relationships from that node. Other typed seeds do not search labels.
|
||||
|
||||
The result contains the matching basis, complete reachable subgraph, terminal
|
||||
paths, authority summaries, and native handoffs. A handoff binds
|
||||
paths, authority summaries, and native handoffs. A bidirectional trace of a
|
||||
large application can exceed one MCP response. Over MCP, pass
|
||||
`view: {"kind":"summary"}` or a `feature-trace` page of `paths`, `nodes`,
|
||||
`edges`, `seed_matches` or `native_handoffs` to retain the complete trace in
|
||||
the session and return only selected rows with the trace's identity and
|
||||
coverage. Page the retained trace with `inspect_analysis_view`; an oversized
|
||||
complete reply offers its first `paths` page as a ready-to-call continuation.
|
||||
See [MCP contracts](mcp-contracts.md) for filters. A handoff binds
|
||||
the exact native artifact digest and requested exports from the application
|
||||
graph. Trace direction selects the reachable addons; requested
|
||||
exports for each selected addon come from the complete supplied graph, so an
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1002,7 +1016,12 @@ reason in the renderer observations.
|
||||
HTML script source ranges follow the HTML parser across LF, CRLF, and bare CR
|
||||
line endings, preserving UTF-16 columns.
|
||||
Inline JavaScript in local HTML, meaning classic or module `<script>` bodies
|
||||
without a `src` attribute, is not analyzed. Each HTML file that has one gets an
|
||||
without a `src` attribute, is not analyzed. Embedded SVG scripts use `href`
|
||||
(or `xlink:href` when `href` is absent) instead: an existing reference, even
|
||||
an empty one, suppresses the inline body. SVG's `src` and HTML-only `language`
|
||||
attributes do not suppress its JavaScript body. SVG references and inline
|
||||
bodies share the same inert parsing and source coordinates as HTML scripts;
|
||||
template content stays inert. Each HTML file that has an inline body gets an
|
||||
unknown `parse-html-inline-scripts` scope with the exact body count and the
|
||||
script type and source range of the first 100 bodies; beyond that, coverage is
|
||||
truncated under the `html_inline_script_locations_per_file` limit, and the
|
||||
|
||||
@@ -65,8 +65,9 @@ confidence and basis, and does not prove a backend endpoint or reachability.
|
||||
Pages obey the shared 1 MiB projection budget; MCP transport applies its own
|
||||
wire-size guard. A byte-limited page returns a continuation; an indivisible
|
||||
oversized record or metadata returns a typed resource error. Select fewer
|
||||
fields or retrieve the complete retained parent through `get_evidence_bundle`,
|
||||
`export_evidence_bundle`, or the CLI's exported bundle. Full Evidence is never
|
||||
fields or retrieve the complete retained parent through `get_evidence_bundle`
|
||||
(`detail: "records"` with its `evidence_id`), `export_evidence_bundle`, or the
|
||||
CLI's exported bundle. Full Evidence is never
|
||||
silently truncated.
|
||||
|
||||
Collections are `summary`, `routes`, `occurrences`, `sources`, and `unknowns`.
|
||||
|
||||
@@ -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
|
||||
|
||||
+26
-1
@@ -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.986, 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 1.986 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.989, 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.989 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.
|
||||
@@ -47,6 +69,8 @@ Windows is not supported yet. Supported targets are ELF and thin Mach-O executab
|
||||
| `xrefs` | Exact-address incoming reference sites, with forwarding aliases filtered explicitly |
|
||||
| `analyze_function` | Pseudocode and assembly from the export, plus call relationships; typed references, comments, strings and CFG remain unavailable |
|
||||
|
||||
Kuna's inventory gives some entries aliases, such as ELF versioned-symbol names and generated `sub_` names. Name selectors accept an alias that names one entry and list every entry a shared alias names. `search_procedures` matches aliases too: each entry appears once under its primary name, with the matching aliases in `matched_aliases`. `list_procedures` shows primary names only.
|
||||
|
||||
Typed procedure references, basic blocks, raw-memory tools, annotations and type recovery are not available from this provider. Use another provider for those operations.
|
||||
|
||||
## How answers are produced
|
||||
@@ -123,6 +147,7 @@ that rollback is resolved; a clean rollback permits another setup attempt.
|
||||
|
||||
## Provenance and limitations
|
||||
|
||||
- If a completed project export reports a failed function, `procedure_pseudo_code` with `analysis: "independent_fresh"` or `rea decompile --analysis independent_fresh` requests a new single-function attempt. It bypasses the project result and prior single-function and snapshot results; it does not replace the original observation. On a cold session it obtains an inventory without starting a project export. The independent attempt can use different analysis context and may also fail. Identical results can share an Evidence ID because Evidence is content-addressed.
|
||||
- Addresses and bytes are Kuna's observations of the image. Procedure entries and extents are its derivations: extents are upper bounds clipped at the next entry, and Kuna can split one function at an entry its own analysis infers. Pseudocode is its reconstruction, never original source.
|
||||
- Pseudocode and assembly are exact Kuna text; text from Kuna and from another provider is not comparable as equal or different.
|
||||
- Results are re-read rather than replayed from REA's analysis snapshots, because a cold answer can differ from the export's.
|
||||
|
||||
+142
-6
@@ -292,6 +292,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
|
||||
@@ -318,6 +324,10 @@ offered for completed `analyze_function` results only after session recording is
|
||||
acknowledged; the complete result and its retention reference remain intact.
|
||||
Oversized native inventories likewise include a ready-to-call first
|
||||
`native-inventory` page. Reduce its limit or select fields when a row is large.
|
||||
An oversized `trace_application_feature` result includes a first `feature-trace`
|
||||
page of its retained terminal `paths`; page `nodes` or `edges` from there.
|
||||
An oversized `batch_decompile` result includes a first body-free
|
||||
`batch-decompile` page of its retained `items`; read bodies from there.
|
||||
|
||||
A broad follow-up or `get_evidence_bundle` can also exceed the response budget; exporting
|
||||
preserves the complete session without sending it through a single MCP frame.
|
||||
@@ -331,7 +341,26 @@ a provider. `retention: "complete-record"` describes stored Evidence, while
|
||||
`native_dossier.value_flow` independently reports truncation, unavailable or
|
||||
unknown metadata. Missing omission counts remain null. Summary output carries
|
||||
every matching record; actual transport admission still applies. Omitted
|
||||
`detail` preserves complete canonical bundle delivery; filters require summary.
|
||||
`detail` preserves complete canonical bundle delivery; filters require summary
|
||||
or records.
|
||||
|
||||
To read a discovered record without transferring the whole bundle, call
|
||||
`get_evidence_bundle` with `detail: "records"` and the same exact `filters`.
|
||||
The result (`kind: "evidence-record-selection"`) pages the matching complete
|
||||
records in canonical bundle (Evidence ID) order with `offset` (default 0) and
|
||||
`limit` (default 8), and reports `matching_records`, `total_retained_records`
|
||||
and `next_offset`. Each record is the retained Evidence itself, with an
|
||||
unchanged ID, subject digest, provider, profile and readback, so it works for
|
||||
any operation, including records that `inspect_analysis_view` does not support
|
||||
such as annotations. `missing_evidence_links` lists direct `evidence_links` of
|
||||
the returned records that this session or bundle does not retain; links are not
|
||||
traversed. A selection is not an Evidence bundle: it omits manifests and
|
||||
residual unknowns, cannot be imported, and describes historical observations
|
||||
rather than current provider or database state. `offset` and `limit` require
|
||||
`detail: "records"`. If one page exceeds the response budget, select fewer
|
||||
records or one `evidence_id`; a single record larger than one response still
|
||||
needs `export_evidence_bundle`, or `inspect_analysis_view` pages where its parent
|
||||
supports views.
|
||||
|
||||
Residual-unknown mutations reject missing Evidence or unknown references and
|
||||
unproven verified resolutions with nonretryable `invalid_request` diagnostics.
|
||||
@@ -367,8 +396,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,
|
||||
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.
|
||||
|
||||
@@ -572,6 +619,69 @@ the working directory). Save its complete `--json` Evidence, then use
|
||||
`inspect-analysis-view` with an `evidence-file` source and the same view. Neither
|
||||
adapter executes the target or repeats analysis for retained queries.
|
||||
|
||||
`trace_application_feature` follows the same delivery contract. Omitting `view`
|
||||
returns the complete trace. `view: {"kind":"summary"}` or a `feature-trace` page
|
||||
retains the complete trace in the session first and returns only the selected
|
||||
facts; query further pages with `inspect_analysis_view` and the returned
|
||||
`parent_evidence_id`. The summary carries the trace identity (`trace_id`,
|
||||
`source_evidence_id`, `source_graph_id`, seed and direction), the producer's
|
||||
`summary` counts, `trace_coverage`, the traced subgraph's `graph_coverage`,
|
||||
`evidence_links` and any no-match `semantic_discovery`. A `feature-trace` page
|
||||
selects one `collection` in retained order and repeats that context:
|
||||
|
||||
- `paths`: exact terminal paths; `where.node_ids` selects paths visiting any of
|
||||
those nodes and `where.end_kinds` selects their terminal node kinds.
|
||||
- `nodes`: compact rows with identity, `seed`, `observation_count` and distinct
|
||||
authority/state/confidence `basis`; `where.node_ids` and `where.kinds` filter
|
||||
exactly, and `include_observations: true` adds each node's complete retained
|
||||
observations.
|
||||
- `edges`: exact relationships; `where.node_ids` matches either endpoint and
|
||||
`where.relations` the relation.
|
||||
- `seed_matches` and `native_handoffs`: the producer's exact rows.
|
||||
|
||||
Omitted filters select all; empty arrays select none. `offset` and `limit`
|
||||
default to 0 and 64. Page `coverage` counts the selected collection only; it
|
||||
never upgrades `trace_coverage`, and graph connectivity does not prove runtime
|
||||
reachability. The CLI keeps complete trace JSON; save it and use
|
||||
`inspect-analysis-view` with an `evidence-file` source.
|
||||
|
||||
### Selected batch decompile results
|
||||
|
||||
`batch_decompile` returns every pseudocode body by default. `detail: "summary"`
|
||||
retains the complete batch in the session first and returns its producer counts,
|
||||
`source_provider` (the composing workflow), `source_analysis_profile_digest`,
|
||||
`decompiling_provider`, procedure identity counts, total and largest
|
||||
`pseudocode` size, and `limitation_count`. A server that retains no Evidence
|
||||
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 `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
|
||||
`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
|
||||
arrays select none. `offset` and `limit` default to 0 and 64.
|
||||
`include_pseudocode: true` adds each selected body.
|
||||
- `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`.
|
||||
|
||||
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
|
||||
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
|
||||
repeated in that reply; resend it with a smaller selection.
|
||||
`export_evidence_bundle` remains the complete alternative. The CLI has no batch command; save the Evidence and use
|
||||
`inspect-analysis-view` with an `evidence-file` source or `--evidence-bundle`.
|
||||
|
||||
Completed `inspect_artifact` and atomic `inventory_artifact` Evidence also
|
||||
support summary and inventory views. A summary reports the manifest's expected
|
||||
counts separately from available record counts, observed entry kinds, inventory
|
||||
@@ -742,13 +852,27 @@ requirements. Compare the union of group names with `tools/list` when checking
|
||||
that a client received the full catalog. An operation absent from all groups is
|
||||
missing from the complete inventory; it is not an implicit unavailable entry.
|
||||
|
||||
Availability also reflects the active provider client's dispatched requests. A
|
||||
refusal or provider failure observed for an operation keeps that operation
|
||||
unavailable until a request for it dispatched later succeeds. A provider adapter
|
||||
or process failure also marks that provider's other declared-available
|
||||
operations unavailable until any request to that provider dispatched later
|
||||
succeeds; their remediation states that the status was inferred rather than
|
||||
observed for them. Recovery is not proof that each operation works, and failures
|
||||
observed on other operations or providers remain. Outcomes are ordered by
|
||||
dispatch to the provider client rather than reply completion, so a late reply
|
||||
from an older request does not override a newer outcome. Request deadlines and
|
||||
cancellations neither mark nor clear availability. A new provider client starts
|
||||
without these outcomes.
|
||||
|
||||
`detail` selects the projection and the result echoes it. The default
|
||||
`summary` keeps open state, target identity, the binding, current-route
|
||||
`capabilities`, run, activity and health status, and each provider candidate's
|
||||
identity, selection, and availability and target-support `status`, `code`, and
|
||||
`reason`. It lists the fields it leaves out in `omitted_fields`: provider
|
||||
`diagnostics`, candidate `capabilities`, and each tool group's static `surface`,
|
||||
`effects`, and `annotations`. Omitted fields are not empty or false.
|
||||
`reason`. It lists the fields it leaves out in `omitted_fields`: an unavailable
|
||||
candidate's `remediation`, provider `diagnostics`, candidate `capabilities`, and
|
||||
each tool group's static `surface`, `effects`, and `annotations`. Omitted fields
|
||||
are not empty or false.
|
||||
`detail: "complete"` returns them inline, groups tools by those flags too, and
|
||||
has no `omitted_fields`. The output schema keys each result's candidate,
|
||||
tool-group and omission fields by its `detail`. CLI `capabilities` and
|
||||
@@ -801,7 +925,9 @@ set, `shared_limitations` is the ordered common prefix; append a group's ordered
|
||||
`effects` arrays use the same true-only closed-vocabulary rule as tool
|
||||
availability. Expand every group operation while retaining its provider
|
||||
identity, availability, reason, rejection code, effects, and ordered
|
||||
limitations.
|
||||
limitations. An unavailable group carries `remediation` when its readiness
|
||||
owner supplied a repair instruction; `tool_availability` uses that instruction
|
||||
rather than the failure description.
|
||||
|
||||
`binary_session.analysis_provider_candidates` is authoritative for deep-engine
|
||||
discovery. Each candidate's capability sets, returned with complete detail,
|
||||
@@ -876,6 +1002,14 @@ is `unavailable` with a reason when ownership could not be revalidated, or
|
||||
at that bounded check. These are historical snapshots, not live process
|
||||
inventories, and do not claim that no short-lived descendant existed.
|
||||
|
||||
After a failed close retires the target, `binary_session.cleanup_retry` names
|
||||
`close_binary` only while that session still retains the cleanup bound to the
|
||||
close error. Target-dependent availability then gives the same conditional
|
||||
cleanup guidance before suggesting `open_binary`. The hint disappears after
|
||||
cleanup succeeds. A pending flag alone does not establish this retry: active
|
||||
targets, an in-flight initial close, and external-mutation blockers keep their
|
||||
own recovery and do not receive this hint.
|
||||
|
||||
`binary_session.cleanup_pending` reports whether the session retains a provider
|
||||
client, session file resource, or provider-candidate resource whose cleanup
|
||||
has not succeeded. It can be true with
|
||||
@@ -1351,7 +1485,9 @@ cursor outside a procedure is represented as
|
||||
procedure resolution returns typed unreadable output; provider errors never
|
||||
become absence from their message text. `inspect_address_context` requires an explicit address and
|
||||
returns name, procedure, comment, inline-comment, and bookmark facets;
|
||||
unsupported facets are local `unavailable` outcomes. Use `current_document`,
|
||||
unsupported facets are local `unavailable` outcomes. A facet whose input is
|
||||
rejected includes its canonical `error` projection, including any structured
|
||||
issues, alongside the other facet outcomes. Use `current_document`,
|
||||
`current_address`, and `current_procedure` for direct single-field lookups; use
|
||||
the aggregate tools when you need related context together.
|
||||
|
||||
|
||||
@@ -68,7 +68,10 @@ function renamed to `dead` remains selectable by name. Use the names returned
|
||||
by the inventory, including their namespaces and any platform symbol prefix.
|
||||
Ghidra also accepts secondary symbols at a function entry, such as an imported
|
||||
`_main` label. A label inside a function is not a procedure-name selector; use
|
||||
its address when selecting the containing function.
|
||||
its address when selecting the containing function. `search_procedures` matches
|
||||
the same names, in leaf or qualified spelling: the function appears once with
|
||||
its primary name as `value`, and `matched_aliases` lists the qualified names of
|
||||
the secondary symbols that matched.
|
||||
Overloads can share a fully qualified name; ambiguity errors return every
|
||||
matching entry address so the caller can select the intended function directly.
|
||||
|
||||
@@ -198,6 +201,11 @@ remains valid. This tool
|
||||
has no dedicated CLI command; the existing `decompile` command remains the
|
||||
single-procedure CLI operation.
|
||||
|
||||
`detail: "summary"` retains the complete batch and returns only its counts,
|
||||
identities and sizes. `inspect_analysis_view` then reads rows or one body from
|
||||
the retained record, an Evidence file or a portable bundle without repeating
|
||||
decompilation: see [selected batch results](mcp-contracts.md#selected-batch-decompile-results).
|
||||
|
||||
Each batch schedules one provider read at a time, but has no shared input-count
|
||||
or aggregate retained-output allowance. It retains every requested row and full
|
||||
pseudocode body through Evidence construction. The MCP response budget checks
|
||||
@@ -219,7 +227,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
|
||||
@@ -380,6 +398,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,
|
||||
@@ -425,7 +448,12 @@ 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.
|
||||
Failures while expanding a validated seed remain unknown observations and do not
|
||||
establish canonical function identity. A successful seed without a native def-use
|
||||
establish canonical function identity. A cancelled expansion read returns the
|
||||
cancellation. An expansion read whose provider cannot confirm cleanup of
|
||||
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.
|
||||
|
||||
@@ -434,8 +462,9 @@ page omits retained nodes, and the edges they source, before `offset` or after
|
||||
`next_offset`. Every page repeats `traversal`: `complete`, or `truncated` with
|
||||
`reasons` naming each observed cause: the `max_depth`, `max_functions`,
|
||||
`max_call_sites`, `max_nodes` or `max_edges` budget, the fixed
|
||||
`node_payload_bytes` allowance, `provider_pcode_truncated`, or
|
||||
`ambiguous_call_candidates`. Each cause also has a located unknown. A budget is
|
||||
`node_payload_bytes` allowance, `provider_pcode_truncated`,
|
||||
`ambiguous_call_candidates`, or, only in a stopped trace's partial Evidence,
|
||||
`cleanup_incomplete`. Each cause also has a located unknown. A budget is
|
||||
reported only after it rejects work, so an exactly filled budget is not
|
||||
truncation; a depth or candidate limit is reported only when no other path
|
||||
analyzes or queues that destination. `total_nodes` and `total_edges` count
|
||||
@@ -990,7 +1019,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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+46
-1186
File diff suppressed because it is too large.
Load diff
@@ -119,7 +119,7 @@ Then rerun this installer.",implemented,verified by shell branch inspection
|
||||
90,install.sh:version mismatch,Installed rea reports a different version,installed version ACTUAL does not match EXPECTED.,Low risk; gives exact integrity mismatch,Keep current copy,unchanged,verified by exact installer test
|
||||
90.5,@modelcontextprotocol/server:tool input validation,Caller sends arguments that violate a declared tool schema,"Canonical invalid_request with the SDK diagnostic plus recovery.kind=inspect_input_contract and a bounded instruction to read the advertised schema or call tools/list",Keeps field/type correction while telling the agent how to recover; diagnostics over 4 KiB stay on the existing retained-error path,Normalize ordinary validation failures and retain oversized diagnostics,implemented,verified by real in-memory and stdio MCP client/server calls with wrong argument types
|
||||
90.6,incur:unknown CLI command,Caller enters an unknown REA command,"'COMMAND' is not a command for 'rea'. Suggested command: rea --help",Low risk; identifies input and exact discovery action,Keep framework-generated copy,unchanged,verified by real packaged dispatcher invocation
|
||||
90.7,src/ghidra/GhidraProviderClient.ts:projectPrimarySessionError,One procedure's decompile reaches REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS,"Ghidra decompilation of {entry} exceeded the {N}-second limit set by REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS. Only this procedure's decompilation was stopped; the Ghidra session remains usable.",Low risk; names the setting and keeps the session,Keep current copy,implemented,provider projection test and real-provider lane
|
||||
90.7,src/ghidra/GhidraProviderClient.ts:projectPrimarySessionError,One procedure's decompile reaches REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS,"Ghidra decompilation of {entry} exceeded the {N}-second limit set by REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS. Only this procedure's decompilation was stopped. (MCP adds: The Ghidra session remains usable.)",Low risk; names the setting; only a continuing MCP caller is told the session remains usable,Keep current copy,implemented,"provider projection test, CLI/MCP fixture-bridge boundary test and real-provider lane"
|
||||
90.8,src/ghidra/GhidraFileOffsetFailure.ts:ghidraFileOffsetUnavailable,"address_to_file_offset receives a well-formed address that is unmapped, uninitialized, without an authoritative target-file source, ambiguous or beyond the exact JSON integer range",{bridge reason}. REA establishes no single exact original-file offset for this well-formed address and does not derive or choose one.,Previous copy called the input invalid and directed schema repair for a valid address,"Keep the bridge reason as the headline, point to inspect_native_load_image, omit recovery.kind=inspect_input_contract and record details.issues[0].observed.file_mapping",implemented,provider projection tests; MCP and CLI analysis path through the real Ghidra client with a simulated bridge; real Ghidra not run
|
||||
91,src/application/binaryDiagnostics/ByteEntropyService.ts (issues); src/domain/analysisErrorPresentation.ts (recovery),Invalid file or window,Expected a regular file.; Selected file could not be found.; Selected byte window starts beyond the end of the file.; Selected byte window extends beyond the end of the file.; File size exceeds exact numeric offset representation.,Invalid selection cannot produce complete entropy Evidence,"Read the declared input schema or CLI usage for inspect_byte_entropy; if using MCP and the catalog may be stale, call tools/list. Correct the listed fields, then retry. Do not repeat the unchanged call.",unchanged,Executed CLI/stdio MCP: path/offset/length refusals and tiny + system-binary controls; shared canonical recovery; service/MCP tests check classification and fields; remaining listed errors source-inspected
|
||||
92,src/domain/analysisView/byteEntropyView.ts,Non-summary entropy view,Byte entropy Evidence supports the summary view; retrieve parent Evidence for the complete block series.,Unsupported view selection,Use the summary view or retrieve complete parent Evidence,implemented,typed input rejection
|
||||
|
||||
|
@@ -83,7 +83,8 @@ valid: partial results state whether WABT validation is unknown, rejected the
|
||||
module, or succeeded before a later inspection failure.
|
||||
CLI errors include this partial Evidence inline. MCP errors can return its
|
||||
`retained-evidence` reference in `details.partial_observation`; use
|
||||
`get_evidence_bundle` with `detail: "complete"` to retrieve the matching record.
|
||||
`get_evidence_bundle` with `detail: "records"` and that `evidence_id` filter to
|
||||
retrieve the matching record.
|
||||
|
||||
Core validation remains authoritative when an optional custom section cannot
|
||||
be interpreted. WABT warning output is preserved in `custom_section_warnings`;
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -87,7 +87,8 @@ example under Smart App Control or an App Control policy, reports failure stage
|
||||
invocation or invalid inspection uses `runtime_unavailable` and doctor
|
||||
classification `runtime_failure`; it does not
|
||||
mean Windows x64 itself is unsupported. Provider diagnostics retain the failure
|
||||
stage under `windows_native_failure`, together with the corresponding remediation.
|
||||
stage under `windows_native_failure`; the availability's `remediation` carries
|
||||
the corresponding repair.
|
||||
Self-test failures preserve the native operation and OS error text. Check the
|
||||
process's writable local NTFS temporary directory and the reported filesystem,
|
||||
DACL, Job Object or cleanup constraint, then rerun doctor after resolving it.
|
||||
|
||||
@@ -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}) {
|
||||
@@ -60,11 +68,11 @@ function readinessLines(markdown) {
|
||||
let comment = false;
|
||||
for (const source of lines) {
|
||||
if (fence) {
|
||||
const closing = /^ {0,3}(`+|~+)\s*$/u.exec(source);
|
||||
if (
|
||||
new RegExp(
|
||||
`^ {0,3}${fence.character}{${fence.length},}\\s*$`,
|
||||
"u",
|
||||
).test(source)
|
||||
closing !== null &&
|
||||
closing[1][0] === fence.character &&
|
||||
closing[1].length >= fence.length
|
||||
)
|
||||
fence = undefined;
|
||||
continue;
|
||||
@@ -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))
|
||||
|
||||
@@ -78,7 +78,6 @@ let correlationSequence = 0;
|
||||
|
||||
const shape = (value, depth = 0) => {
|
||||
if (depth > 2 || value === null) return value === null ? "null" : "nested";
|
||||
if (arrayIsArray(value)) return "array";
|
||||
switch (typeof value) {
|
||||
case "string":
|
||||
return "string";
|
||||
@@ -91,7 +90,12 @@ const shape = (value, depth = 0) => {
|
||||
case "function":
|
||||
return "function";
|
||||
case "object":
|
||||
return "object";
|
||||
try {
|
||||
return arrayIsArray(value) ? "array" : "object";
|
||||
} catch {
|
||||
// Revoked proxies retain their typeof identity but reject IsArray.
|
||||
return "object";
|
||||
}
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
@@ -1321,20 +1325,25 @@ try {
|
||||
boundaries.patchApplicationEffects(electron.app);
|
||||
boundaries.patchNativeAddonLoading();
|
||||
boundaries.patchChildProcess();
|
||||
process.once("uncaughtException", (cause, origin) => {
|
||||
recordRuntime("process-lifecycle", "uncaught-exception", "observed", {
|
||||
argument_shape_values: [cause, origin],
|
||||
error: true,
|
||||
});
|
||||
throw cause;
|
||||
});
|
||||
process.once("unhandledRejection", (reason, promise) => {
|
||||
recordRuntime("process-lifecycle", "unhandled-rejection", "observed", {
|
||||
argument_shape_values: [reason, promise],
|
||||
error: true,
|
||||
});
|
||||
throw reason;
|
||||
});
|
||||
// Observe dispatch without adding listeners: their presence changes Node's
|
||||
// rejection fallback and Electron's uncaught-exception listener-count policy.
|
||||
const processEmit = process.emit;
|
||||
process.emit = function observedProcessEmit(event, ...args) {
|
||||
if (this === process) {
|
||||
const kind =
|
||||
event === "unhandledRejection"
|
||||
? "unhandled-rejection"
|
||||
: event === "uncaughtExceptionMonitor"
|
||||
? "uncaught-exception"
|
||||
: null;
|
||||
if (kind !== null)
|
||||
recordRuntime("process-lifecycle", kind, "observed", {
|
||||
argument_shape_values: args,
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
return applyFunction(processEmit, this, arguments);
|
||||
};
|
||||
} catch (cause) {
|
||||
globalThis.__reaElectronActiveHookError = true;
|
||||
recordRuntime(
|
||||
|
||||
@@ -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)"
|
||||
]
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createServer } from "node:http";
|
||||
|
||||
/** Interpret request targets once, before a fixture routes or forwards them. */
|
||||
export const createBrowserFixtureHttpServer = (handleRequest) =>
|
||||
createServer((request, response) => {
|
||||
let target;
|
||||
try {
|
||||
// A leading // is an origin-form path, not a replacement authority.
|
||||
const requested = new URL(
|
||||
request.url?.startsWith("/")
|
||||
? `http://fixture.invalid${request.url}`
|
||||
: request.url,
|
||||
);
|
||||
if (requested.protocol !== "http:" && requested.protocol !== "https:")
|
||||
throw new TypeError("Unsupported fixture request protocol");
|
||||
target = { pathname: requested.pathname, search: requested.search };
|
||||
} catch {
|
||||
response.writeHead(400, {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
});
|
||||
response.end("Invalid HTTP request target");
|
||||
return;
|
||||
}
|
||||
handleRequest(request, response, target);
|
||||
});
|
||||
@@ -1,11 +1,12 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { once } from "node:events";
|
||||
import { createServer, request } from "node:http";
|
||||
import { request } from "node:http";
|
||||
import { connect } from "node:net";
|
||||
|
||||
import { createBrowserScenarioProvider } from "../../dist/composition/browserScenario.js";
|
||||
import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
|
||||
import { PlaywrightScenarioServiceWorkers } from "../../dist/browser/PlaywrightScenarioServiceWorkers.js";
|
||||
import { createBrowserFixtureHttpServer } from "./browser-fixture-http.mjs";
|
||||
|
||||
const beforeDeadline = async (promise, label, timeoutMs = 2_000) => {
|
||||
let timer;
|
||||
@@ -26,7 +27,7 @@ const beforeDeadline = async (promise, label, timeoutMs = 2_000) => {
|
||||
|
||||
// Real loopback proxy: Playwright's first transport is forwarded to Chrome,
|
||||
// then only the policy owner's discovery/handshake is deliberately withheld.
|
||||
const startProxy = async (endpoint, mode, ownerOnly = false) => {
|
||||
export const startProxy = async (endpoint, mode, ownerOnly = false) => {
|
||||
const upstream = new URL(endpoint);
|
||||
const sockets = new Set();
|
||||
let versions = 0;
|
||||
@@ -47,38 +48,44 @@ const startProxy = async (endpoint, mode, ownerOnly = false) => {
|
||||
socket.once("close", () => closed());
|
||||
entered();
|
||||
};
|
||||
const server = createServer((incoming, response) => {
|
||||
const versionRequest = incoming.url.replace(/\/$/, "") === "/json/version";
|
||||
if (versionRequest) versions += 1;
|
||||
if (
|
||||
enabled &&
|
||||
mode === "http-discovery" &&
|
||||
versions === 2 &&
|
||||
versionRequest
|
||||
) {
|
||||
stall(incoming.socket);
|
||||
return;
|
||||
}
|
||||
const outgoing = request(new URL(incoming.url, upstream), (received) => {
|
||||
const chunks = [];
|
||||
received.on("data", (chunk) => chunks.push(chunk));
|
||||
received.on("end", () => {
|
||||
const content = Buffer.concat(chunks).toString("utf8");
|
||||
response.writeHead(received.statusCode, {
|
||||
"content-type": "application/json",
|
||||
const server = createBrowserFixtureHttpServer(
|
||||
(incoming, response, target) => {
|
||||
const versionRequest =
|
||||
target.pathname.replace(/\/$/, "") === "/json/version";
|
||||
if (versionRequest) versions += 1;
|
||||
if (
|
||||
enabled &&
|
||||
mode === "http-discovery" &&
|
||||
versions === 2 &&
|
||||
versionRequest
|
||||
) {
|
||||
stall(incoming.socket);
|
||||
return;
|
||||
}
|
||||
const destination = new URL(upstream);
|
||||
destination.pathname = target.pathname;
|
||||
destination.search = target.search;
|
||||
const outgoing = request(destination, (received) => {
|
||||
const chunks = [];
|
||||
received.on("data", (chunk) => chunks.push(chunk));
|
||||
received.on("end", () => {
|
||||
const content = Buffer.concat(chunks).toString("utf8");
|
||||
response.writeHead(received.statusCode, {
|
||||
"content-type": "application/json",
|
||||
});
|
||||
response.end(
|
||||
content.replaceAll(
|
||||
`127.0.0.1:${upstream.port}`,
|
||||
`127.0.0.1:${server.address().port}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
response.end(
|
||||
content.replaceAll(
|
||||
`127.0.0.1:${upstream.port}`,
|
||||
`127.0.0.1:${server.address().port}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
outgoing.on("error", () => response.destroy());
|
||||
response.on("close", () => outgoing.destroy());
|
||||
outgoing.end();
|
||||
});
|
||||
outgoing.on("error", () => response.destroy());
|
||||
response.on("close", () => outgoing.destroy());
|
||||
outgoing.end();
|
||||
},
|
||||
);
|
||||
server.on("connection", (socket) => {
|
||||
sockets.add(socket);
|
||||
socket.on("close", () => sockets.delete(socket));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { once } from "node:events";
|
||||
import { createServer } from "node:http";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
|
||||
import { chromium } from "playwright-core";
|
||||
@@ -11,6 +10,7 @@ import { createBrowserScenarioProvider } from "../../dist/composition/browserSce
|
||||
import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
|
||||
import { runScenarioCli } from "./browser-scenario-verifier.mjs";
|
||||
import { verifyScenarioPolicySetup } from "./browser-scenario-service-worker-setup-e2e.mjs";
|
||||
import { createBrowserFixtureHttpServer } from "./browser-fixture-http.mjs";
|
||||
|
||||
const diagnosticError = (error) =>
|
||||
error instanceof Error
|
||||
@@ -26,38 +26,38 @@ const diagnosticError = (error) =>
|
||||
}
|
||||
: { kind: typeof error };
|
||||
|
||||
const startSite = async () => {
|
||||
export const startSite = async () => {
|
||||
let otherOrigin;
|
||||
const server = createServer((request, response) => {
|
||||
const path = new URL(request.url, "http://localhost").pathname;
|
||||
if (path === "/sw.js") {
|
||||
response.setHeader("content-type", "application/javascript");
|
||||
response.end(`self.addEventListener("install", () => self.skipWaiting());
|
||||
const server = createBrowserFixtureHttpServer(
|
||||
(_request, response, { pathname: path }) => {
|
||||
if (path === "/sw.js") {
|
||||
response.setHeader("content-type", "application/javascript");
|
||||
response.end(`self.addEventListener("install", () => self.skipWaiting());
|
||||
self.addEventListener("activate", event => event.waitUntil(clients.claim()));
|
||||
self.addEventListener("fetch", event => {
|
||||
if (new URL(event.request.url).pathname === "/probe")
|
||||
event.respondWith(new Response("worker"));
|
||||
});`);
|
||||
} else if (path === "/probe") {
|
||||
response.end("network");
|
||||
} else if (path === "/failed-navigation") {
|
||||
response.destroy();
|
||||
} else if (path === "/stalled-image.png") {
|
||||
response.writeHead(200, { "content-type": "image/png" });
|
||||
response.flushHeaders();
|
||||
} else if (path === "/pending-image" || path === "/pending-child") {
|
||||
response.setHeader("content-type", "text/html");
|
||||
response.end(
|
||||
'<!doctype html><img id="pending-image" src="/stalled-image.png">',
|
||||
);
|
||||
} else if (path === "/pending-frame") {
|
||||
response.setHeader("content-type", "text/html");
|
||||
response.end(
|
||||
'<!doctype html><iframe id="pending-frame" src="/pending-child"></iframe>',
|
||||
);
|
||||
} else {
|
||||
response.setHeader("content-type", "text/html");
|
||||
response.end(`<!doctype html><p id="state">ready</p>
|
||||
} else if (path === "/probe") {
|
||||
response.end("network");
|
||||
} else if (path === "/failed-navigation") {
|
||||
response.destroy();
|
||||
} else if (path === "/stalled-image.png") {
|
||||
response.writeHead(200, { "content-type": "image/png" });
|
||||
response.flushHeaders();
|
||||
} else if (path === "/pending-image" || path === "/pending-child") {
|
||||
response.setHeader("content-type", "text/html");
|
||||
response.end(
|
||||
'<!doctype html><img id="pending-image" src="/stalled-image.png">',
|
||||
);
|
||||
} else if (path === "/pending-frame") {
|
||||
response.setHeader("content-type", "text/html");
|
||||
response.end(
|
||||
'<!doctype html><iframe id="pending-frame" src="/pending-child"></iframe>',
|
||||
);
|
||||
} else {
|
||||
response.setHeader("content-type", "text/html");
|
||||
response.end(`<!doctype html><p id="state">ready</p>
|
||||
<a id="navigate" href="${otherOrigin}/">Navigate</a>
|
||||
${path === "/frame" ? `<iframe src="${otherOrigin}/"></iframe>` : ""}
|
||||
${
|
||||
@@ -72,8 +72,9 @@ const startSite = async () => {
|
||||
</script>`
|
||||
: ""
|
||||
}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
server.listen(0, "127.0.0.1");
|
||||
await once(server, "listening");
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createServer } from "node:http";
|
||||
import { once } from "node:events";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
scenarioProfiles,
|
||||
} from "./browser-scenario-verifier.mjs";
|
||||
import { withVerifierCleanup } from "./verifier-cleanup.mjs";
|
||||
import { createBrowserFixtureHttpServer } from "./browser-fixture-http.mjs";
|
||||
|
||||
const scenarioFor = (browser, origin) =>
|
||||
browserScenarioSchema.parse({
|
||||
@@ -149,28 +149,29 @@ const storagePage = (kind, port) => {
|
||||
</script></body></html>`;
|
||||
};
|
||||
|
||||
const startStorageSite = async () => {
|
||||
export const startStorageSite = async () => {
|
||||
let port = 0;
|
||||
const server = createServer((request, response) => {
|
||||
const path = new URL(request.url ?? "/", "http://127.0.0.1").pathname;
|
||||
const kind =
|
||||
path === "/storage-main"
|
||||
? "main"
|
||||
: path === "/storage-frame"
|
||||
? "frame"
|
||||
: path === "/storage-debugger"
|
||||
? "debugger"
|
||||
: path === "/storage-top"
|
||||
? "top"
|
||||
: undefined;
|
||||
if (kind === undefined) {
|
||||
response.writeHead(404).end();
|
||||
return;
|
||||
}
|
||||
response.setHeader("content-type", "text/html; charset=utf-8");
|
||||
response.end(storagePage(kind, port));
|
||||
});
|
||||
server.listen(0, "::");
|
||||
const server = createBrowserFixtureHttpServer(
|
||||
(_request, response, { pathname: path }) => {
|
||||
const kind =
|
||||
path === "/storage-main"
|
||||
? "main"
|
||||
: path === "/storage-frame"
|
||||
? "frame"
|
||||
: path === "/storage-debugger"
|
||||
? "debugger"
|
||||
: path === "/storage-top"
|
||||
? "top"
|
||||
: undefined;
|
||||
if (kind === undefined) {
|
||||
response.writeHead(404).end();
|
||||
return;
|
||||
}
|
||||
response.setHeader("content-type", "text/html; charset=utf-8");
|
||||
response.end(storagePage(kind, port));
|
||||
},
|
||||
);
|
||||
server.listen(0, "127.0.0.1");
|
||||
await once(server, "listening");
|
||||
port = server.address().port;
|
||||
return {
|
||||
|
||||
@@ -253,20 +253,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,
|
||||
@@ -395,21 +384,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"));
|
||||
}
|
||||
@@ -87,11 +87,50 @@ export async function verifyGhidraEntryAliases({
|
||||
(await call("procedure_info", { procedure: alias.value })).entrypoint,
|
||||
alias.address,
|
||||
);
|
||||
// Search matches the names exact lookup accepts, as one procedure row
|
||||
// under its primary name.
|
||||
const escaped = alias.value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
||||
for (const query of [
|
||||
{ pattern: alias.value },
|
||||
{ pattern: `^${escaped}$`, mode: "regex", case_sensitive: true },
|
||||
]) {
|
||||
assert.deepEqual(
|
||||
(await call("search_procedures", query))
|
||||
.filter(({ address }) => address === alias.address)
|
||||
.map(({ value, matched_aliases }) => ({ value, matched_aliases })),
|
||||
[{ value: canonical.value, matched_aliases: [alias.value] }],
|
||||
`Procedure search missed the entry alias for ${JSON.stringify(query)}`,
|
||||
);
|
||||
}
|
||||
assert.ok(
|
||||
(await call("list_procedures")).every(
|
||||
(row) => row.matched_aliases === undefined,
|
||||
),
|
||||
"Listings must not report search matches",
|
||||
);
|
||||
const traced = await call("trace_feature", { query: alias.value });
|
||||
assert.ok(
|
||||
traced.matches.some(
|
||||
({ type, address, matched_aliases }) =>
|
||||
type === "procedure" &&
|
||||
address === alias.address &&
|
||||
matched_aliases?.includes(alias.value) === true,
|
||||
),
|
||||
"trace_feature missed the procedure its entry alias names",
|
||||
);
|
||||
const interior = names.find(
|
||||
({ value }) => value === `${prefix}rea_interior`,
|
||||
);
|
||||
assert.ok(interior);
|
||||
assert.notEqual(interior.address, alias.address);
|
||||
assert.ok(
|
||||
(await call("search_procedures", { pattern: interior.value })).every(
|
||||
({ address, matched_aliases = [] }) =>
|
||||
address !== interior.address &&
|
||||
!matched_aliases.includes(interior.value),
|
||||
),
|
||||
"An interior label must not match procedure search",
|
||||
);
|
||||
assert.equal(
|
||||
interior.symbol.type,
|
||||
"label",
|
||||
|
||||
@@ -68,6 +68,21 @@ export async function verifyGhidraNamespaceAnnotations({
|
||||
assert.equal(ambiguous.code, "invalid_request");
|
||||
for (const { address } of entries)
|
||||
assert.ok(JSON.stringify(ambiguous.details.issues).includes(address));
|
||||
// Search accepts the leaf spelling exact lookup accepts, keeping each
|
||||
// procedure as one row under its qualified primary name.
|
||||
const leafMatches = await call("search_procedures", {
|
||||
pattern: "^same$",
|
||||
mode: "regex",
|
||||
case_sensitive: true,
|
||||
});
|
||||
for (const { namespace, address } of entries)
|
||||
assert.deepEqual(
|
||||
leafMatches
|
||||
.filter((row) => row.address === address)
|
||||
.map(({ value }) => value),
|
||||
[`${namespace}::same`],
|
||||
`Leaf-name search missed ${namespace}::same`,
|
||||
);
|
||||
for (const { namespace, address } of entries) {
|
||||
const leaf = await call("annotate_native_function", {
|
||||
procedure: address,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseEvidence } from "../../dist/domain/evidence.js";
|
||||
import { PrivateRuntimeRoot } from "../../dist/process/PrivateRuntimeRoot.js";
|
||||
import { withVerifierCleanup } from "./verifier-cleanup.mjs";
|
||||
import { createVerifierCommands } from "./verify-commands.mjs";
|
||||
|
||||
const source = fileURLToPath(
|
||||
new URL(
|
||||
"../../tests/conformance/ghidra/relocation-symbol.c",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
/** Verify named and unnamed ELF relocations through the shipped CLI and Ghidra. */
|
||||
export async function verifyGhidraRelocationSymbols({ clang, lld, env }) {
|
||||
const commands = await createVerifierCommands();
|
||||
let root;
|
||||
let commandsClosed = false;
|
||||
return withVerifierCleanup(async () => {
|
||||
root = await PrivateRuntimeRoot.create({ prefix: "rea-relocation-proof-" });
|
||||
const object = join(root.path, "relocation-symbol.o");
|
||||
const target = join(root.path, "relocation-symbol.so");
|
||||
const commandEnv = {
|
||||
...env,
|
||||
TMPDIR: root.path,
|
||||
TMP: root.path,
|
||||
TEMP: root.path,
|
||||
};
|
||||
const run = (command, args) =>
|
||||
commands.exec(command, args, {
|
||||
env: commandEnv,
|
||||
timeout: 120000,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
});
|
||||
await run(clang, [
|
||||
"--target=x86_64-linux-gnu",
|
||||
"-fPIC",
|
||||
"-O0",
|
||||
"-c",
|
||||
source,
|
||||
"-o",
|
||||
object,
|
||||
]);
|
||||
await run(lld, ["-shared", object, "-o", target]);
|
||||
const hash = async () =>
|
||||
createHash("sha256")
|
||||
.update(await readFile(target))
|
||||
.digest("hex");
|
||||
const before = await hash();
|
||||
const { stdout } = await run(process.execPath, [
|
||||
"scripts/rea.mjs",
|
||||
"inspect-native-load-image",
|
||||
target,
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
]);
|
||||
const evidence = parseEvidence(JSON.parse(stdout));
|
||||
assert.equal(evidence.subject?.digest.sha256, before);
|
||||
assert.equal(evidence.provider.id, "ghidra");
|
||||
const report = evidence.normalized_result;
|
||||
assert.equal(report.status, "unsupported");
|
||||
const relocations = report.observations.relocations;
|
||||
assert.ok(
|
||||
relocations.some(({ symbol }) => symbol === "rea_external"),
|
||||
"The Ghidra relocation must retain its imported symbol",
|
||||
);
|
||||
assert.ok(
|
||||
relocations.some(({ symbol }) => symbol === ""),
|
||||
"The provider's empty symbol must remain distinct from an unrecorded field",
|
||||
);
|
||||
assert.equal(await hash(), before);
|
||||
return {
|
||||
source,
|
||||
target_sha256: before,
|
||||
named_symbol: "rea_external",
|
||||
empty_symbol: true,
|
||||
};
|
||||
}, [
|
||||
async () => {
|
||||
await commands.close();
|
||||
commandsClosed = true;
|
||||
},
|
||||
async () => {
|
||||
if (commandsClosed && root !== undefined) await root.close();
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -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")),
|
||||
) {
|
||||
|
||||
@@ -10,5 +10,10 @@ export function compareTestDiscovery(
|
||||
readonly file: string;
|
||||
readonly projects: readonly string[];
|
||||
}[];
|
||||
readonly wrongProjects: readonly {
|
||||
readonly file: string;
|
||||
readonly expected: string;
|
||||
readonly actual: string;
|
||||
}[];
|
||||
readonly discovered: number;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { relative, resolve } from "node:path";
|
||||
import { projectForTest } from "./test-projects.mjs";
|
||||
|
||||
/** Compare actual Vitest discovery with repository-owned test paths. */
|
||||
export const compareTestDiscovery = (expectedPaths, discovery, root) => {
|
||||
@@ -36,6 +37,14 @@ export const compareTestDiscovery = (expectedPaths, discovery, root) => {
|
||||
const projects = projectsByFile.get(file);
|
||||
return projects.length > 1 ? [{ file, projects: sorted(projects) }] : [];
|
||||
}),
|
||||
wrongProjects: sorted(expected).flatMap((file) => {
|
||||
const projects = projectsByFile.get(file);
|
||||
if (!projects || projects.length !== 1) return [];
|
||||
const expectedProject = projectForTest(file).name;
|
||||
return projects[0] === expectedProject
|
||||
? []
|
||||
: [{ file, expected: expectedProject, actual: projects[0] }];
|
||||
}),
|
||||
discovered: projectsByFile.size,
|
||||
};
|
||||
};
|
||||
@@ -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__":
|
||||
|
||||
@@ -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()
|
||||
@@ -539,11 +539,13 @@ class WebsiteMaintenanceChecks(unittest.TestCase):
|
||||
self.assertIn(b"color: black", response.read())
|
||||
with self.assertRaises(HTTPError) as error:
|
||||
urlopen(base + prefix + "missing")
|
||||
self.assertEqual(error.exception.code, 404)
|
||||
with contextlib.closing(error.exception) as response:
|
||||
self.assertEqual(response.code, 404)
|
||||
if prefix != "/":
|
||||
with self.assertRaises(HTTPError) as error:
|
||||
urlopen(base + "/guide/")
|
||||
self.assertEqual(error.exception.code, 404)
|
||||
with contextlib.closing(error.exception) as response:
|
||||
self.assertEqual(response.code, 404)
|
||||
finally:
|
||||
server.shutdown()
|
||||
worker.join()
|
||||
|
||||
+9
-78
@@ -15,8 +15,10 @@ if (options.help) {
|
||||
process.stdout.write(
|
||||
"Usage: npm run verify:adb [-- --serial SERIAL --pull PACKAGE]\n" +
|
||||
"Requires a caller-selected adb binary (REA_ADB_PATH or PATH) and a\n" +
|
||||
"connected device or emulator. Verifies all five operations against\n" +
|
||||
"the live device; --pull acquires a real package and re-digests the\n" +
|
||||
"connected device or emulator. Verifies observation and transfer operations\n" +
|
||||
"against the live device. --install-apk is unavailable until APK identity\n" +
|
||||
"and device cleanup ownership can be established safely.\n" +
|
||||
"--pull acquires a real package and re-digests the\n" +
|
||||
"files on disk against the returned SHA-256 values.\n",
|
||||
);
|
||||
process.exit(0);
|
||||
@@ -356,79 +358,6 @@ try {
|
||||
`versionCode=${String(details.version_code)}`,
|
||||
);
|
||||
}
|
||||
if (options.installApk !== null && options.installApk !== undefined) {
|
||||
const probePackage = "com.rea.adb.probe";
|
||||
const installedProbe = await run("install_adb_package", {
|
||||
serial,
|
||||
apk_path: options.installApk,
|
||||
replace: false,
|
||||
});
|
||||
if (installedProbe !== null) {
|
||||
check(
|
||||
"install reports the APK digest",
|
||||
typeof installedProbe.sha256 === "string" &&
|
||||
installedProbe.sha256.length === 64,
|
||||
`${installedProbe.bytes} bytes`,
|
||||
);
|
||||
const resolved = await run("resolve_adb_packages", {
|
||||
serial,
|
||||
query: "rea.adb.probe",
|
||||
});
|
||||
if (resolved !== null)
|
||||
check(
|
||||
"resolve finds the installed probe uniquely",
|
||||
resolved.matches.length === 1 &&
|
||||
resolved.matches[0]?.package_name === probePackage,
|
||||
`${resolved.matches.length} match(es)`,
|
||||
);
|
||||
const started = await run("start_adb_app", {
|
||||
serial,
|
||||
package: probePackage,
|
||||
});
|
||||
if (started !== null)
|
||||
check(
|
||||
"app start resolves and launches the launcher activity",
|
||||
typeof started.component === "string" &&
|
||||
started.component.startsWith(probePackage),
|
||||
String(started.component),
|
||||
);
|
||||
const observedProcesses = await run("list_adb_processes", { serial });
|
||||
if (observedProcesses !== null)
|
||||
check(
|
||||
"started app appears in the process listing",
|
||||
observedProcesses.processes.some(
|
||||
(process) => process.name === probePackage,
|
||||
),
|
||||
);
|
||||
const stopped = await run("stop_adb_app", {
|
||||
serial,
|
||||
package: probePackage,
|
||||
});
|
||||
if (stopped !== null)
|
||||
check(
|
||||
"force-stop completes the dynamic-analysis run",
|
||||
stopped.package_name === probePackage,
|
||||
);
|
||||
const removed = await run("uninstall_adb_package", {
|
||||
serial,
|
||||
package: probePackage,
|
||||
});
|
||||
if (removed !== null)
|
||||
check(
|
||||
"uninstall completes the lifecycle",
|
||||
removed.package_name === probePackage,
|
||||
);
|
||||
const afterRemoval = await run("resolve_adb_packages", {
|
||||
serial,
|
||||
query: "rea.adb.probe",
|
||||
});
|
||||
if (afterRemoval !== null)
|
||||
check(
|
||||
"resolve reports zero matches after removal",
|
||||
afterRemoval.matches.length === 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await rm(transferRoot, { recursive: true, force: true });
|
||||
}
|
||||
@@ -459,7 +388,7 @@ if (failures > 0) {
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const parsed = { help: false, serial: null, pull: null, installApk: null };
|
||||
const parsed = { help: false, serial: null, pull: null };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const argument = argv[index];
|
||||
if (argument === "--help" || argument === "-h") parsed.help = true;
|
||||
@@ -470,8 +399,10 @@ function parseArguments(argv) {
|
||||
parsed.pull = argv[index + 1] ?? null;
|
||||
index += 1;
|
||||
} else if (argument === "--install-apk") {
|
||||
parsed.installApk = argv[index + 1] ?? null;
|
||||
index += 1;
|
||||
process.stderr.write(
|
||||
"--install-apk is unavailable: APK identity and device cleanup ownership cannot be established safely\n",
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
process.stderr.write(`unknown argument: ${argument}\n`);
|
||||
process.exit(2);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
import { createVerifierCommands } from "./lib/verify-commands.mjs";
|
||||
|
||||
const root = await PrivateRuntimeRoot.create({ prefix: "rea-bun-verifier-" });
|
||||
const commands = await createVerifierCommands();
|
||||
let commands;
|
||||
const run = (command, args, options = {}) =>
|
||||
commands.exec(command, args, {
|
||||
timeout: 120_000,
|
||||
@@ -19,6 +19,7 @@ const run = (command, args, options = {}) =>
|
||||
// only after every owned command family has settled.
|
||||
let commandsClosed = false;
|
||||
await withVerifierCleanup(async () => {
|
||||
commands = await createVerifierCommands();
|
||||
const source = join(root.path, "hello.ts");
|
||||
await writeFile(source, 'console.log("REA_BUN_VERIFICATION");\n');
|
||||
const docker = process.argv.includes("--docker");
|
||||
@@ -104,7 +105,7 @@ await withVerifierCleanup(async () => {
|
||||
);
|
||||
}, [
|
||||
async () => {
|
||||
await commands.close();
|
||||
if (commands !== undefined) await commands.close();
|
||||
commandsClosed = true;
|
||||
},
|
||||
async () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import { createCatalogIdentity } from "../dist/catalogIdentity.js";
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
TOOL_CONTRACTS,
|
||||
toolContract,
|
||||
} from "../dist/contracts/toolContracts.js";
|
||||
import { isPathWithinRoot } from "../dist/domain/localPath.js";
|
||||
import { createVerifierCommands } from "./lib/verify-commands.mjs";
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
import { saveVerifierCommandOutput } from "./lib/verifier-command-output.mjs";
|
||||
@@ -306,7 +307,9 @@ await withVerifierCleanup(async () => {
|
||||
assert(
|
||||
parsed.plannedActions.every(
|
||||
(a) =>
|
||||
a.target === configPath || a.target.startsWith(skillDirectory),
|
||||
a.target === configPath ||
|
||||
(isAbsolute(a.target) &&
|
||||
isPathWithinRoot(skillDirectory, a.target)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { isAbsolute, join, relative } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
|
||||
import { isPathWithinRoot } from "../dist/domain/localPath.js";
|
||||
import { createVerifierCommands } from "./lib/verify-commands.mjs";
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
import { saveVerifierCommandOutput } from "./lib/verifier-command-output.mjs";
|
||||
@@ -54,6 +55,7 @@ const expectedNames = TOOL_CONTRACTS.map(
|
||||
).sort();
|
||||
let clientVersion = null;
|
||||
let expectedSkill;
|
||||
let skillCallId;
|
||||
let inventoryChecked = false;
|
||||
let catalogSize = null;
|
||||
let skillLoaded = false;
|
||||
@@ -180,10 +182,24 @@ await withVerifierCleanup(async () => {
|
||||
finalSeen = true;
|
||||
return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
|
||||
}
|
||||
if (stage === 0)
|
||||
return call("activate_skill", { name: "reverse-engineer-anything" });
|
||||
if (stage === 0) {
|
||||
const request = call("activate_skill", {
|
||||
name: "reverse-engineer-anything",
|
||||
});
|
||||
skillCallId = request.tool_calls[0].id;
|
||||
return request;
|
||||
}
|
||||
assert(
|
||||
strings.some((text) => text.includes(expectedSkill)),
|
||||
skillCallId !== undefined &&
|
||||
expectedSkill !== undefined &&
|
||||
body.messages.some(
|
||||
(message) =>
|
||||
message.role === "tool" &&
|
||||
message.tool_call_id === skillCallId &&
|
||||
textValues(message.content).some((text) =>
|
||||
text.includes(expectedSkill),
|
||||
),
|
||||
),
|
||||
"The model must receive the complete activated skill body",
|
||||
);
|
||||
skillLoaded = true;
|
||||
@@ -322,8 +338,8 @@ await withVerifierCleanup(async () => {
|
||||
result.plannedActions.every(
|
||||
(action) =>
|
||||
action.target === configPath ||
|
||||
action.target === skillDirectory ||
|
||||
action.target.startsWith(skillDirectory + "/"),
|
||||
(isAbsolute(action.target) &&
|
||||
isPathWithinRoot(skillDirectory, action.target)),
|
||||
),
|
||||
"Refuse setup writes outside the disposable account's configuration and skill",
|
||||
);
|
||||
@@ -342,9 +358,16 @@ await withVerifierCleanup(async () => {
|
||||
assert.deepEqual(repeated.appliedActions, []);
|
||||
assert.equal(await readFile(configPath, "utf8"), configured);
|
||||
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
|
||||
expectedSkill = (await readFile(join(skillDirectory, "SKILL.md"), "utf8"))
|
||||
.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "")
|
||||
.trim();
|
||||
const installedSkill = await readFile(
|
||||
join(skillDirectory, "SKILL.md"),
|
||||
"utf8",
|
||||
);
|
||||
const frontmatter = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/u.exec(
|
||||
installedSkill,
|
||||
);
|
||||
assert(frontmatter, "Installed skill must have authored frontmatter");
|
||||
expectedSkill = installedSkill.slice(frontmatter[0].length).trim();
|
||||
assert(expectedSkill.length > 0, "Installed skill body must not be empty");
|
||||
const response = await saveVerifierCommandOutput(
|
||||
() =>
|
||||
commands.exec(
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { advertisedInputJsonSchema } from "../dist/contracts/toolSchemaMetadata.js";
|
||||
import { presentAdvertisedInputSchema } from "../dist/contracts/compactInputSchema.js";
|
||||
import { compactPresentationForMcpInputSchemaProfile } from "../dist/config/mcpInputSchemaProfile.js";
|
||||
import { isPathWithinRoot } from "../dist/domain/localPath.js";
|
||||
import { createVerifierCommands } from "./lib/verify-commands.mjs";
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
import { saveVerifierCommandOutput } from "./lib/verifier-command-output.mjs";
|
||||
@@ -300,8 +301,8 @@ await withVerifierCleanup(async () => {
|
||||
parsed.plannedActions.every(
|
||||
(a) =>
|
||||
a.target === configPath ||
|
||||
a.target === skillDirectory ||
|
||||
a.target.startsWith(skillDirectory + "/"),
|
||||
(isAbsolute(a.target) &&
|
||||
isPathWithinRoot(skillDirectory, a.target)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ let catalog;
|
||||
let patternCount = 0;
|
||||
let skillRequested = false;
|
||||
let skillLoaded = false;
|
||||
let expectedSkillBody;
|
||||
let evidenceSeen = false;
|
||||
let finalSeen = false;
|
||||
let artifactDigest;
|
||||
@@ -169,11 +170,34 @@ await withVerifierCleanup(async () => {
|
||||
const toolMessages = (body.messages ?? []).filter(
|
||||
(message) => message.role === "tool",
|
||||
);
|
||||
const serialized = JSON.stringify(toolMessages);
|
||||
skillLoaded ||=
|
||||
serialized.includes(
|
||||
'<skill_content name=\\"reverse-engineer-anything\\">',
|
||||
) && serialized.includes(dirname(skillPath));
|
||||
skillLoaded ||= toolMessages.some((message) => {
|
||||
if (
|
||||
typeof message.content !== "string" ||
|
||||
expectedSkillBody === undefined
|
||||
)
|
||||
return false;
|
||||
const open =
|
||||
'<skill_content name="reverse-engineer-anything">\n<skill_resources>\n';
|
||||
const start = message.content.indexOf(open);
|
||||
if (start < 0) return false;
|
||||
const resourcesStart = start + open.length;
|
||||
const instructions = `\n</skill_resources>\n\n<skill_instructions>\n${expectedSkillBody}\n</skill_instructions>\n</skill_content>`;
|
||||
const resourcesEnd = message.content.indexOf(
|
||||
instructions,
|
||||
resourcesStart,
|
||||
);
|
||||
if (resourcesEnd < 0) return false;
|
||||
const expectedPath = dirname(skillPath)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">");
|
||||
const resources = message.content.slice(resourcesStart, resourcesEnd);
|
||||
return (
|
||||
!resources.includes("</skill_content>") &&
|
||||
!resources.includes("</skill_resources>") &&
|
||||
resources.includes(expectedPath)
|
||||
);
|
||||
});
|
||||
for (const message of toolMessages) {
|
||||
const found = findClientResult(
|
||||
message.content,
|
||||
@@ -281,11 +305,15 @@ await withVerifierCleanup(async () => {
|
||||
const configured = await readFile(configPath, "utf8");
|
||||
assert.deepEqual(withoutRea(configured), withoutRea(original));
|
||||
assert(configured.includes("!!js process.cwd()"));
|
||||
assert(
|
||||
(await readFile(skillPath, "utf8")).includes(
|
||||
"name: reverse-engineer-anything",
|
||||
),
|
||||
const installedSkill = await readFile(skillPath, "utf8");
|
||||
assert(installedSkill.includes("name: reverse-engineer-anything"));
|
||||
const frontmatter = /^---\r?\n[\s\S]*?^---(?:\r?\n|$)/mu.exec(
|
||||
installedSkill,
|
||||
);
|
||||
assert(frontmatter, "Installed skill must have its authored frontmatter");
|
||||
expectedSkillBody = installedSkill.slice(frontmatter[0].length).trim();
|
||||
assert(expectedSkillBody.length > 0);
|
||||
|
||||
lifecycle.push("setup");
|
||||
const repeated = await runRea(
|
||||
["setup", "--client", "dsh", "--yes"],
|
||||
|
||||
@@ -104,6 +104,10 @@ await withVerifierCleanup(
|
||||
}
|
||||
// The same checks apply to the default selection and each remaining ABI.
|
||||
const preferred = await run("inspect_dart_aot", { path: apk });
|
||||
assert.ok(
|
||||
abis.some((abi) => abi.abi === preferred.abi),
|
||||
"Default AOT selection must be one of the identified ABIs",
|
||||
);
|
||||
for (const abi of abis) {
|
||||
const aot =
|
||||
abi.abi === preferred.abi
|
||||
|
||||
@@ -2,7 +2,7 @@ import { basename } from "node:path";
|
||||
|
||||
import { FridaInstrumentationService } from "../dist/application/frida/FridaInstrumentationService.js";
|
||||
import { FridaInstrumentationManager } from "../dist/frida/FridaInstrumentationManager.js";
|
||||
import { isValidFridaRemoteAddress } from "../dist/contracts/fridaRemoteAddress.js";
|
||||
import { isValidFridaRemoteAddress } from "../dist/domain/fridaRemoteAddress.js";
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
@@ -80,6 +80,10 @@ try {
|
||||
!Array.isArray(observed.messages)
|
||||
)
|
||||
throw new Error("Frida returned no message observation");
|
||||
if (!Number.isSafeInteger(observed.pid) || observed.pid <= 0)
|
||||
throw new Error("Frida returned no valid target PID");
|
||||
if (remote !== undefined && observed.pid !== options.pid)
|
||||
throw new Error("Frida observed a different target PID");
|
||||
const messageObserved = observed.messages.some(
|
||||
(message) =>
|
||||
typeof message === "object" &&
|
||||
|
||||
Loaded 100 of 598 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user