mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
Compare 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 |
No files matched your search
@@ -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: |
|
||||
|
||||
@@ -154,6 +154,15 @@ jobs:
|
||||
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:
|
||||
@@ -169,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}")
|
||||
@@ -189,21 +194,20 @@ jobs:
|
||||
if pending:
|
||||
sys.exit("Could not verify published revision on: " + ", ".join(pending))
|
||||
for name, base in hosts.items():
|
||||
with urlopen(urljoin(base, ".well-known/agent-skills/index.json"), timeout=15) as response:
|
||||
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 urlopen(artifact_url, timeout=15) as response:
|
||||
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 urlopen(urljoin(base, ".well-known/" + manifest), timeout=15) as response:
|
||||
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":
|
||||
request = Request(base, headers={"Accept": "text/markdown"})
|
||||
with urlopen(request, timeout=15) as response:
|
||||
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")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -147,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.
|
||||
|
||||
@@ -227,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
|
||||
@@ -388,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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+10
-7
@@ -269,8 +269,8 @@ APK for the negative check. No engine or external tool is involved. Record:
|
||||
Flutter Gallery 2.9.2 (112 MB, arm64-v8a / armeabi-v7a / x86_64) — every
|
||||
ABI reports the same agreeing snapshot hash
|
||||
(`65817c30a78bb44c3dc3771876b6010a`, two sections each) read 20 bytes after
|
||||
the Dart snapshot magic, per-ABI engine GNU build-ids, no labeled Dart
|
||||
version string (expected for a release engine build), and the Android
|
||||
the Dart snapshot magic, per-ABI engine GNU build-ids, an unlabeled
|
||||
Dart-shaped version candidate, and the Android
|
||||
clang/LLVM toolchain line; the AOT inspection locates all four snapshot
|
||||
symbols through the dynamic symbol table on both ELF64 and ELF32 images,
|
||||
validates the data-section headers, agrees with the identification hash,
|
||||
@@ -284,7 +284,7 @@ prefixes, with no private local-file dependency. The verifier now compares
|
||||
compiled CLI and stdio MCP Evidence on every ABI, including data-header kind
|
||||
and declared lengths. Add
|
||||
`--expectations scripts/fixtures/flutter-gallery-expectations.json` for the
|
||||
documented Gallery APK to assert its pinned digest, hash, exact engine labels
|
||||
documented Gallery APK to assert its pinned digest, hash, exact engine strings
|
||||
and per-ABI URI counts. The verifier requires complete structural snapshot
|
||||
coverage, reports bounded pool coverage as partial/unverified, and reports
|
||||
exact labels/counts on other APKs unverified without an expectations file.
|
||||
@@ -421,7 +421,10 @@ the first cold pseudocode answer comes from a single-function run or an export
|
||||
that already finished, assembly waits for and serves from that export, and
|
||||
pseudocode and both dossier facets match their recorded file spans and digests,
|
||||
a fresh server reuses the export without rebuilding it, staging is empty after
|
||||
a completed export, and no owned process survives. Pass
|
||||
a completed export, and no owned process survives. A separate forced 1 ms
|
||||
project deadline checks that a timed-out inventory recommends raising the
|
||||
project deadline without changing jobs; a fresh server retries with 60 seconds
|
||||
and the same two-worker setting. Pass
|
||||
`--targets DIR --functions a,b` to add caller-built binaries laid out as
|
||||
`DIR/<flavor>/<name>` beside `<name>.stripped`; addresses come from the
|
||||
symbol-bearing build. `--compare-ghidra` with `GHIDRA_INSTALL_DIR` runs those
|
||||
@@ -1108,13 +1111,13 @@ coverage limits.
|
||||
### Offline binary layout
|
||||
|
||||
`npm run verify:binary:layout` requires Linux x64, GCC/binutils, the
|
||||
`i686-linux-gnu`, `arm-linux-gnueabi`, `mips-linux-gnu` and `mipsel-linux-gnu`
|
||||
cross `gcc`/`readelf` toolchains, absolute
|
||||
`aarch64-linux-gnu`, `i686-linux-gnu`, `arm-linux-gnueabi`, `mips-linux-gnu` and
|
||||
`mipsel-linux-gnu` cross `gcc`/`readelf` toolchains, absolute
|
||||
`REA_PWNTOOLS_PYTHON` with pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2 and
|
||||
absolute `REA_VERIFY_STRACE_COMMAND`. It compiles ephemeral source-owned ELF
|
||||
fixtures and checks public CLI/MCP, lossless addresses/names, file ranges,
|
||||
mitigation inferences, malformed/unsupported input, original file hashes and
|
||||
released process ownership. Cross-compiled ELF32 executables and relocatable
|
||||
released process ownership. Cross-compiled ELF64 AArch64 and ELF32 executables and relocatable
|
||||
objects check the reported class, machine and byte order, and every section
|
||||
name, address, offset and size against GNU `readelf`. Exec syscall tracing must identify only the declared
|
||||
Node/Python launchers; no target binary is executed. Core/debugger claims need
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)"
|
||||
]
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -28,7 +28,10 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { inspectKunaInstallation } from "../dist/kuna/KunaInstallation.js";
|
||||
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
|
||||
import { AnalysisCleanupLease } from "../dist/artifacts/AnalysisCleanupLease.js";
|
||||
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
|
||||
import {
|
||||
requireMcpOperationResult,
|
||||
requireMcpToolError,
|
||||
} from "./lib/mcp-verifier-results.mjs";
|
||||
import { verifyKunaReferences } from "./lib/kuna-reference-verifier.mjs";
|
||||
import {
|
||||
nativeKunaStrings,
|
||||
@@ -172,7 +175,7 @@ const sampleTree = async (root) => {
|
||||
};
|
||||
|
||||
/** One MCP server with a selected provider, sampled while it runs. */
|
||||
const openServer = async (provider) => {
|
||||
const openServer = async (provider, kunaSettings = {}) => {
|
||||
const owner = Symbol(provider.id);
|
||||
pendingServers.add(owner);
|
||||
const transport = new StdioClientTransport({
|
||||
@@ -185,6 +188,7 @@ const openServer = async (provider) => {
|
||||
REA_PROCESS_RUN_ID: run.run_id,
|
||||
REA_KUNA_COMMAND: kuna,
|
||||
REA_KUNA_CACHE_DIR: cacheRoot,
|
||||
...kunaSettings,
|
||||
...(process.env.GHIDRA_INSTALL_DIR === undefined
|
||||
? {}
|
||||
: { GHIDRA_INSTALL_DIR: process.env.GHIDRA_INSTALL_DIR }),
|
||||
@@ -229,6 +233,13 @@ const openServer = async (provider) => {
|
||||
structured: result.structuredContent,
|
||||
};
|
||||
};
|
||||
const callError = async (name, arguments_ = {}) => {
|
||||
const result = await client.callTool(
|
||||
{ name, arguments: arguments_ },
|
||||
{ timeout: 3_600_000 },
|
||||
);
|
||||
return requireMcpToolError(result);
|
||||
};
|
||||
const close = async (priorFailure) => {
|
||||
clearInterval(timer);
|
||||
await sample();
|
||||
@@ -270,7 +281,59 @@ const openServer = async (provider) => {
|
||||
// A rejected connection/target still owns its transport and workspace.
|
||||
await close(cause);
|
||||
}
|
||||
return { call, close };
|
||||
return { call, callError, close };
|
||||
};
|
||||
|
||||
/** Verify a forced inventory timeout can be retried without changing jobs. */
|
||||
const verifyProjectTimeoutRecovery = async (path, address) => {
|
||||
const before = createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex");
|
||||
const settings = {
|
||||
REA_KUNA_JOBS: "2",
|
||||
REA_KUNA_TIMEOUT_MS: "60000",
|
||||
REA_KUNA_PROJECT_TIMEOUT_MS: "1",
|
||||
};
|
||||
const timed = await openServer({ path, id: "kuna" }, settings);
|
||||
let timedFailure;
|
||||
try {
|
||||
const failure = await timed.callError("procedure_assembly", {
|
||||
procedure: address,
|
||||
});
|
||||
assert.equal(failure.code, "capability_unavailable");
|
||||
assert.match(failure.message, /kuna functions/u);
|
||||
assert.match(failure.remediation.action, /REA_KUNA_PROJECT_TIMEOUT_MS/u);
|
||||
assert.doesNotMatch(failure.remediation.action, /REA_KUNA_JOBS/u);
|
||||
} catch (cause) {
|
||||
timedFailure = cause;
|
||||
} finally {
|
||||
await timed.close(timedFailure);
|
||||
}
|
||||
const retry = await openServer(
|
||||
{ path, id: "kuna" },
|
||||
{ ...settings, REA_KUNA_PROJECT_TIMEOUT_MS: "60000" },
|
||||
);
|
||||
let retryFailure;
|
||||
try {
|
||||
const assembly = await retry.call("procedure_assembly", {
|
||||
procedure: address,
|
||||
});
|
||||
assert.equal(typeof assembly.value, "string");
|
||||
assert.ok(assembly.value.length > 0);
|
||||
assert.equal(assembly.structured.analysis_profile.parameters.jobs, 2);
|
||||
} catch (cause) {
|
||||
retryFailure = cause;
|
||||
} finally {
|
||||
await retry.close(retryFailure);
|
||||
}
|
||||
assert.equal(
|
||||
createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex"),
|
||||
before,
|
||||
"Kuna timeout recovery changed the target",
|
||||
);
|
||||
return { forced_deadline_ms: 1, retry_deadline_ms: 60000, jobs: 2 };
|
||||
};
|
||||
|
||||
/** Count structuring and diagnostic markers in provider pseudocode. */
|
||||
@@ -535,6 +598,7 @@ const verifyVariant = async ({ flavor, directory, symbols }, variant) => {
|
||||
};
|
||||
|
||||
const results = [];
|
||||
let timeoutRecovery;
|
||||
const failures = [];
|
||||
let inspectionResourcesClean = true;
|
||||
try {
|
||||
@@ -596,6 +660,11 @@ try {
|
||||
}),
|
||||
});
|
||||
|
||||
timeoutRecovery = await verifyProjectTimeoutRecovery(
|
||||
built,
|
||||
fixtureFunctions.get("rea_checksum"),
|
||||
);
|
||||
|
||||
// Caller-built matrix.
|
||||
for (const pair of callerPairs)
|
||||
for (const variant of [pair.file, `${pair.file}.stripped`])
|
||||
@@ -650,6 +719,9 @@ const report = {
|
||||
platform: `${process.platform}/${process.arch}`,
|
||||
verifier_run: lifecycle,
|
||||
results,
|
||||
...(timeoutRecovery === undefined
|
||||
? {}
|
||||
: { timeout_recovery: timeoutRecovery }),
|
||||
limitations: [
|
||||
"Targets are analyzed statically; they are never executed.",
|
||||
"Memory is the sampled peak resident set of the MCP server process tree; CPU is its sampled cumulative time. A short-lived peak between 200 ms samples is not observed.",
|
||||
|
||||
@@ -18,6 +18,10 @@ import type {
|
||||
EvidenceSubjectTarget,
|
||||
} from "../domain/evidence.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import {
|
||||
analysisSourceDescriptorSchema,
|
||||
type AnalysisSourceDescriptor,
|
||||
} from "../domain/analysisSource.js";
|
||||
import type { ProgressReporter } from "./ProgressReporter.js";
|
||||
import type { ProviderRejectionCode } from "../domain/providerSelection.js";
|
||||
import type { ProcessLineageObservation } from "../process/ProcessOwnership.js";
|
||||
@@ -56,6 +60,12 @@ export type AnalysisOperation =
|
||||
/** Atomic successful provider observation with exact producing provenance. */
|
||||
export interface AnalysisExecution extends AnalysisObservation {
|
||||
readonly provider: ProviderIdentity;
|
||||
/**
|
||||
* Compact provider-owned facts for this execution. This is not an Evidence
|
||||
* record and is not restored by snapshot replay; consumers must treat its
|
||||
* absence as unknown.
|
||||
*/
|
||||
readonly sourceDescriptor?: AnalysisSourceDescriptor;
|
||||
/** Session-issued identity of the committed record consumed by this execution. */
|
||||
readonly evidenceId?: string;
|
||||
/** Result-specific veto of snapshot replay; Evidence remains retainable. */
|
||||
@@ -71,6 +81,7 @@ export interface AnalysisExecution extends AnalysisObservation {
|
||||
|
||||
interface CreateAnalysisExecutionOptions {
|
||||
readonly rawResult?: unknown;
|
||||
readonly sourceDescriptor?: AnalysisSourceDescriptor;
|
||||
readonly analysisProfile?: AnalysisProfileCommitment;
|
||||
readonly cachePolicy?: "live";
|
||||
readonly limitations?: readonly string[];
|
||||
@@ -110,6 +121,13 @@ export function createAnalysisExecution(
|
||||
? null
|
||||
: jsonValueSchema.parse(options.rawResult),
|
||||
provider,
|
||||
...(options.sourceDescriptor === undefined
|
||||
? {}
|
||||
: {
|
||||
sourceDescriptor: analysisSourceDescriptorSchema.parse(
|
||||
options.sourceDescriptor,
|
||||
),
|
||||
}),
|
||||
...(options.cachePolicy === undefined
|
||||
? {}
|
||||
: { cachePolicy: options.cachePolicy }),
|
||||
@@ -275,6 +293,7 @@ export type CapabilityDescriptor = CapabilityAvailability & {
|
||||
* exclusions apply to every policy.
|
||||
*/
|
||||
readonly cachePolicy?: "snapshot" | "live";
|
||||
readonly independentDecompile?: true;
|
||||
/** Exact source profile for an operation implemented by a versioned local reader. */
|
||||
readonly analysisProfile?: AnalysisProfileCommitment;
|
||||
readonly provider: ProviderIdentity;
|
||||
|
||||
@@ -38,7 +38,10 @@ import {
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { access, stat } from "node:fs/promises";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { requiresNativeSourceAdmissionBeforeReplay } from "./binary/AnalysisSnapshotCache.js";
|
||||
import {
|
||||
requestsFreshIndependentDecompile,
|
||||
requiresNativeSourceAdmissionBeforeReplay,
|
||||
} from "./binary/AnalysisSnapshotCache.js";
|
||||
import { classifyBinaryTargetFilesystemFailure } from "./BinaryTargetResolver.js";
|
||||
import {
|
||||
snapshotBindingMismatch,
|
||||
@@ -446,6 +449,7 @@ const runAnalysis = async (
|
||||
) &&
|
||||
tool !== "trace_native_ui_action" &&
|
||||
tool !== "decompile_all" &&
|
||||
!requestsFreshIndependentDecompile(tool, arguments_) &&
|
||||
routeAllowsSnapshotReplay(route, tool)
|
||||
) {
|
||||
const cached = snapshotEvidenceForQuery(snapshot, {
|
||||
|
||||
@@ -125,6 +125,8 @@ export interface ReferenceSourceImportOptions {
|
||||
export const DEFAULT_REFERENCE_SOURCE_IGNORE_PATTERNS = [
|
||||
".git/",
|
||||
".git/hooks/",
|
||||
// Match pointer files after directory rules to preserve their provenance.
|
||||
".git",
|
||||
"node_modules/",
|
||||
"dist/",
|
||||
"build/",
|
||||
|
||||
@@ -301,10 +301,13 @@ describe("setup client selection", () => {
|
||||
configPath: "/codex.toml",
|
||||
format: "toml" as const,
|
||||
};
|
||||
host.clients = [codex];
|
||||
host.clients = [
|
||||
codex,
|
||||
{ name: "grok_bot", configPath: "/grokbot", format: "unsupported" },
|
||||
];
|
||||
host.availableClients = [
|
||||
{ name: "cursor", configPath: "/cursor.json" },
|
||||
codex,
|
||||
...host.clients,
|
||||
];
|
||||
|
||||
const result = await runSetup(
|
||||
@@ -331,6 +334,31 @@ describe("setup client selection", () => {
|
||||
expect(host.skillInstalls).toBe(0);
|
||||
});
|
||||
|
||||
it.each([undefined, false])(
|
||||
"omits manual clients from automatic readiness while preserving detection (skill: %s)",
|
||||
async (installSkill) => {
|
||||
const host = new FakeSetupHost();
|
||||
host.clients = [
|
||||
{ name: "grok_bot", configPath: "/grokbot", format: "unsupported" },
|
||||
];
|
||||
const result = await runSetup(
|
||||
{
|
||||
...options(true),
|
||||
allDetectedClients: true,
|
||||
...(installSkill === undefined ? {} : { installSkill }),
|
||||
},
|
||||
host,
|
||||
);
|
||||
expect(result.status).toBe("ready");
|
||||
expect(result.doctor.scope.clients).toEqual([]);
|
||||
expect(result.clientStates).toMatchObject([
|
||||
{ client: { name: "grok_bot" }, detected: true },
|
||||
]);
|
||||
expect(result.clients).toEqual({});
|
||||
expect(host.configurations).toBe(0);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([true, false])(
|
||||
"handles repeated clients once in first-selected order (dry run: %s)",
|
||||
async (dryRun) => {
|
||||
|
||||
@@ -139,7 +139,9 @@ export const discoverSetupState = async (input: {
|
||||
installHopper,
|
||||
hopperInstall,
|
||||
clients,
|
||||
detectedClientIds: [...detectedIds],
|
||||
detectedClientIds: detectedClients
|
||||
.filter(({ format }) => format !== "unsupported")
|
||||
.map(({ name }) => name),
|
||||
defaultClientIds,
|
||||
clientStates,
|
||||
...(linuxDistribution?.packageFamily === undefined
|
||||
|
||||
@@ -125,12 +125,21 @@ export const isSourceEvidenceRetainable = (
|
||||
descriptor.effects.mayWriteFilesystem === false) &&
|
||||
descriptor.effects.changesPermissions === false;
|
||||
|
||||
/** Explicit independent decompiles must run even if a snapshot has a matching entry. */
|
||||
export const requestsFreshIndependentDecompile = (
|
||||
operation: string,
|
||||
parameters: Readonly<Record<string, JsonValue>>,
|
||||
): boolean =>
|
||||
operation === "procedure_pseudo_code" &&
|
||||
parameters.analysis === "independent_fresh";
|
||||
|
||||
/** Whether an observation can additionally be replayed as a snapshot. */
|
||||
export const isSnapshotCacheable = (
|
||||
operation: AnalysisOperation,
|
||||
descriptor: CapabilityDescriptor | undefined,
|
||||
parameters: Readonly<Record<string, JsonValue>>,
|
||||
): descriptor is CapabilityDescriptor =>
|
||||
!requestsFreshIndependentDecompile(operation, parameters) &&
|
||||
isSourceEvidenceRetainable(operation, descriptor, parameters) &&
|
||||
replayDocumentSelected(operation, parameters) &&
|
||||
descriptor.cachePolicy !== "live";
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
} from "../AnalysisProvider.js";
|
||||
import {
|
||||
isSnapshotCacheable,
|
||||
requestsFreshIndependentDecompile,
|
||||
requiresNativeSourceAdmissionBeforeReplay,
|
||||
} from "./AnalysisSnapshotCache.js";
|
||||
import type { SessionProviderRoute } from "./SessionProviderRouter.js";
|
||||
@@ -79,6 +80,17 @@ export const prepareSessionExecution = (
|
||||
),
|
||||
);
|
||||
}
|
||||
if (
|
||||
requestsFreshIndependentDecompile(operation, parameters) &&
|
||||
capability.independentDecompile !== true
|
||||
)
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
capability.provider.id,
|
||||
operation,
|
||||
"This provider does not declare fresh independent decompilation.",
|
||||
),
|
||||
);
|
||||
const profile =
|
||||
active.profile !== null &&
|
||||
capability.provider.id === active.profile.provider.id
|
||||
|
||||
@@ -414,6 +414,7 @@ export const providerCapabilitySets = (
|
||||
const key = JSON.stringify([
|
||||
effects,
|
||||
limitations,
|
||||
descriptor.independentDecompile ?? null,
|
||||
status.available,
|
||||
status.reason,
|
||||
status.availability_code,
|
||||
@@ -425,6 +426,9 @@ export const providerCapabilitySets = (
|
||||
operations: [descriptor.operation],
|
||||
effects,
|
||||
limitations,
|
||||
...(descriptor.independentDecompile === undefined
|
||||
? {}
|
||||
: { independent_decompile: descriptor.independentDecompile }),
|
||||
...status,
|
||||
});
|
||||
else group.operations.push(descriptor.operation);
|
||||
|
||||
@@ -58,6 +58,7 @@ it("records the decoded layout once in its Evidence", async () => {
|
||||
|
||||
it.each([
|
||||
[{ machine: "EM_X86_64", bits: 64, byte_order: "little" }, "x86_64"],
|
||||
[{ machine: "EM_AARCH64", bits: 64, byte_order: "little" }, "arm64"],
|
||||
[{ machine: "EM_386", bits: 32, byte_order: "little" }, "x86"],
|
||||
[{ machine: "EM_ARM", bits: 32, byte_order: "little" }, "arm"],
|
||||
[{ machine: "EM_MIPS", bits: 32, byte_order: "big" }, "mips"],
|
||||
|
||||
@@ -14,6 +14,7 @@ import { parseAnalysisInput } from "../AnalysisInput.js";
|
||||
const OPERATION = "inspect_binary_layout";
|
||||
const EVIDENCE_ARCHITECTURES = {
|
||||
EM_X86_64: "x86_64",
|
||||
EM_AARCH64: "arm64",
|
||||
EM_386: "x86",
|
||||
EM_ARM: "arm",
|
||||
EM_MIPS: "mips",
|
||||
|
||||
@@ -535,6 +535,12 @@ const publishOne = async (input: {
|
||||
const caveats = [
|
||||
...new Set([...infoCaveats, ...decompiled.value.limitations]),
|
||||
];
|
||||
const observationSource: NonNullable<
|
||||
DecompileDirectoryFunctionRecord["provider_source"]
|
||||
> =
|
||||
decompiled.value.sourceDescriptor === undefined
|
||||
? { status: "unknown", reason: "source_not_available" }
|
||||
: { status: "reported", source: decompiled.value.sourceDescriptor };
|
||||
const pseudocode = decompiled.value.result;
|
||||
if (typeof pseudocode !== "string" || pseudocode.length === 0) {
|
||||
const reason = pseudocodeFailure(pseudocode);
|
||||
@@ -543,6 +549,7 @@ const publishOne = async (input: {
|
||||
result: await input.session.append(
|
||||
failureRecord(input.procedure, body.fact, reason, {
|
||||
limitations: caveats,
|
||||
providerSource: observationSource,
|
||||
}),
|
||||
),
|
||||
bodySupport: body.support,
|
||||
@@ -580,6 +587,7 @@ const publishOne = async (input: {
|
||||
failureRecord(input.procedure, body.fact, reason, {
|
||||
relativePath,
|
||||
limitations: caveats,
|
||||
providerSource: observationSource,
|
||||
}),
|
||||
),
|
||||
bodySupport: body.support,
|
||||
@@ -591,6 +599,11 @@ const publishOne = async (input: {
|
||||
status: "written",
|
||||
relative_path: relativePath,
|
||||
sha256: committed.sha256,
|
||||
// Matching existing bytes prove equality, not who originally wrote them.
|
||||
provider_source:
|
||||
committed.status === "matched"
|
||||
? { status: "unknown", reason: "existing_file_origin_unknown" }
|
||||
: observationSource,
|
||||
body: bodyRecord(body.fact),
|
||||
// The header may truncate; the journal keeps the complete list.
|
||||
...(caveats.length === 0 ? {} : { limitations: caveats }),
|
||||
@@ -889,6 +902,7 @@ const failureRecord = (
|
||||
readonly relativePath?: string;
|
||||
/** Caveats of the observations this attempt used. */
|
||||
readonly limitations?: readonly string[];
|
||||
readonly providerSource?: DecompileDirectoryFunctionRecord["provider_source"];
|
||||
} = {},
|
||||
): DecompileDirectoryFunctionRecord => ({
|
||||
address: procedure.address,
|
||||
@@ -896,6 +910,9 @@ const failureRecord = (
|
||||
status: "failed",
|
||||
body: bodyRecord(body),
|
||||
failure: reason,
|
||||
...(options.providerSource === undefined
|
||||
? {}
|
||||
: { provider_source: options.providerSource }),
|
||||
...(options.relativePath === undefined
|
||||
? {}
|
||||
: { relative_path: options.relativePath }),
|
||||
|
||||
@@ -64,6 +64,56 @@ const compare = (...probes: (readonly TimedCaptureRow[])[]) => {
|
||||
return report.value;
|
||||
};
|
||||
|
||||
describe("step difference coverage", () => {
|
||||
it("keeps categories from differences beyond the returned prefix", () => {
|
||||
const fields = Object.fromEntries(
|
||||
Array.from({ length: 20 }, (_, index) => [`field${String(index)}`, 0]),
|
||||
);
|
||||
const capture = timedCaptureEvidence([["GET", "/api/config", fields]]);
|
||||
const served = environmentStateServeFixture(capture);
|
||||
const report = compareEnvironment({
|
||||
source: environmentServeSource(served),
|
||||
captureValue: capture,
|
||||
probes: [
|
||||
{
|
||||
label: "changed",
|
||||
value: timedCaptureEvidence([
|
||||
[
|
||||
"GET",
|
||||
"/api/config",
|
||||
{
|
||||
...Object.fromEntries(
|
||||
Object.keys(fields).map((key) => [key, 1]),
|
||||
),
|
||||
z_missing: true,
|
||||
},
|
||||
],
|
||||
]),
|
||||
},
|
||||
],
|
||||
operation: "environment-compare",
|
||||
});
|
||||
if (!report.ok) throw report.error;
|
||||
const step = report.value.probes[0]!.results[0]!;
|
||||
expect(step).toMatchObject({
|
||||
equal: false,
|
||||
equal_ignoring_order: false,
|
||||
categories: ["value", "missing-field"],
|
||||
differences_total: 21,
|
||||
differences_omitted: 1,
|
||||
differences_truncated: true,
|
||||
});
|
||||
expect(step.differences).toHaveLength(20);
|
||||
expect(step.differences.every(({ category }) => category === "value")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(report.value.summary.categories).toMatchObject({
|
||||
value: 1,
|
||||
"missing-field": 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("matchProbeRequest", () => {
|
||||
it("compares a repeated-query probe against the matching recorded response", () => {
|
||||
const recorded = timedCaptureEvidence([
|
||||
|
||||
@@ -81,6 +81,10 @@ export interface CompareStep {
|
||||
readonly equal_ignoring_order: boolean;
|
||||
readonly categories: DifferenceCategory[];
|
||||
readonly differences: Difference[];
|
||||
/** All differences computed for this step, before the returned list is capped. */
|
||||
readonly differences_total: number;
|
||||
readonly differences_omitted: number;
|
||||
readonly differences_truncated: boolean;
|
||||
readonly ignored_query: string[];
|
||||
}
|
||||
|
||||
@@ -287,6 +291,8 @@ const stepOf = (
|
||||
outcome: { env_status: number | null; differences: Difference[] },
|
||||
): CompareStep => {
|
||||
const categories = [...new Set(outcome.differences.map((d) => d.category))];
|
||||
const differences = outcome.differences.slice(0, STEP_DIFFERENCE_LIMIT);
|
||||
const omitted = outcome.differences.length - differences.length;
|
||||
return {
|
||||
...base,
|
||||
tool: call?.tool.name ?? null,
|
||||
@@ -296,7 +302,10 @@ const stepOf = (
|
||||
(difference) => difference.category === "order",
|
||||
),
|
||||
categories,
|
||||
differences: outcome.differences.slice(0, STEP_DIFFERENCE_LIMIT),
|
||||
differences,
|
||||
differences_total: outcome.differences.length,
|
||||
differences_omitted: omitted,
|
||||
differences_truncated: omitted > 0,
|
||||
ignored_query: [...(call?.ignoredQuery ?? [])],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,6 +14,10 @@ import {
|
||||
resolveOccurrenceParents,
|
||||
} from "./ArtifactGraphConstruction.js";
|
||||
import { createArtifactClassificationProbe } from "../ArtifactClassificationProbe.js";
|
||||
import { createAsarHeaderSignatureProbe } from "../AsarHeader.js";
|
||||
import { ArtifactReaderFailure } from "../ArtifactReader.js";
|
||||
import { translateArtifactFailure } from "../ArtifactOperationFailure.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import { artifactIdForContent } from "../../domain/artifactIdentity.js";
|
||||
import { pe } from "../../domain/binaryTarget.fixture.js";
|
||||
import type { ArtifactEntry } from "../ArtifactReader.js";
|
||||
@@ -26,6 +30,7 @@ import {
|
||||
} from "../../domain/firmware/uf2.fixture.js";
|
||||
|
||||
import { classifyArtifactBytesAtPath } from "../../../tests/fixtures/artifactClassification.js";
|
||||
import { asarHeaderBytes } from "../../../tests/fixtures/asarHeader.js";
|
||||
import {
|
||||
ESP32C3_APP_SEGMENTS,
|
||||
esp8266Image,
|
||||
@@ -73,6 +78,42 @@ it("classifies original DOS header padding as an executable", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("projects the parsed ASAR header size when admission rejects an archive", () => {
|
||||
const bytes = asarHeaderBytes("{}", { padding: Buffer.alloc(8, 0x20) });
|
||||
const headerBytes = bytes.readUInt32LE(4);
|
||||
const probe = createAsarHeaderSignatureProbe({
|
||||
maximumBytes: headerBytes - 1,
|
||||
source: "setting",
|
||||
});
|
||||
probe.write(bytes);
|
||||
const asarHeader = probe.finish(bytes.length);
|
||||
expect(asarHeader).toMatchObject({
|
||||
state: "limit",
|
||||
extents: { headerBytes },
|
||||
});
|
||||
|
||||
let failure: unknown;
|
||||
try {
|
||||
classifyArtifactContent(
|
||||
"app.asar",
|
||||
{ prefix: bytes, asarHeader },
|
||||
bytes.length,
|
||||
);
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
}
|
||||
expect(failure).toBeInstanceOf(ArtifactReaderFailure);
|
||||
if (!(failure instanceof ArtifactReaderFailure))
|
||||
throw new Error("ASAR admission failure was not classified");
|
||||
|
||||
const projected = projectAnalysisError(
|
||||
translateArtifactFailure("inspect_artifact", failure),
|
||||
);
|
||||
expect(projected.details?.limit).toMatchObject({
|
||||
required_value: headerBytes,
|
||||
});
|
||||
});
|
||||
|
||||
const entry = (path: string, kind: ArtifactEntry["kind"]): ArtifactEntry => ({
|
||||
path,
|
||||
kind,
|
||||
|
||||
@@ -557,7 +557,10 @@ export const classifyArtifactContent = (
|
||||
asarHeader.state !== "rejected"
|
||||
) {
|
||||
if (asarHeader.state === "limit")
|
||||
throw asarHeaderAdmissionFailure(asarHeader.admission);
|
||||
throw asarHeaderAdmissionFailure(
|
||||
asarHeader.admission,
|
||||
asarHeader.extents?.headerBytes,
|
||||
);
|
||||
if (asarHeader.state === "matched")
|
||||
role = { kind: "container", format: "asar" };
|
||||
else role = artifactRoleForFormat(path, contentFormat);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { mediaTypeEssence } from "../domain/analysisView/recordedMediaType.js";
|
||||
import { cdpResourceColumnSchema } from "../domain/cdpObservationSchemas.js";
|
||||
import {
|
||||
admitRecords,
|
||||
isHttpUrl,
|
||||
@@ -57,18 +58,22 @@ export const exclusionReasonForUrl = (
|
||||
? "disallowed_origin"
|
||||
: "unsupported_url";
|
||||
|
||||
/** Preserve omitted and invalid protocol offsets as unknown, never invented coordinates. */
|
||||
/**
|
||||
* Preserve omitted and invalid protocol offsets as unknown, never invented
|
||||
* coordinates. Columns follow the shared CDP resource-column policy, which
|
||||
* keeps a negative column Chrome reports.
|
||||
*/
|
||||
export const cdpSourceCoordinates = (frame: UnknownRecord | undefined) => {
|
||||
const valid = (value: unknown): value is number =>
|
||||
const validLine = (value: unknown): value is number =>
|
||||
typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
||||
const line = frame?.lineNumber;
|
||||
const column = frame?.columnNumber;
|
||||
const column = cdpResourceColumnSchema.safeParse(frame?.columnNumber);
|
||||
return {
|
||||
line: valid(line) ? line : null,
|
||||
column: valid(column) ? column : null,
|
||||
line: validLine(line) ? line : null,
|
||||
column: column.success ? column.data : null,
|
||||
invalid:
|
||||
(line !== undefined && !valid(line)) ||
|
||||
(column !== undefined && !valid(column)),
|
||||
(line !== undefined && !validLine(line)) ||
|
||||
(frame?.columnNumber !== undefined && !column.success),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
recordValue,
|
||||
type UnknownRecord,
|
||||
} from "./CdpCaptureValues.js";
|
||||
import { cdpResourceColumnSchema } from "../domain/cdpObservationSchemas.js";
|
||||
|
||||
const integer = z.int().min(0);
|
||||
/** Active runtime admission remains stricter than passive request observations. */
|
||||
@@ -25,7 +26,7 @@ export const runtimeRequestSchema = z.object({
|
||||
stack: z.unknown().optional(),
|
||||
url: z.string().optional(),
|
||||
lineNumber: integer.optional(),
|
||||
columnNumber: integer.optional(),
|
||||
columnNumber: cdpResourceColumnSchema.optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
numberValue,
|
||||
recordValue,
|
||||
} from "./CdpCaptureValues.js";
|
||||
import { cdpResourceColumnSchema } from "../domain/cdpObservationSchemas.js";
|
||||
|
||||
const integer = z.int().min(0);
|
||||
const contextAuxDataSchema = z
|
||||
@@ -31,9 +32,9 @@ export const runtimeScriptParsedSchema = scriptDeclaration.extend({
|
||||
scriptId: z.string().min(1),
|
||||
executionContextId: integer,
|
||||
startLine: integer,
|
||||
startColumn: integer,
|
||||
startColumn: cdpResourceColumnSchema,
|
||||
endLine: integer,
|
||||
endColumn: integer,
|
||||
endColumn: cdpResourceColumnSchema,
|
||||
hash: z.string().optional(),
|
||||
sourceMapURL: z.string().optional(),
|
||||
hasSourceURL: z.boolean().optional(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { cdpResourceColumnSchema } from "../../domain/cdpObservationSchemas.js";
|
||||
|
||||
const integer = z.int().min(0);
|
||||
const range = z.object({
|
||||
@@ -30,7 +31,7 @@ export const runtimeStackSchema = z.object({
|
||||
scriptId: z.string(),
|
||||
url: z.string(),
|
||||
lineNumber: integer,
|
||||
columnNumber: integer,
|
||||
columnNumber: cdpResourceColumnSchema,
|
||||
functionName: z.string(),
|
||||
}),
|
||||
),
|
||||
@@ -49,7 +50,7 @@ export const runtimeListenersSchema = z.object({
|
||||
once: z.boolean(),
|
||||
scriptId: z.string(),
|
||||
lineNumber: integer,
|
||||
columnNumber: integer,
|
||||
columnNumber: cdpResourceColumnSchema,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -805,12 +805,46 @@ it.each(["[2001:db8::1]", "[::1]", "[::ffff:192.0.2.1]"])(
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["[2620:0:860:ed1a::1]", "[::1]"])(
|
||||
"retains Chromium DevTools bracketed IPv6 spelling in reported Evidence: %s",
|
||||
(address) => {
|
||||
const fixture = historicalHar();
|
||||
const first = fixture.log.entries[0];
|
||||
if (first === undefined) throw new Error("fixture missing");
|
||||
const input = {
|
||||
log: {
|
||||
...fixture.log,
|
||||
creator: { name: "WebInspector", version: "537.36" },
|
||||
entries: [
|
||||
{ ...first, serverIPAddress: address },
|
||||
structuredClone(first),
|
||||
],
|
||||
},
|
||||
};
|
||||
const decoded = decodeHarCapture(JSON.stringify(input), []);
|
||||
expect(decoded.records[0]).toMatchObject({
|
||||
reported: { serverIPAddress: address },
|
||||
});
|
||||
expect(decoded.records[0]?.limitations).toContain(
|
||||
"This capture declares Chromium DevTools (WebInspector 537.36) and reports bracketed IPv6 serverIPAddress. Original spellings are retained; brackets were removed only from valid IPv6 values in the validation copy. Producer declarations are not authenticated.",
|
||||
);
|
||||
expect(
|
||||
decoded.records[1]?.limitations.some((limitation) =>
|
||||
limitation.includes("bracketed IPv6 serverIPAddress"),
|
||||
),
|
||||
).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ name: "other", version: "1.58.2", address: "[2001:db8::1]" },
|
||||
{ name: "Playwright", version: "1.58.1", address: "[2001:db8::1]" },
|
||||
{ name: "Playwright", version: "1.58.2", address: "[192.0.2.1]" },
|
||||
{ name: "Playwright", version: "1.58.2", address: "[2001:db8::1]:443" },
|
||||
{ name: "Playwright", version: "1.58.2", address: "[fe80::1%eth0]" },
|
||||
{ name: "WebInspector", version: "605.1.15", address: "[2001:db8::1]" },
|
||||
{ name: "WebInspector", version: "537.36", address: "[192.0.2.1]" },
|
||||
{ name: "WebInspector", version: "537.36", address: "[2001:db8::1]:443" },
|
||||
])(
|
||||
"rejects unrecognized bracketed HAR address input: $name $version $address",
|
||||
({ name, version, address }) => {
|
||||
|
||||
@@ -33,9 +33,14 @@ export const decodeHarCapture = (
|
||||
): DecodedCapture => {
|
||||
const raw = parseCaptureJson(text, "HAR");
|
||||
const validation = projectHar(raw, [], "validation");
|
||||
const adaptedPlaywrightEntries = new Set(
|
||||
createHarValidator()(validation.reported),
|
||||
);
|
||||
const ipv6Adaptation = createHarValidator()(validation.reported);
|
||||
const adaptedIpv6Entries = new Set(ipv6Adaptation?.entries);
|
||||
const ipv6Limitations =
|
||||
ipv6Adaptation === null
|
||||
? []
|
||||
: [
|
||||
`This capture declares ${ipv6Adaptation.producer} and reports bracketed IPv6 serverIPAddress. Original spellings are retained; brackets were removed only from valid IPv6 values in the validation copy. Producer declarations are not authenticated.`,
|
||||
];
|
||||
if (
|
||||
!captureObject(raw) ||
|
||||
!captureObject(raw.log) ||
|
||||
@@ -76,11 +81,7 @@ export const decodeHarCapture = (
|
||||
...projection,
|
||||
limitations: [
|
||||
"HAR text without an encoding declaration is producer-decoded Unicode. Unsupported declared body encodings remain opaque; original body bytes remain unknown unless content explicitly uses valid base64. Declared sizes are preserved independently of retained bytes. mitmproxy 12.2.3 SaveHar may report null optional postData.text; that null is retained and omitted only from the upstream validation copy.",
|
||||
...(adaptedPlaywrightEntries.has(ordinal)
|
||||
? [
|
||||
"This capture declares Playwright 1.58.2 and reports bracketed IPv6 serverIPAddress. Original spellings are retained; brackets were removed only from valid IPv6 values in the validation copy. Producer declarations are not authenticated.",
|
||||
]
|
||||
: []),
|
||||
...(adaptedIpv6Entries.has(ordinal) ? ipv6Limitations : []),
|
||||
...(recordsExcluded
|
||||
? [
|
||||
"This record's payload and sidecars are excluded because its structural ancestor property was explicitly marked sensitive.",
|
||||
|
||||
@@ -30,10 +30,28 @@ const timingsSchema = {
|
||||
},
|
||||
};
|
||||
|
||||
// Observed producers that copy Chromium's bracketed IPv6 remote address into
|
||||
// serverIPAddress. Chromium DevTools declares WebInspector with the frozen
|
||||
// AppleWebKit version from its user agent.
|
||||
const BRACKETED_IPV6_PRODUCERS = [
|
||||
{ name: "Playwright", version: "1.58.2", label: "Playwright 1.58.2" },
|
||||
{
|
||||
name: "WebInspector",
|
||||
version: "537.36",
|
||||
label: "Chromium DevTools (WebInspector 537.36)",
|
||||
},
|
||||
] as const;
|
||||
|
||||
/** Entries whose bracketed IPv6 address was adapted, and the declared producer. */
|
||||
interface BracketedIpv6Adaptation {
|
||||
readonly producer: string;
|
||||
readonly entries: readonly number[];
|
||||
}
|
||||
|
||||
/** Validate with pinned HAR 1.2 draft-06 schemas and the owned schema corrections. */
|
||||
export const createHarValidator = (): ((
|
||||
value: unknown,
|
||||
) => readonly number[]) => {
|
||||
) => BracketedIpv6Adaptation | null) => {
|
||||
const require = createRequire(import.meta.url);
|
||||
const schemas = z
|
||||
.record(z.string(), z.record(z.string(), z.unknown()))
|
||||
@@ -78,8 +96,7 @@ export const createHarValidator = (): ((
|
||||
if (validate === undefined)
|
||||
throw new Error("Pinned HAR root schema is unavailable");
|
||||
return (value) => {
|
||||
const adaptedPlaywrightEntries =
|
||||
allowObservedProducerRepresentations(value);
|
||||
const adaptations = allowObservedProducerRepresentations(value);
|
||||
if (!validate(value)) {
|
||||
const error = validate.errors?.[0];
|
||||
// AJV messages name the violated constraint, such as the missing
|
||||
@@ -92,36 +109,43 @@ export const createHarValidator = (): ((
|
||||
error?.instancePath ?? "",
|
||||
);
|
||||
}
|
||||
return adaptedPlaywrightEntries;
|
||||
return adaptations;
|
||||
};
|
||||
};
|
||||
|
||||
/** Name the declared producer when it is observed to bracket IPv6 addresses. */
|
||||
const bracketedIpv6ProducerOf = (
|
||||
creator: Record<string, unknown>,
|
||||
): string | null =>
|
||||
BRACKETED_IPV6_PRODUCERS.find(
|
||||
({ name, version }) => creator.name === name && creator.version === version,
|
||||
)?.label ?? null;
|
||||
|
||||
/** Adapt only observed producer representations in the disposable validation copy. */
|
||||
const allowObservedProducerRepresentations = (
|
||||
value: unknown,
|
||||
): readonly number[] => {
|
||||
if (!captureObject(value) || !captureObject(value.log)) return [];
|
||||
): BracketedIpv6Adaptation | null => {
|
||||
if (!captureObject(value) || !captureObject(value.log)) return null;
|
||||
const log = value.log;
|
||||
if (!captureObject(log.creator) || !Array.isArray(log.entries)) return [];
|
||||
if (!captureObject(log.creator) || !Array.isArray(log.entries)) return null;
|
||||
const mitmproxy =
|
||||
log.creator.name === "mitmproxy" && log.creator.version === "12.2.3";
|
||||
const playwright =
|
||||
log.creator.name === "Playwright" && log.creator.version === "1.58.2";
|
||||
if (!mitmproxy && !playwright) return [];
|
||||
const adaptedPlaywrightEntries: number[] = [];
|
||||
const bracketedIpv6Producer = bracketedIpv6ProducerOf(log.creator);
|
||||
if (!mitmproxy && bracketedIpv6Producer === null) return null;
|
||||
const adaptedEntries: number[] = [];
|
||||
for (const [ordinal, entry] of log.entries.entries()) {
|
||||
const candidate: unknown = entry;
|
||||
if (!captureObject(candidate)) continue;
|
||||
const address = candidate.serverIPAddress;
|
||||
if (
|
||||
playwright &&
|
||||
bracketedIpv6Producer !== null &&
|
||||
typeof address === "string" &&
|
||||
address.startsWith("[") &&
|
||||
address.endsWith("]") &&
|
||||
isIP(address.slice(1, -1)) === 6
|
||||
) {
|
||||
candidate.serverIPAddress = address.slice(1, -1);
|
||||
adaptedPlaywrightEntries.push(ordinal);
|
||||
adaptedEntries.push(ordinal);
|
||||
}
|
||||
if (!mitmproxy) continue;
|
||||
const request = candidate.request;
|
||||
@@ -129,7 +153,9 @@ const allowObservedProducerRepresentations = (
|
||||
const body = request.postData;
|
||||
if (captureObject(body) && body.text === null) delete body.text;
|
||||
}
|
||||
return adaptedPlaywrightEntries;
|
||||
return bracketedIpv6Producer === null
|
||||
? null
|
||||
: { producer: bracketedIpv6Producer, entries: adaptedEntries };
|
||||
};
|
||||
|
||||
const HAR_REQUEST_URL_FORMAT = "absolute-url";
|
||||
|
||||
+38
-5
@@ -252,7 +252,7 @@ export const createCli = (
|
||||
);
|
||||
cli.serve = (arguments_ = process.argv.slice(2), options) => {
|
||||
if (complete(arguments_, options ?? {})) return Promise.resolve();
|
||||
const normalized = normalizeCliArguments(arguments_);
|
||||
let normalized = normalizeCliArguments(arguments_);
|
||||
const { command, flags } = splitIncurBuiltinArguments(normalized);
|
||||
const builtin = command[0] === PRODUCT_IDENTITY.cliBinary ? 1 : 0;
|
||||
if (
|
||||
@@ -320,10 +320,34 @@ export const createCli = (
|
||||
// prevents a save. Guard that path: reject a settings file REA cannot
|
||||
// update, back up the file, preserve the caller's environment, and verify
|
||||
// the registration landed.
|
||||
// SyncMcp.register also writes Amp when no agents are selected. Mode
|
||||
// switches return before registration and must not touch its settings.
|
||||
// Use the same normalized arguments for selection and dispatch, including
|
||||
// non-Amp equals forms. Incur consumes command values before agent flags.
|
||||
normalized = serveArguments;
|
||||
const registration = splitIncurBuiltinArguments(serveArguments);
|
||||
const rest = registration.command.slice(builtin + 2);
|
||||
const agents: string[] = [];
|
||||
for (let index = 0; index < rest.length; index += 1) {
|
||||
if (
|
||||
(rest[index] === "--command" || rest[index] === "-c") &&
|
||||
rest[index + 1]
|
||||
)
|
||||
index += 1;
|
||||
else if (rest[index] === "--agent" && rest[index + 1])
|
||||
agents.push(rest[++index]!);
|
||||
}
|
||||
const ampRequested =
|
||||
command.some(
|
||||
(token, index) => token === "--agent" && command[index + 1] === "amp",
|
||||
) || command.includes("--agent=amp");
|
||||
![
|
||||
"--help",
|
||||
"-h",
|
||||
"--llms",
|
||||
"--llms-full",
|
||||
"--mcp",
|
||||
"--update",
|
||||
"--incur-update-check",
|
||||
].some((flag) => registration.flags.has(flag)) &&
|
||||
(agents.length === 0 || agents.includes("amp"));
|
||||
if (ampRequested) {
|
||||
const settingsPath = join(homedir(), ".config", "amp", "settings.json");
|
||||
let priorEnv: unknown;
|
||||
@@ -409,9 +433,17 @@ export const createCli = (
|
||||
}
|
||||
|
||||
const runRegistration = (backupPath?: string): Promise<void> => {
|
||||
let failed = false;
|
||||
const result = serveStrict(
|
||||
serveArguments,
|
||||
options ?? {},
|
||||
{
|
||||
...options,
|
||||
exit: (code) => {
|
||||
failed ||= code !== 0;
|
||||
if (options?.exit !== undefined) options.exit(code);
|
||||
else process.exitCode = code;
|
||||
},
|
||||
},
|
||||
(tokens, selected) =>
|
||||
serveDeclared(tokens, selected, (declared, declaredOptions) =>
|
||||
serve(
|
||||
@@ -421,6 +453,7 @@ export const createCli = (
|
||||
),
|
||||
);
|
||||
return result.then(() => {
|
||||
if (failed) return;
|
||||
let failure: string | undefined;
|
||||
try {
|
||||
if (!existsSync(settingsPath))
|
||||
|
||||
@@ -36,6 +36,7 @@ import { localPathStringSchema } from "../domain/localPath.js";
|
||||
import type { ExecutableFormatHint } from "../domain/dosCom.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { registerDecompileDirectoryCommand } from "./decompileDirectoryCommand.js";
|
||||
import { decompileAnalysisSelection } from "../contracts/officialToolContracts.js";
|
||||
|
||||
const decompileSelectorGuidance = `${addressOrProcedureGuidance} Alternatively, use --file-offset alone.`;
|
||||
const namedAddressGuidance = requiredSelectorGuidance(
|
||||
@@ -221,6 +222,7 @@ const registerDecompileCommand = (
|
||||
.describe(
|
||||
`Ghidra source-file byte offset; mapped internally to an analysis address. ${decompileSelectorGuidance}`,
|
||||
),
|
||||
analysis: decompileAnalysisSelection,
|
||||
snapshot: snapshotSelectionOption,
|
||||
...nativeOpenOptionFields,
|
||||
}),
|
||||
@@ -245,6 +247,9 @@ const registerDecompileCommand = (
|
||||
: "file-offset",
|
||||
"procedure_pseudo_code",
|
||||
),
|
||||
...(options.analysis === undefined
|
||||
? {}
|
||||
: { analysis: options.analysis }),
|
||||
},
|
||||
directAnalysisOptionsFromCli(logger, options),
|
||||
).then(projectCliAnalysisResult),
|
||||
|
||||
@@ -46,8 +46,8 @@ export interface IncurBuiltinArgumentSplit {
|
||||
|
||||
/**
|
||||
* Mirror Incur's builtin pass over normalized arguments without stopping at a
|
||||
* rejected value, so callers can still see every later token. REA's boolean
|
||||
* timing global follows the same valueless pass; no config flag is registered.
|
||||
* rejected value, so callers can still see every later token. Then mirror the
|
||||
* boolean timing global pass over the remaining tokens; no config is registered.
|
||||
*/
|
||||
export const splitIncurBuiltinArguments = (
|
||||
arguments_: readonly string[],
|
||||
@@ -59,7 +59,7 @@ export const splitIncurBuiltinArguments = (
|
||||
const argument = arguments_[index]!;
|
||||
const next = arguments_[index + 1];
|
||||
if (
|
||||
INCUR_BUILTIN_FLAGS.has(argument) ||
|
||||
(INCUR_BUILTIN_FLAGS.has(argument) && argument !== "--timings") ||
|
||||
// A following value belongs to a command-local `--version` option.
|
||||
(argument === "--version" && (next === undefined || next.startsWith("-")))
|
||||
) {
|
||||
@@ -73,7 +73,24 @@ export const splitIncurBuiltinArguments = (
|
||||
}
|
||||
command.push(argument);
|
||||
}
|
||||
return { command, flags, outputValues };
|
||||
// Incur extracts builtin output values first, then parses globals until `--`.
|
||||
// A timing-shaped output value is data; a command option's value is still
|
||||
// subject to this global pass before the registration builtin consumes it.
|
||||
let globalsEnded = false;
|
||||
const remaining = command.filter((token) => {
|
||||
if (token === "--") globalsEnded = true;
|
||||
if (
|
||||
!globalsEnded &&
|
||||
(token === "--timings" ||
|
||||
token === "--no-timings" ||
|
||||
token.startsWith("--timings="))
|
||||
) {
|
||||
flags.add(token);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return { command: remaining, flags, outputValues };
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { digestSchema } from "../domain/digests.js";
|
||||
import { absoluteLocalPathStringSchema } from "../domain/localPath.js";
|
||||
import { analysisSourceDescriptorSchema } from "../domain/analysisSource.js";
|
||||
|
||||
/** On-disk journal. Append-only so a crash keeps committed function files. */
|
||||
export const DECOMPILE_DIRECTORY_JOURNAL_NAME = "decompile-journal.jsonl";
|
||||
@@ -245,6 +246,22 @@ export const decompileDirectoryFunctionSchema = z
|
||||
)
|
||||
.optional(),
|
||||
sha256: digestSchema.optional(),
|
||||
/** Source of this attempt's pseudocode observation; a matched file's original producer remains unknown. */
|
||||
provider_source: z
|
||||
.discriminatedUnion("status", [
|
||||
z.strictObject({
|
||||
status: z.literal("reported"),
|
||||
source: analysisSourceDescriptorSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
status: z.literal("unknown"),
|
||||
reason: z.enum([
|
||||
"source_not_available",
|
||||
"existing_file_origin_unknown",
|
||||
]),
|
||||
}),
|
||||
])
|
||||
.optional(),
|
||||
body: bodySchema,
|
||||
failure: z
|
||||
.strictObject({
|
||||
|
||||
@@ -245,7 +245,7 @@ export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [
|
||||
...toolContractMetadata("inspect_binary_layout"),
|
||||
kind: "native-provider",
|
||||
description:
|
||||
"Inspect an explicit local binary without launching it as a host process. Returns file-backed section/segment ranges, linked addresses, original symbol/REL/RELA table identities, complete encoded RELR tables and upstream-derived offsets with unknown per-word locations/addends, raw name bytes, dependency/interpreter names and static mitigation inferences inline with artifact SHA-256 Evidence. Profile: EXEC/DYN/REL ELF64 x86-64, ELF32 i386, ELF32 little-endian ARM and ELF32 little- or big-endian MIPS on Linux x64 via caller-supplied Python with unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2. Runtime addresses/library paths, overall relocation inventory completeness and complete derived GOT/PLT coverage remain unknown. Zero executable entry values mean absence; relocatable entries are not applicable. Upstream can emulate PLT instructions with Unicorn for derived static maps. Uses bounded complete output and owned temporary files/processes; malformed, unsupported or oversized objects return no partial success. Select detail summary to retain the complete layout Evidence in this session and receive only its summary view, then inspect sections, symbols, mitigations or linkage with inspect_analysis_view. pwntools_timeout_ms is each owned pwntools command's deadline (omitted means 30000 ms, range 1..2147483647, next call). cpuSeconds 30 and the output, diagnostic, and address-space byte allowances have no runtime override.",
|
||||
"Inspect an explicit local binary without launching it as a host process. Returns file-backed section/segment ranges, linked addresses, original symbol/REL/RELA table identities, complete encoded RELR tables and upstream-derived offsets with unknown per-word locations/addends, raw name bytes, dependency/interpreter names and static mitigation inferences inline with artifact SHA-256 Evidence. Profile: EXEC/DYN/REL ELF64 x86-64, ELF64 little-endian AArch64, ELF32 i386, ELF32 little-endian ARM and ELF32 little- or big-endian MIPS on Linux x64 via caller-supplied Python with unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2. Runtime addresses/library paths, overall relocation inventory completeness and complete derived GOT/PLT coverage remain unknown. Zero executable entry values mean absence; relocatable entries are not applicable. Upstream can emulate PLT instructions with Unicorn for derived static maps. Uses bounded complete output and owned temporary files/processes; malformed, unsupported or oversized objects return no partial success. Select detail summary to retain the complete layout Evidence in this session and receive only its summary view, then inspect sections, symbols, mitigations or linkage with inspect_analysis_view. pwntools_timeout_ms is each owned pwntools command's deadline (omitted means 30000 ms, range 1..2147483647, next call). cpuSeconds 30 and the output, diagnostic, and address-space byte allowances have no runtime override.",
|
||||
inputSchema: inspectBinaryLayoutRequestSchema,
|
||||
outputSchema: analysisDetailOutputSchemaOf(
|
||||
binaryLayoutSchema,
|
||||
|
||||
@@ -23,6 +23,13 @@ import { nativeSearchInput } from "../domain/native/nativeSearch.js";
|
||||
const hopperRegexDeadline =
|
||||
"Hopper regex mode uses ECMAScript Unicode syntax in a cancellable worker. The matching deadline is REA_REGEX_MATCHING_DEADLINE_MS (a positive decimal integer of milliseconds from 1 to 2147483647, default 5000). Unset keeps the default. A deadline failure names the effective milliseconds, source (default or setting), the default, and the maximum, and leaves the API usable. The worker old-generation request is REA_REGEX_WORKER_HEAP_REQUEST_MIB (a decimal integer of mebibytes from 7 to 16384, default 128). Unset keeps the default. A positive Node --max-old-space-size, including NODE_OPTIONS, overrides that request and is not limited by the setting's floor or maximum. A heap failure names the effective request, its source (default or setting), the default, the floor, and the maximum.";
|
||||
|
||||
export const decompileAnalysisSelection = z
|
||||
.enum(["independent_fresh"])
|
||||
.optional()
|
||||
.describe(
|
||||
"Request a new independent decompile, bypassing project and snapshot results. Use only when the routed provider's procedure_pseudo_code capability declares independent_decompile; otherwise the request is refused. This may use different analysis context.",
|
||||
);
|
||||
|
||||
const official = <
|
||||
Name extends keyof typeof officialOutputSchemas,
|
||||
Schema extends z.ZodObject,
|
||||
@@ -207,8 +214,12 @@ export const OFFICIAL_TOOL_CONTRACTS = [
|
||||
),
|
||||
official(
|
||||
"procedure_pseudo_code",
|
||||
"Decompile one analyzed procedure by symbol name or provider-normalized address. Ghidra also accepts file-offset:0x... selectors and falls back to a unique source-file mapping when a 0x-prefixed number resolves to no procedure; existing analysis addresses take precedence and ambiguous mappings fail explicitly. Returns provider-specific pseudocode, never original source or cross-provider text equivalence, and may return null; request procedure_assembly when instruction precision matters. Ghidra no-return assumptions may cut the analyzed body; Evidence limitations report observed terminal calls and available callee identities.",
|
||||
z.object({ procedure, document }),
|
||||
"Decompile one analyzed procedure by symbol name or provider-normalized address. To request a fresh independent attempt, use analysis: independent_fresh when binary_session reports independent_decompile for this provider operation; otherwise it is refused. The independent result may have different analysis context. Ghidra also accepts file-offset:0x... selectors and falls back to a unique source-file mapping when a 0x-prefixed number resolves to no procedure; existing analysis addresses take precedence and ambiguous mappings fail explicitly. Returns provider-specific pseudocode, never original source or cross-provider text equivalence, and may return null; request procedure_assembly when instruction precision matters. Ghidra no-return assumptions may cut the analyzed body; Evidence limitations report observed terminal calls and available callee identities.",
|
||||
z.object({
|
||||
procedure,
|
||||
document,
|
||||
analysis: decompileAnalysisSelection,
|
||||
}),
|
||||
),
|
||||
official(
|
||||
"resolve_containing_procedure",
|
||||
|
||||
@@ -65,6 +65,7 @@ const providerCapabilityFacts = {
|
||||
requires_root: z.boolean(),
|
||||
}),
|
||||
limitations: z.array(z.string()),
|
||||
independent_decompile: z.literal(true).exactOptional(),
|
||||
};
|
||||
|
||||
export const providerCapability = z.discriminatedUnion("available", [
|
||||
@@ -99,6 +100,7 @@ const providerCapabilityGroupFacts = {
|
||||
operations: z.array(z.string().min(1)).min(1),
|
||||
effects: z.array(providerCapabilityFacts.effects.keyof()),
|
||||
limitations: z.array(z.string()),
|
||||
independent_decompile: providerCapabilityFacts.independent_decompile,
|
||||
};
|
||||
|
||||
const providerCapabilityGroup = z.discriminatedUnion("available", [
|
||||
@@ -136,6 +138,9 @@ export const expandProviderCapabilities = (
|
||||
group.operations.map((operation) => ({
|
||||
...providerCapability.parse({
|
||||
operation,
|
||||
...(group.independent_decompile === undefined
|
||||
? {}
|
||||
: { independent_decompile: group.independent_decompile }),
|
||||
available: group.available,
|
||||
reason: group.reason,
|
||||
availability_code: group.availability_code,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { z } from "zod";
|
||||
import { digestSchema } from "./digests.js";
|
||||
import { absoluteLocalPathStringSchema } from "./localPath.js";
|
||||
|
||||
/** Compact source facts supplied by the provider for one observation. */
|
||||
export const analysisSourceDescriptorSchema = z.discriminatedUnion("kind", [
|
||||
z.strictObject({
|
||||
kind: z.literal("file-range"),
|
||||
path: absoluteLocalPathStringSchema,
|
||||
sha256: digestSchema,
|
||||
byte_offset: z.int().nonnegative(),
|
||||
byte_length: z.int().nonnegative(),
|
||||
}),
|
||||
z.strictObject({ kind: z.literal("single-function-decompile") }),
|
||||
]);
|
||||
|
||||
export type AnalysisSourceDescriptor = z.infer<
|
||||
typeof analysisSourceDescriptorSchema
|
||||
>;
|
||||
@@ -4,6 +4,7 @@ import { utcDateTimeSchema } from "./offsetDateTime.js";
|
||||
import { browserCompletenessSchema } from "./browserCompleteness.js";
|
||||
import {
|
||||
cdpDomSnapshotSchema,
|
||||
cdpResourceColumnSchema,
|
||||
cdpScriptSourceSchema,
|
||||
cdpScriptMetadataSchema,
|
||||
} from "./cdpObservationSchemas.js";
|
||||
@@ -125,7 +126,7 @@ const browserNetworkRequestSchema = z.object({
|
||||
type: z.string(),
|
||||
url: z.string().nullable(),
|
||||
line: z.int().min(0).nullable(),
|
||||
column: z.int().min(0).nullable(),
|
||||
column: cdpResourceColumnSchema.nullable(),
|
||||
}),
|
||||
body_shapes: z.object({
|
||||
status: z.enum(["not_approved", "included", "partial", "unavailable"]),
|
||||
@@ -149,7 +150,7 @@ const browserConsoleEventSchema = z.object({
|
||||
),
|
||||
url: z.string().nullable(),
|
||||
line: z.int().min(0).nullable(),
|
||||
column: z.int().min(0).nullable(),
|
||||
column: cdpResourceColumnSchema.nullable(),
|
||||
text_capture: z.object({
|
||||
status: z.enum(["not_approved", "included", "unavailable"]),
|
||||
values: z.array(
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { z } from "zod";
|
||||
import { webTextArtifactSchema } from "./webContentArtifact.js";
|
||||
|
||||
/**
|
||||
* Zero-based UTF-16 column in a resource, as CDP reports it. CDP declares
|
||||
* columns as plain integers. Chrome reports a negative start column for some
|
||||
* inline scripts, and positions on a script's first line include that start
|
||||
* column, so a negative column is kept as reported. It does not locate a
|
||||
* column in the resource.
|
||||
*/
|
||||
export const cdpResourceColumnSchema = z.int();
|
||||
|
||||
/** Structural DOM facts shared by passive web and Electron observations. */
|
||||
export const cdpDomNodeSchema = z.object({
|
||||
index: z.number().int().min(0),
|
||||
|
||||
@@ -142,7 +142,7 @@ export const flutterResultSchemas = {
|
||||
.string()
|
||||
.nullable()
|
||||
.describe(
|
||||
"Labeled 'Dart VM version' string, when a build carries one",
|
||||
"Observed string matching the labeled 'Dart VM version' or unlabeled Dart Version::String() pattern; unlabeled matches are candidates rather than verified source identities",
|
||||
),
|
||||
toolchain_lines: z.array(z.string().min(1)),
|
||||
/** Dart labels omitted by grammar or single-version projection bounds. */
|
||||
|
||||
@@ -42,9 +42,11 @@ export const javaScriptSemanticQuerySeedSchema = z.discriminatedUnion("kind", [
|
||||
kind: z.literal("property"),
|
||||
name: z.string(),
|
||||
}),
|
||||
// Analysis preserves the complete literal endpoint, including the empty
|
||||
// string, so the seed matches any retained value exactly, as event does.
|
||||
z.strictObject({
|
||||
kind: z.literal("endpoint"),
|
||||
value: z.string().min(1),
|
||||
value: z.string(),
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.literal("event"),
|
||||
|
||||
@@ -122,6 +122,9 @@ it("retains high linked values and NOBITS without inventing file backing", () =>
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ machine: "EM_AARCH64", bits: 64, byte_order: "little" }, true],
|
||||
[{ machine: "EM_AARCH64", bits: 64, byte_order: "big" }, false],
|
||||
[{ machine: "EM_AARCH64", bits: 32, byte_order: "little" }, false],
|
||||
[{ machine: "EM_386", bits: 32, byte_order: "little" }, true],
|
||||
[{ machine: "EM_ARM", bits: 32, byte_order: "little" }, true],
|
||||
[{ machine: "EM_MIPS", bits: 32, byte_order: "little" }, true],
|
||||
@@ -150,7 +153,7 @@ it.each([
|
||||
if (problem === "file-backed-outside") section.file_backing = "file";
|
||||
if (problem === "header-outside") section.header_location.offset = "0x80";
|
||||
if (problem === "wrong-index") section.index = 1;
|
||||
if (problem === "wrong-machine") value.architecture.machine = "EM_AARCH64";
|
||||
if (problem === "wrong-machine") value.architecture.machine = "EM_RISCV";
|
||||
if (problem === "outside-elf64")
|
||||
value.entry_point.reported_value = "0x10000000000000000";
|
||||
if (problem === "noncanonical-base64") section.name.bytes_base64 = "AR==";
|
||||
|
||||
@@ -35,7 +35,7 @@ const binaryLayoutObjectSchema = z.strictObject({
|
||||
format: z.literal("elf"),
|
||||
architecture: z.union([
|
||||
z.strictObject({
|
||||
machine: z.literal("EM_X86_64"),
|
||||
machine: z.enum(["EM_X86_64", "EM_AARCH64"]),
|
||||
bits: z.literal(64),
|
||||
byte_order: z.literal("little"),
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { objcSwiftMetadataSchema } from "./objcSwiftMetadata.js";
|
||||
import {
|
||||
inspectNativeDispatchMetadata,
|
||||
objcSwiftMetadataSchema,
|
||||
} from "./objcSwiftMetadata.js";
|
||||
|
||||
import {
|
||||
nativeInvestigationGraphSchema,
|
||||
@@ -191,9 +194,18 @@ describe("native investigation graph traces", () => {
|
||||
});
|
||||
|
||||
describe("Interface Builder dispatch joins", () => {
|
||||
it.each(["BuildViewController", "BuildView", "BuildButton", "BuildHandler"])(
|
||||
"joins an action to its declared %s receiver class",
|
||||
(className) => {
|
||||
it.each([
|
||||
{
|
||||
className: "BuildViewController",
|
||||
recordOffset: 1944,
|
||||
existingOffset: null,
|
||||
},
|
||||
{ className: "BuildView", recordOffset: 6040, existingOffset: null },
|
||||
{ className: "BuildButton", recordOffset: null, existingOffset: null },
|
||||
{ className: "BuildHandler", recordOffset: 1944, existingOffset: 1916 },
|
||||
])(
|
||||
"joins the declared $className without mixing record and implementation offsets",
|
||||
({ className, recordOffset, existingOffset }) => {
|
||||
const ui = buildInterfaceBuilderAnalysis({
|
||||
targetSha256: "b".repeat(64),
|
||||
toolVersion: "test",
|
||||
@@ -221,38 +233,80 @@ describe("Interface Builder dispatch joins", () => {
|
||||
],
|
||||
limits: interfaceBuilderLimitsSchema.parse({}),
|
||||
});
|
||||
const joined = joinInterfaceBuilderDispatch(ui.graph, {
|
||||
objc_classes: [],
|
||||
objc_protocols: [],
|
||||
objc_categories: [],
|
||||
swift_decls: [],
|
||||
objc_ivars: [],
|
||||
objc_protocol_records: [],
|
||||
const recordLocation = {
|
||||
address: recordOffset === null ? "0x1000" : "0x2000",
|
||||
file_offset: recordOffset,
|
||||
};
|
||||
const recordEvidence = [
|
||||
{
|
||||
kind: recordOffset === null ? "symbol" : "binary_metadata",
|
||||
description: "Explicit synthetic method-record location",
|
||||
location: recordLocation,
|
||||
artifact_path: "/synthetic/Fixture",
|
||||
artifact_sha256: "b".repeat(64),
|
||||
},
|
||||
];
|
||||
const mappedEvidence = [
|
||||
{
|
||||
...recordEvidence[0],
|
||||
kind: "reference",
|
||||
description: "Explicit independently mapped handler",
|
||||
location: { address: "0x1000", file_offset: existingOffset },
|
||||
},
|
||||
];
|
||||
const inputGraph =
|
||||
existingOffset === null
|
||||
? ui.graph
|
||||
: nativeInvestigationGraphSchema.parse({
|
||||
...ui.graph,
|
||||
nodes: [
|
||||
...ui.graph.nodes,
|
||||
{
|
||||
id: "native:function:0x1000",
|
||||
kind: "function",
|
||||
name: "Already mapped handler",
|
||||
location: { address: "0x1000", file_offset: existingOffset },
|
||||
attributes: {},
|
||||
evidence: mappedEvidence,
|
||||
},
|
||||
],
|
||||
});
|
||||
const metadata = objcSwiftMetadataSchema.parse({
|
||||
...inspectNativeDispatchMetadata([], 1),
|
||||
objc_dispatch_implementations: [
|
||||
{
|
||||
class_name: className,
|
||||
selector: "buildTapped:",
|
||||
method_type: "instance",
|
||||
implementation_address: "0x1000",
|
||||
location: { address: "0x1000", file_offset: 0 },
|
||||
location: recordLocation,
|
||||
decode: { status: "decoded", reason: null },
|
||||
evidence,
|
||||
evidence: recordEvidence,
|
||||
},
|
||||
],
|
||||
swift_conformances: [],
|
||||
swift_dispatch_slots: [],
|
||||
swift_symbols: [],
|
||||
relative_pointers: [],
|
||||
coverage: [],
|
||||
db_save_result: null,
|
||||
});
|
||||
const joined = joinInterfaceBuilderDispatch(inputGraph, metadata);
|
||||
expect(joined.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
relation: "objc_dispatch",
|
||||
resolution: "resolved",
|
||||
to: "native:function:0x1000",
|
||||
evidence: expect.arrayContaining(recordEvidence),
|
||||
}),
|
||||
);
|
||||
const handler = joined.nodes.find(
|
||||
({ id }) => id === "native:function:0x1000",
|
||||
);
|
||||
expect(handler?.location).toEqual({
|
||||
address: "0x1000",
|
||||
file_offset: existingOffset,
|
||||
});
|
||||
expect(handler?.evidence).toEqual(
|
||||
existingOffset === null ? recordEvidence : mappedEvidence,
|
||||
);
|
||||
expect(metadata.objc_dispatch_implementations[0]?.location).toEqual(
|
||||
recordLocation,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -391,7 +391,8 @@ export const joinInterfaceBuilderDispatch = (
|
||||
name: `${implementation.class_name} ${selector}`,
|
||||
location: {
|
||||
address: implementation.implementation_address,
|
||||
file_offset: implementation.location.file_offset,
|
||||
// The implementation location identifies the method record, not its code.
|
||||
file_offset: null,
|
||||
},
|
||||
attributes: {
|
||||
class_name: implementation.class_name,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
browserObservationEndpointSchema,
|
||||
} from "./browserObservation.js";
|
||||
import { browserVersionSchema } from "./browserObservationSchemas.js";
|
||||
import { cdpResourceColumnSchema } from "./cdpObservationSchemas.js";
|
||||
import { jsonValueSchema } from "./jsonValue.js";
|
||||
import { digestSchema, prefixedDigestSchema } from "./../domain/digests.js";
|
||||
|
||||
@@ -44,7 +45,7 @@ const webMcpToolSchema = z.object({
|
||||
.object({
|
||||
url: z.string(),
|
||||
line: z.int().min(0).nullable(),
|
||||
column: z.int().min(0).nullable(),
|
||||
column: cdpResourceColumnSchema.nullable(),
|
||||
})
|
||||
.nullable(),
|
||||
trust: z.literal("page-declared-untrusted"),
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
webRuntimeCommandTimeoutSchema,
|
||||
} from "./browserDeadline.js";
|
||||
import { jsonObjectSchema } from "./jsonValue.js";
|
||||
import { cdpResourceColumnSchema } from "./cdpObservationSchemas.js";
|
||||
|
||||
// CDP positions are zero-based and relative to the resource named by url.
|
||||
// With a script ID that is the script's enclosing resource: the script file,
|
||||
@@ -18,17 +19,18 @@ import { jsonObjectSchema } from "./jsonValue.js";
|
||||
// association to join captured source or resource_start.
|
||||
// trace_web_source_location takes one-based, script-relative lines, so the
|
||||
// advertised units keep callers from passing a position off by one or by an
|
||||
// inline script's offset.
|
||||
// inline script's offset. Columns are kept as reported even when Chrome reports
|
||||
// a negative one, so subtracting resource_start's column stays consistent.
|
||||
const SCRIPT_ID =
|
||||
"CDP session script ID; empty for a position a request initiator reported, which has no session-script association for joining captured source or resource_start.";
|
||||
const RESOURCE_LINE =
|
||||
"Zero-based line in the resource named by url, as reported by CDP: the script file for an external script (including a module-import initiator), or the HTML document for an inline script or parser markup.";
|
||||
const RESOURCE_COLUMN =
|
||||
"Zero-based UTF-16 column on that line of the resource named by url, as reported by CDP.";
|
||||
"Zero-based UTF-16 column on that line of the resource named by url, as reported by CDP. Chrome can report a negative column, for example on the first line of a script whose resource_start column is negative; it then does not locate a column in url.";
|
||||
const START_LINE =
|
||||
"Zero-based line where the script text starts in its enclosing resource (the HTML document for an inline script).";
|
||||
const START_COLUMN =
|
||||
"Zero-based UTF-16 column where the script text starts on that line of its enclosing resource.";
|
||||
"Zero-based UTF-16 column where the script text starts on that line of its enclosing resource. Chrome reports a negative column for some inline scripts; it is kept as reported and does not locate a column in that resource.";
|
||||
|
||||
/** Scope for one externally owned page; omitted origins select its current origin. */
|
||||
export const webRuntimeScopeSchema = z.strictObject({
|
||||
@@ -53,7 +55,7 @@ export const webRuntimeSourceSchema = z.object({
|
||||
resource_start: z
|
||||
.object({
|
||||
line_number: z.int().min(0).describe(START_LINE),
|
||||
column_number: z.int().min(0).describe(START_COLUMN),
|
||||
column_number: cdpResourceColumnSchema.describe(START_COLUMN),
|
||||
})
|
||||
.nullable(),
|
||||
source: z.discriminatedUnion("state", [
|
||||
@@ -77,7 +79,7 @@ export const webRuntimeLocationSchema = z.object({
|
||||
script_id: z.string().describe(SCRIPT_ID),
|
||||
url: z.string().nullable(),
|
||||
line_number: z.int().min(0).describe(RESOURCE_LINE),
|
||||
column_number: z.int().min(0).nullable().describe(RESOURCE_COLUMN),
|
||||
column_number: cdpResourceColumnSchema.nullable().describe(RESOURCE_COLUMN),
|
||||
function_name: z.string().nullable(),
|
||||
source_association: z.enum(["script_id", "unknown"]),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EvmAdmissionError } from "./EvmAdmissionError.js";
|
||||
import type { ProviderRejectionCode } from "../domain/providerSelection.js";
|
||||
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { safeParseUtf8Json } from "../domain/safeJson.js";
|
||||
@@ -115,6 +116,7 @@ type EvmLimiterSelection = {
|
||||
readonly diagnostics: Readonly<Record<string, JsonValue>>;
|
||||
};
|
||||
type EvmLimiterUnavailable = EvmLimiterSelection & {
|
||||
readonly code: ProviderRejectionCode;
|
||||
readonly reason: string;
|
||||
readonly cause: unknown;
|
||||
};
|
||||
@@ -159,16 +161,39 @@ const selectEvmLimiter = async (
|
||||
if (cause instanceof EvmAdmissionError)
|
||||
return err({
|
||||
path,
|
||||
code: "not_configured",
|
||||
diagnostics: {
|
||||
...diagnostics,
|
||||
code: cause.code,
|
||||
...cause.detail,
|
||||
},
|
||||
reason: cause.code,
|
||||
reason: `Set REA_EVM_PRLIMIT_COMMAND to an absolute executable regular file for caller-supplied util-linux prlimit. The selected path ${path} ${cause.code === "prlimit_not_absolute" ? "is not absolute" : "is not a regular file"}.`,
|
||||
cause,
|
||||
});
|
||||
const reason = `Offline EVM interface inspection requires caller-supplied util-linux prlimit: ${path}; ${cause instanceof Error ? cause.message : String(cause)}`;
|
||||
return err({ path, diagnostics, reason, cause });
|
||||
const systemCode =
|
||||
cause instanceof Error &&
|
||||
"code" in cause &&
|
||||
typeof cause.code === "string"
|
||||
? cause.code
|
||||
: undefined;
|
||||
const permissionDenied = systemCode === "EACCES" || systemCode === "EPERM";
|
||||
const missing = systemCode === "ENOENT" || systemCode === "ENOTDIR";
|
||||
const recovery = permissionDenied
|
||||
? "Grant execute permission and filesystem access to the selected file, or set REA_EVM_PRLIMIT_COMMAND to an accessible absolute executable regular file for util-linux prlimit."
|
||||
: missing
|
||||
? "Set REA_EVM_PRLIMIT_COMMAND to an existing absolute executable regular file for util-linux prlimit."
|
||||
: "Check the selected file and filesystem access, or set REA_EVM_PRLIMIT_COMMAND to an accessible absolute executable regular file for util-linux prlimit.";
|
||||
const reason = `Offline EVM interface inspection requires caller-supplied util-linux prlimit: ${path}; ${cause instanceof Error ? cause.message : String(cause)}. ${recovery}`;
|
||||
return err({
|
||||
path,
|
||||
code: missing ? "executable_missing" : "runtime_unavailable",
|
||||
diagnostics: {
|
||||
...diagnostics,
|
||||
...(systemCode === undefined ? {} : { system_code: systemCode }),
|
||||
},
|
||||
reason,
|
||||
cause,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -193,7 +218,7 @@ export const evmLimiterAvailability = async (
|
||||
}
|
||||
: {
|
||||
status: "unavailable",
|
||||
code: "executable_missing",
|
||||
code: selected.error.code,
|
||||
reason: selected.error.reason,
|
||||
// The limiter owner does not yet author a separate repair (#8508).
|
||||
remediation: selected.error.reason,
|
||||
|
||||
@@ -1023,7 +1023,7 @@ export class FlutterBuildProvider {
|
||||
limitations: [
|
||||
"The APK is read through an owned read-only snapshot; concurrent changes to the original cannot mix into this observation",
|
||||
"The snapshot hash identifies a Dart SDK build only against an external mapping dataset; REA performs no lookup",
|
||||
"Release engine builds often carry no labeled Dart version string; absence is reported, not guessed",
|
||||
"Unlabeled Dart version values are identified by their printable-string shape, not a symbol or other source identity; a matching unrelated string may be reported and a null value means no supported pattern was found",
|
||||
"Engine build IDs come only from ELF-declared PT_NOTE/SHT_NOTE records; conflicting or incompletely examined notes preserve candidates and report partial coverage",
|
||||
...[...facts].flatMap(([abi, value]) => [
|
||||
...value.libflutter.buildIdFailures.map(
|
||||
|
||||
@@ -124,34 +124,26 @@ it("distinguishes a full candidate set from omitted conflicts", () => {
|
||||
expect(scanner.result().candidates).toHaveLength(64);
|
||||
});
|
||||
|
||||
describe("LabeledStringScanner", () => {
|
||||
const assertSingleToolchainLine = (line: string): void => {
|
||||
const scanner = new LabeledStringScanner();
|
||||
scanner.push(
|
||||
Uint8Array.from(
|
||||
Array.from(`\x00${line}\x00`, (character) => character.charCodeAt(0)),
|
||||
),
|
||||
);
|
||||
expect(scanner.result().toolchainLines).toEqual([line]);
|
||||
};
|
||||
const assertSingleToolchainLine = (line: string): void => {
|
||||
const scanner = new LabeledStringScanner();
|
||||
scanner.push(
|
||||
Uint8Array.from(
|
||||
Array.from(`\x00${line}\x00`, (character) => character.charCodeAt(0)),
|
||||
),
|
||||
);
|
||||
expect(scanner.result().toolchainLines).toEqual([line]);
|
||||
};
|
||||
|
||||
describe("LabeledStringScanner toolchain lines", () => {
|
||||
it("collects a full clang line within the 500-character bound", () => {
|
||||
assertSingleToolchainLine(
|
||||
"Android (5900059 based on r365631c) clang version 9.0.8 (https://android.googlesource.com/toolchain/llvm-project 207d7abc1a2abf3ef8d4301736d6a7ebc224a290) (based on LLVM 9.0.8svn)",
|
||||
);
|
||||
});
|
||||
|
||||
it("reads a labeled Dart VM version string when a build carries one", () => {
|
||||
const scanner = new LabeledStringScanner();
|
||||
scanner.push(
|
||||
Uint8Array.from(
|
||||
Array.from(
|
||||
"\x00Dart VM version: 3.1.0 (stable) somewhere\x00",
|
||||
(character) => character.charCodeAt(0),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(scanner.result().dartVersion).toBe(
|
||||
"Dart VM version: 3.1.0 (stable) somewhere",
|
||||
it("collects the labeled Android clang line", () => {
|
||||
assertSingleToolchainLine(
|
||||
"Android (5900059 based on r365631c) clang version 9.0.8 (https://android.googlesource.com/toolchain/llvm-project 207d7abc) (based on LLVM 9.0.8svn)",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -170,6 +162,67 @@ describe("LabeledStringScanner", () => {
|
||||
expect(result.toolchainLines).toEqual([]);
|
||||
});
|
||||
|
||||
it("preserves trailing whitespace on LLVM toolchain lines", () => {
|
||||
assertSingleToolchainLine("based on LLVM 9 ");
|
||||
});
|
||||
});
|
||||
|
||||
describe("LabeledStringScanner Dart version strings", () => {
|
||||
it("reads a labeled Dart VM version string when a build carries one", () => {
|
||||
const scanner = new LabeledStringScanner();
|
||||
scanner.push(
|
||||
Uint8Array.from(
|
||||
Array.from(
|
||||
"\x00Dart VM version: 3.1.0 (stable) somewhere\x00",
|
||||
(character) => character.charCodeAt(0),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(scanner.result().dartVersion).toBe(
|
||||
"Dart VM version: 3.1.0 (stable) somewhere",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"release engine",
|
||||
'3.7.2 (stable) (Tue Mar 11 04:27:50 2025 -0700) on "android_arm64"',
|
||||
],
|
||||
[
|
||||
"Flutter Gallery engine",
|
||||
"2.14.0-151.0.dev (dev) (Tue May 25 10:58:39 2021 -0700)",
|
||||
],
|
||||
] as const)(
|
||||
"reads an unlabeled Dart Version::String() from a %s",
|
||||
(_label, line) => {
|
||||
const scanner = new LabeledStringScanner();
|
||||
scanner.push(Buffer.from(`\x00${line}\x00`));
|
||||
expect(scanner.result()).toMatchObject({
|
||||
dartVersion: line,
|
||||
dartVersionCapped: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("ignores unlabeled version-shaped text without a channel token", () => {
|
||||
const scanner = new LabeledStringScanner();
|
||||
scanner.push(
|
||||
Buffer.from(
|
||||
'1.2.3 (Tue May 25 10:58:39 2021 -0700) on "android_arm64"\x00',
|
||||
),
|
||||
);
|
||||
expect(scanner.result().dartVersion).toBeNull();
|
||||
});
|
||||
|
||||
it("does not claim a Dart cap for unlabeled runs that miss the grammar", () => {
|
||||
const scanner = new LabeledStringScanner();
|
||||
scanner.push(Buffer.from("3.7.2 (stable)\x00"));
|
||||
expect(scanner.result()).toMatchObject({
|
||||
dartVersion: null,
|
||||
dartVersionCapped: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("claims the cap only for an overlong run shaped like a toolchain line", () => {
|
||||
const overlongToolchain = `Android (${"r".repeat(700)}) clang version 9`;
|
||||
for (const [text, toolchainCapped, dartVersionCapped] of [
|
||||
@@ -194,6 +247,13 @@ describe("LabeledStringScanner", () => {
|
||||
|
||||
it.each([
|
||||
{ label: "Dart", line: `Dart VM version:${"x".repeat(200)}`, dart: true },
|
||||
{
|
||||
label: "embedded Dart",
|
||||
line: '3.7.2 (stable) (Tue Mar 11 04:27:50 2025 -0700) on "android_arm64"',
|
||||
dart: true,
|
||||
// Trailing junk breaks the whole-run grammar; do not treat as a cap.
|
||||
invalidCaps: false,
|
||||
},
|
||||
{
|
||||
label: "Android clang",
|
||||
line: `Android (${"r".repeat(80)}) clang version ${"x".repeat(500)}`,
|
||||
@@ -202,7 +262,7 @@ describe("LabeledStringScanner", () => {
|
||||
{ label: "LLVM", line: `based on LLVM 9${"x".repeat(80)}`, dart: false },
|
||||
])(
|
||||
"requires the complete $label run at its own grammar boundary",
|
||||
({ line, dart }) => {
|
||||
({ line, dart, invalidCaps }) => {
|
||||
for (const suffix of ["", "x", "x".repeat(1000)]) {
|
||||
const payload = Buffer.from(`${line}${suffix}\x00`);
|
||||
for (const chunkSize of [1, 7, payload.length]) {
|
||||
@@ -210,9 +270,10 @@ describe("LabeledStringScanner", () => {
|
||||
for (let offset = 0; offset < payload.length; offset += chunkSize)
|
||||
scanner.push(payload.subarray(offset, offset + chunkSize));
|
||||
const valid = suffix === "";
|
||||
const capsInvalid = invalidCaps ?? true;
|
||||
expect(scanner.result()).toEqual({
|
||||
dartVersion: dart && valid ? line : null,
|
||||
dartVersionCapped: dart && !valid,
|
||||
dartVersionCapped: dart && !valid && capsInvalid,
|
||||
toolchainLines: !dart && valid ? [line] : [],
|
||||
toolchainCapped: !dart && !valid,
|
||||
});
|
||||
@@ -221,8 +282,7 @@ describe("LabeledStringScanner", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves whitespace and separates distinct Dart-label omissions", () => {
|
||||
assertSingleToolchainLine("based on LLVM 9 ");
|
||||
it("separates distinct Dart-label omissions", () => {
|
||||
const scanner = new LabeledStringScanner();
|
||||
scanner.push(Buffer.from("Dart VM version: 3.1\0Dart VM version: 3.2\0"));
|
||||
expect(scanner.result()).toMatchObject({
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Buffer } from "node:buffer";
|
||||
*
|
||||
* All facts are observations of byte patterns at read time: the Dart
|
||||
* snapshot hash sits 20 bytes after each snapshot magic, the GNU build-id
|
||||
* is an ELF note, and version facts are labeled ASCII strings. Nothing is
|
||||
* inferred or looked up externally.
|
||||
* is an ELF note, and version facts are ASCII strings. Nothing is inferred or
|
||||
* looked up externally.
|
||||
*/
|
||||
|
||||
/** First index of `needle` in `haystack` at or after `from`, or -1. */
|
||||
@@ -124,6 +124,13 @@ export interface LabeledStringScan {
|
||||
}
|
||||
|
||||
const DART_VERSION_PATTERN = /^Dart VM version:[^\x00\n]{0,200}$/u;
|
||||
/**
|
||||
* Unlabeled Dart `Version::String()` form embedded in release engines:
|
||||
* `<version> (<channel>) (<commit time>)` with optional ` on "<os>_<arch>"`.
|
||||
* Requires a channel token so ordinary dotted version text is ignored.
|
||||
*/
|
||||
const DART_EMBEDDED_VERSION_PATTERN =
|
||||
/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)? \((?:stable|beta|dev|main)\) \([^)\x00\n]{1,80}\)(?: on "[a-z0-9._-]+")?$/u;
|
||||
const TOOLCHAIN_PATTERN =
|
||||
/^(?:Android \([^)\x00\n]{0,80}\) clang version [^\x00\n]{0,500}|based on LLVM \d[^\x00\n]{0,80})$/u;
|
||||
const MAX_TOOLCHAIN_LINES = 4;
|
||||
@@ -135,11 +142,11 @@ const MAX_TOOLCHAIN_LINES = 4;
|
||||
const CARRY_BYTES = 640;
|
||||
|
||||
/**
|
||||
* Incremental ASCII string collector that keeps only labeled version and
|
||||
* toolchain facts. A match is committed only when a printable run is
|
||||
* terminated by a delimiter, so a value split across chunk boundaries is
|
||||
* never committed truncated; the carry holds the longest accepted match
|
||||
* so boundaries in either pattern survive.
|
||||
* Incremental ASCII string collector that keeps Dart version and toolchain
|
||||
* facts. A match is committed only when a printable run is terminated by a
|
||||
* delimiter, so a value split across chunk boundaries is never committed
|
||||
* truncated; the carry holds the longest accepted match so boundaries in
|
||||
* either pattern survive.
|
||||
*/
|
||||
export class LabeledStringScanner {
|
||||
#dartVersion: string | null = null;
|
||||
@@ -173,15 +180,28 @@ export class LabeledStringScanner {
|
||||
this.overflowed = false;
|
||||
}
|
||||
|
||||
#recordDartVersion(text: string, accepted: boolean): void {
|
||||
if (!accepted) {
|
||||
this.#dartVersionCapped = true;
|
||||
return;
|
||||
}
|
||||
if (this.#dartVersion === null) this.#dartVersion = text;
|
||||
else if (this.#dartVersion !== text) this.#dartVersionCapped = true;
|
||||
}
|
||||
|
||||
#consider(text: string): void {
|
||||
// Keep a bounded prefix plus overflow state, never a sliding suffix.
|
||||
// Every accepted label must consume its whole printable run so a
|
||||
// pattern's shorter grammar cannot publish a fabricated prefix.
|
||||
if (text.startsWith("Dart VM version:")) {
|
||||
if (this.overflowed || !DART_VERSION_PATTERN.test(text))
|
||||
this.#dartVersionCapped = true;
|
||||
else if (this.#dartVersion === null) this.#dartVersion = text;
|
||||
else if (this.#dartVersion !== text) this.#dartVersionCapped = true;
|
||||
this.#recordDartVersion(
|
||||
text,
|
||||
!this.overflowed && DART_VERSION_PATTERN.test(text),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (DART_EMBEDDED_VERSION_PATTERN.test(text)) {
|
||||
this.#recordDartVersion(text, !this.overflowed);
|
||||
return;
|
||||
}
|
||||
if (!text.startsWith("Android (") && !text.startsWith("based on LLVM "))
|
||||
|
||||
+1
-23
@@ -341,28 +341,6 @@ export const copyAndHash = async (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* The same run takes as long again, so only a longer deadline helps. Only a
|
||||
* session that outlives this request can also read the later project export.
|
||||
*/
|
||||
export const kunaTimeoutRecovery = (
|
||||
setting: string,
|
||||
subcommand: string,
|
||||
): {
|
||||
readonly remediationAction: string;
|
||||
readonly continuingCallerGuidance?: { readonly remediationAction: string };
|
||||
} => {
|
||||
const remediationAction = `Raise ${setting} in the environment that starts REA, then restart REA; it reads Kuna settings when it starts.`;
|
||||
return subcommand === "decompile"
|
||||
? {
|
||||
remediationAction,
|
||||
continuingCallerGuidance: {
|
||||
remediationAction: `${remediationAction} Within a session, pseudocode is also read from the project export once it finishes.`,
|
||||
},
|
||||
}
|
||||
: { remediationAction };
|
||||
};
|
||||
|
||||
/** Map an owned-command failure to its typed error, keeping unproven cleanup owned. */
|
||||
const commandFailure = (
|
||||
cause: unknown,
|
||||
@@ -413,7 +391,7 @@ const commandFailure = (
|
||||
primary = new AnalysisTimeoutError(operation, ms, {
|
||||
...details,
|
||||
userMessage: `kuna ${subcommand} did not finish within ${setting} (${String(ms)} ms).`,
|
||||
...kunaTimeoutRecovery(setting, subcommand),
|
||||
remediationAction: `Raise ${setting} in the environment that starts REA, then restart REA; it reads Kuna settings when it starts.`,
|
||||
});
|
||||
}
|
||||
// The same input overflows the same fixed buffer; waiting cannot help.
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface KunaProjectFileSpan {
|
||||
|
||||
export interface KunaProjectText {
|
||||
readonly result: string | null;
|
||||
readonly unavailableReason?: string;
|
||||
readonly files?: readonly KunaProjectFileSpan[];
|
||||
readonly limitations: readonly string[];
|
||||
}
|
||||
@@ -314,13 +315,15 @@ export class KunaProjectReader {
|
||||
this.project.functions.map((block) => [block.address, block]),
|
||||
);
|
||||
const block = this.#blocks.get(address);
|
||||
if (block === undefined)
|
||||
if (block === undefined) {
|
||||
const reason =
|
||||
"Kuna's export decompiles code entries only; this inventory entry, such as an import slot, has no body.";
|
||||
return ok({
|
||||
result: null,
|
||||
limitations: [
|
||||
"Kuna's export decompiles code entries only; this inventory entry, such as an import slot, has no body.",
|
||||
],
|
||||
unavailableReason: reason,
|
||||
limitations: [reason],
|
||||
});
|
||||
}
|
||||
const files = [
|
||||
{
|
||||
facet: "pseudocode" as const,
|
||||
@@ -329,12 +332,18 @@ export class KunaProjectReader {
|
||||
byteLength: block.cLength,
|
||||
},
|
||||
];
|
||||
if (block.error !== null)
|
||||
if (block.error !== null) {
|
||||
const reason = `Kuna could not decompile this function: ${block.error}`;
|
||||
return ok({
|
||||
result: null,
|
||||
unavailableReason: reason,
|
||||
files,
|
||||
limitations: [`Kuna could not decompile this function: ${block.error}`],
|
||||
limitations: [
|
||||
reason,
|
||||
"To try an independent decompile, call procedure_pseudo_code with analysis: independent_fresh, or use rea decompile --analysis independent_fresh. It may use different analysis context and does not replace this observation.",
|
||||
],
|
||||
});
|
||||
}
|
||||
const text = await readSpan(
|
||||
operation,
|
||||
join(this.project.directory, this.project.manifest.files.c.path),
|
||||
|
||||
@@ -104,6 +104,9 @@ export const kunaCapabilities = (
|
||||
KUNA_OPERATIONS.map((operation) => ({
|
||||
provider: KUNA_PROVIDER_IDENTITY,
|
||||
operation,
|
||||
...(operation === "procedure_pseudo_code"
|
||||
? { independentDecompile: true as const }
|
||||
: {}),
|
||||
...(availability.status === "available"
|
||||
? ({ available: true, reason: null } as const)
|
||||
: ({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { bindArtifactFileCleanup } from "../artifacts/ArtifactFileOperation.js";
|
||||
import { basename } from "node:path";
|
||||
import { basename, join } from "node:path";
|
||||
import {
|
||||
regularFileCancellation,
|
||||
RegularFileCleanupFailure,
|
||||
@@ -258,7 +258,7 @@ const exportUnavailable = (
|
||||
"REA reads Kuna settings when it starts, so restart REA, then reopen the target.";
|
||||
const next =
|
||||
failure instanceof AnalysisTimeoutError
|
||||
? `To retry, set REA_KUNA_PROJECT_TIMEOUT_MS above ${String(failure.timeoutMs)} ms or raise REA_KUNA_JOBS in the environment that starts REA. ${restart}`
|
||||
? `To retry, set REA_KUNA_PROJECT_TIMEOUT_MS above ${String(failure.timeoutMs)} ms in the environment that starts REA. ${restart}`
|
||||
: failure instanceof AnalysisResourceConstraintError &&
|
||||
failure.remediationAction !== undefined
|
||||
? // A fixed buffer (`memory`) is no setting, so a restart cannot help.
|
||||
@@ -501,7 +501,8 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
}),
|
||||
);
|
||||
if (
|
||||
operation === "procedure_pseudo_code" ||
|
||||
(operation === "procedure_pseudo_code" &&
|
||||
parameters.analysis !== "independent_fresh") ||
|
||||
operation === "procedure_assembly" ||
|
||||
operation === "analyze_function"
|
||||
)
|
||||
@@ -624,6 +625,30 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
// component once, including its producing file digest and byte range.
|
||||
const recordedFiles =
|
||||
files.length === 1 ? files[0] : files.length === 0 ? {} : { files };
|
||||
const rawSource =
|
||||
source === null
|
||||
? undefined
|
||||
: source.kind === "single-function"
|
||||
? { kind: "single-function" }
|
||||
: {
|
||||
kind: "project-export",
|
||||
key: source.project.key,
|
||||
directory: source.project.directory,
|
||||
...recordedFiles,
|
||||
};
|
||||
const file = files.length === 1 ? files[0] : undefined;
|
||||
const sourceDescriptor =
|
||||
source?.kind === "single-function"
|
||||
? { kind: "single-function-decompile" as const }
|
||||
: source?.kind === "project-export" && file?.file_sha256 != null
|
||||
? {
|
||||
kind: "file-range" as const,
|
||||
path: join(source.project.directory, file.file),
|
||||
sha256: file.file_sha256,
|
||||
byte_offset: file.byte_offset,
|
||||
byte_length: file.byte_length,
|
||||
}
|
||||
: undefined;
|
||||
return createAnalysisExecution(
|
||||
answer.result,
|
||||
{
|
||||
@@ -632,6 +657,7 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
version: installation.version,
|
||||
},
|
||||
{
|
||||
...(sourceDescriptor === undefined ? {} : { sourceDescriptor }),
|
||||
// Kuna's identity is in the analysis profile; the raw result records
|
||||
// only where file-backed text came from.
|
||||
...(source === null &&
|
||||
@@ -643,15 +669,7 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
...(source === null
|
||||
? {}
|
||||
: {
|
||||
source:
|
||||
source.kind === "single-function"
|
||||
? { kind: "single-function" }
|
||||
: {
|
||||
kind: "project-export",
|
||||
key: source.project.key,
|
||||
directory: source.project.directory,
|
||||
...recordedFiles,
|
||||
},
|
||||
source: rawSource,
|
||||
project_export: this.#exportState(),
|
||||
}),
|
||||
...(answer.referenceSource === undefined
|
||||
@@ -774,6 +792,33 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
limitations: classification,
|
||||
});
|
||||
case "procedure_pseudo_code": {
|
||||
if (parameters.analysis === "independent_fresh") {
|
||||
const signal =
|
||||
options.signal === undefined
|
||||
? this.#session.signal
|
||||
: AbortSignal.any([options.signal, this.#session.signal]);
|
||||
const standalone = await this.#singleFunction(
|
||||
operation,
|
||||
selected.address,
|
||||
installation,
|
||||
signal,
|
||||
true,
|
||||
);
|
||||
if (standalone === EXPORT_READY)
|
||||
throw new Error(
|
||||
"Independent decompilation unexpectedly yielded to project",
|
||||
);
|
||||
if (!standalone.ok) return standalone;
|
||||
return ok({
|
||||
result: standalone.value,
|
||||
source: { kind: "single-function" },
|
||||
locations,
|
||||
limitations: [
|
||||
"This is a fresh independent single-function decompile; it does not use the project export's whole-program prototypes.",
|
||||
...classification,
|
||||
],
|
||||
});
|
||||
}
|
||||
const pseudocode = await this.#pseudocode(
|
||||
operation,
|
||||
selected,
|
||||
@@ -1057,6 +1102,9 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
err(
|
||||
new AnalysisInputError(operation, {
|
||||
userMessage: kunaDataEntryMessage(entry, index.data.get(entry)!),
|
||||
conformsToInputContract: true,
|
||||
remediationAction:
|
||||
"Inspect the data address with address_name or xrefs, or select a procedure entry from list_procedures. Do not repeat the unchanged call.",
|
||||
}),
|
||||
);
|
||||
const selected = (entry: KunaInventoryFunction) => {
|
||||
@@ -1076,6 +1124,9 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
return err(
|
||||
new AnalysisInputError(operation, {
|
||||
userMessage: `${address} is not a procedure entry Kuna found; list_procedures shows the entries it found.`,
|
||||
conformsToInputContract: true,
|
||||
remediationAction:
|
||||
"Inspect list_procedures and select a discovered entry if it answers your question. An unlisted address is not proof of a function; REA cannot use it as a Kuna procedure in this session. Do not repeat the unchanged call.",
|
||||
}),
|
||||
);
|
||||
return refused(entry) ? dataRefusal(entry) : selected(entry);
|
||||
@@ -1088,9 +1139,20 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
return dataRefusal(named[0]);
|
||||
return err(
|
||||
new AnalysisInputError(operation, {
|
||||
conformsToInputContract: true,
|
||||
remediationAction:
|
||||
matches.length === 0 && named.length === 0
|
||||
? field === "procedure"
|
||||
? "Use search_procedures or list_procedures to inspect Kuna's discovered entries, then select a matching entry. Do not repeat the unchanged call."
|
||||
: "Inspect the selected target for the intended address and use that address explicitly. Do not repeat the unchanged call."
|
||||
: matches.length === 0
|
||||
? "Inspect the data addresses with address_name or xrefs, or select a procedure entry from list_procedures. Do not repeat the unchanged call."
|
||||
: "Select the intended entry by its exact address. Do not repeat the unchanged call.",
|
||||
userMessage:
|
||||
matches.length === 0 && named.length === 0
|
||||
? `Kuna found no procedure named ${selector}; search_procedures finds similar names.`
|
||||
? field === "procedure"
|
||||
? `Kuna found no procedure named ${selector}; search_procedures finds similar names.`
|
||||
: `Kuna found no inventory entry named ${selector}.`
|
||||
: matches.length === 0
|
||||
? `${selector} names ${String(named.length)} Kuna entries that lie in Mach-O data regions (${named.map(({ address }) => address).join(", ")}); they are data, not procedures.`
|
||||
: `${selector} names ${String(matches.length)} Kuna entries (${matches.map(({ address }) => address).join(", ")}); select one by address.`,
|
||||
@@ -1172,7 +1234,11 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
);
|
||||
if (!text.ok) return text;
|
||||
return ok({
|
||||
...text.value,
|
||||
result: text.value.result,
|
||||
limitations: text.value.limitations,
|
||||
...(text.value.files === undefined
|
||||
? {}
|
||||
: { files: text.value.files }),
|
||||
source: { kind: "project-export", project: reader.project },
|
||||
});
|
||||
} catch (cause: unknown) {
|
||||
@@ -1315,10 +1381,13 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
address: string,
|
||||
installation: KunaInstallation,
|
||||
signal: AbortSignal,
|
||||
independent = false,
|
||||
): Promise<SingleFunctionOutcome> {
|
||||
const prepared = await this.#commands.prepare();
|
||||
if (!prepared.ok) return prepared;
|
||||
const admitted = await this.#admitSingleFunction(signal);
|
||||
const admitted = independent
|
||||
? await this.#runSlots.acquire(signal)
|
||||
: await this.#admitSingleFunction(signal);
|
||||
if (admitted === EXPORT_READY) return admitted;
|
||||
if (admitted === undefined)
|
||||
return err(
|
||||
@@ -1461,7 +1530,7 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
pseudocode:
|
||||
text === null
|
||||
? unavailable(
|
||||
pseudocode.value.limitations.at(-1) ??
|
||||
pseudocode.value.unavailableReason ??
|
||||
"Kuna did not supply pseudocode.",
|
||||
)
|
||||
: { available: true },
|
||||
|
||||
+13
-1
@@ -232,6 +232,14 @@ interface DoctorCatalogExpectation {
|
||||
readonly expectedPromptNames: readonly string[];
|
||||
}
|
||||
|
||||
const listPromptsWhenAdvertised = (
|
||||
client: Client,
|
||||
request: ReturnType<typeof requestOptions>,
|
||||
) =>
|
||||
client.getServerCapabilities()?.prompts
|
||||
? client.listPrompts(undefined, request)
|
||||
: Promise.resolve({ prompts: [] });
|
||||
|
||||
const inspectProductionMcpSession = async (
|
||||
client: Client,
|
||||
deadline: number,
|
||||
@@ -240,9 +248,13 @@ const inspectProductionMcpSession = async (
|
||||
budget: McpPhaseBudget,
|
||||
) => {
|
||||
const request = requestOptions(deadline, signal);
|
||||
// The SDK logs to stdout when listPrompts is called without the advertised
|
||||
// capability, which corrupts the doctor's structured output. An absent
|
||||
// capability is an empty prompt inventory for this diagnostic.
|
||||
const promptsRequest = listPromptsWhenAdvertised(client, request);
|
||||
const [tools, prompts] = await Promise.all([
|
||||
client.listTools(undefined, request),
|
||||
client.listPrompts(undefined, request),
|
||||
promptsRequest,
|
||||
]);
|
||||
const sessionTool = tools.tools.find(({ name }) => name === "binary_session");
|
||||
// A scope without the session family cannot serve request flow at all;
|
||||
|
||||
@@ -69,8 +69,12 @@ import { PWNTOOLS_LIMITS } from "./PwntoolsRelease.js";
|
||||
import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsProviderIdentity.js";
|
||||
import { pwntoolsInputBytes } from "./PwntoolsInputLimit.js";
|
||||
|
||||
const HOST_REQUIREMENT =
|
||||
"The initial real-verified pwntools artifact profile supports Linux x64 only.";
|
||||
/** Names the detected host so a host refusal is not read as a target refusal. */
|
||||
export const pwntoolsHostRequirement = (
|
||||
platform: NodeJS.Platform,
|
||||
arch: string,
|
||||
): string =>
|
||||
`The real-verified pwntools artifact profile runs only on a Linux x64 host; this host is ${platform}/${arch}. The selected target was not read.`;
|
||||
|
||||
/** The pwntools profile runs only on Linux x64; discovery and execution share this check. */
|
||||
export const pwntoolsHostSupported = (
|
||||
@@ -290,13 +294,18 @@ export class PwntoolsDecoder<
|
||||
throw new AnalysisCancelledError(OPERATION, {
|
||||
cause: options.signal.reason,
|
||||
});
|
||||
if (!pwntoolsHostSupported(process.platform, process.arch))
|
||||
if (!pwntoolsHostSupported(process.platform, process.arch)) {
|
||||
const requirement = pwntoolsHostRequirement(
|
||||
process.platform,
|
||||
process.arch,
|
||||
);
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
this.identity.id,
|
||||
OPERATION,
|
||||
HOST_REQUIREMENT,
|
||||
{ userMessage: HOST_REQUIREMENT },
|
||||
requirement,
|
||||
{ userMessage: requirement },
|
||||
);
|
||||
}
|
||||
if (!isAbsolute(selectedPath))
|
||||
throw pwntoolsUnavailable(
|
||||
"Set REA_PWNTOOLS_PYTHON to an absolute caller-supplied Python executable with pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2; REA never installs Python or packages.",
|
||||
|
||||
@@ -2,6 +2,10 @@ import { expect, it } from "vitest";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import { OwnedCommandFailure } from "../../process/OwnedCommand.js";
|
||||
import {
|
||||
pwntoolsHostRequirement,
|
||||
pwntoolsHostSupported,
|
||||
} from "./PwntoolsDecoder.js";
|
||||
import { pwntoolsDecoderFailure } from "./PwntoolsFailures.js";
|
||||
|
||||
it.each([undefined, null, false, 0, "stop", { stop: true }, new Error("stop")])(
|
||||
@@ -149,3 +153,11 @@ it("retains legacy owner causes without guessing cancellation from error text",
|
||||
"ProviderAdapterError",
|
||||
);
|
||||
});
|
||||
|
||||
it("names the refused host instead of implying the target was refused", () => {
|
||||
expect(pwntoolsHostSupported("linux", "arm64")).toBe(false);
|
||||
const message = pwntoolsHostRequirement("linux", "arm64");
|
||||
expect(message).toContain("Linux x64 host");
|
||||
expect(message).toContain("this host is linux/arm64");
|
||||
expect(message).toContain("The selected target was not read.");
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type AnalysisErrorProjection,
|
||||
} from "../domain/analysisErrorProjection.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { NoBinaryOpenError } from "../domain/configurationErrors.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import type { Result } from "../domain/result.js";
|
||||
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
|
||||
@@ -104,12 +105,22 @@ export class ToolResultDelivery {
|
||||
selected?: JsonValue,
|
||||
viewSource?: JsonValue,
|
||||
) => Readonly<Record<string, JsonValue>> = () => ({}),
|
||||
private readonly noTargetRecovery?: string,
|
||||
) {
|
||||
this.maximumResponseBytes = maximumResponseBytes;
|
||||
this.resultBudgetBytes = maximumResponseBytes - 1024;
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
/** Bind scope-specific recovery without mutating a shared delivery policy. */
|
||||
withNoTargetRecovery(recovery: string): ToolResultDelivery {
|
||||
return new ToolResultDelivery(
|
||||
this.maximumResponseBytes,
|
||||
this.nativeContinuation,
|
||||
recovery,
|
||||
);
|
||||
}
|
||||
|
||||
/** Serialize a plain application result or its actionable error. */
|
||||
toCallToolResult(
|
||||
result: Result<JsonValue, AnalysisError>,
|
||||
@@ -184,10 +195,20 @@ export class ToolResultDelivery {
|
||||
error: AnalysisError,
|
||||
targetContent: boolean,
|
||||
): CallToolResult {
|
||||
return this.projectedErrorResult(
|
||||
projectAnalysisError(error),
|
||||
targetContent,
|
||||
);
|
||||
const projected = projectAnalysisError(error);
|
||||
if (
|
||||
error instanceof NoBinaryOpenError &&
|
||||
this.noTargetRecovery !== undefined
|
||||
)
|
||||
return this.projectedErrorResult(
|
||||
{
|
||||
...projected,
|
||||
message: this.noTargetRecovery,
|
||||
remediation: { action: this.noTargetRecovery },
|
||||
},
|
||||
targetContent,
|
||||
);
|
||||
return this.projectedErrorResult(projected, targetContent);
|
||||
}
|
||||
|
||||
/** Retain valid partial Evidence and reference it only after recording succeeds. */
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
type CapabilityServiceOptions,
|
||||
} from "../composition/capabilityServices.js";
|
||||
import { registerToolFamilies } from "../contracts/toolFamilyIds.js";
|
||||
import { SCOPED_NO_TARGET_RECOVERY } from "./recoveryInstructions.js";
|
||||
import { AnalysisSettings } from "../application/AnalysisSettings.js";
|
||||
import { parseMcpInputBudget } from "../config/mcpInputBudget.js";
|
||||
import { registerAnalysisSettingsTools } from "./registerAnalysisSettingsTools.js";
|
||||
@@ -174,7 +175,13 @@ export const createServer = (
|
||||
options.providerEnvironment,
|
||||
environment,
|
||||
);
|
||||
const delivery = selectToolResultDelivery(environment, options.delivery);
|
||||
const baseDelivery = selectToolResultDelivery(environment, options.delivery);
|
||||
const delivery =
|
||||
session !== undefined &&
|
||||
advertisedToolFamilies !== undefined &&
|
||||
!advertisedToolFamilies.has("session")
|
||||
? baseDelivery.withNoTargetRecovery(SCOPED_NO_TARGET_RECOVERY)
|
||||
: baseDelivery;
|
||||
const capabilities = createCapabilityServices({
|
||||
environment,
|
||||
providerEnvironment,
|
||||
|
||||
@@ -16,3 +16,7 @@ export const recoveryInstructions = (
|
||||
runtimeSettingsAvailable: boolean,
|
||||
): string =>
|
||||
`${RECOVERY_START}${runtimeSettingsAvailable ? RUNTIME_SETTINGS_RECOVERY : LAUNCH_SETTINGS_RECOVERY}${EXPLICIT_ARGUMENT_RECOVERY}${RECOVERY_END}`;
|
||||
|
||||
/** Recovery when a scoped session cannot advertise the target lifecycle. */
|
||||
export const SCOPED_NO_TARGET_RECOVERY =
|
||||
"No target is open, and this MCP connection does not advertise open_binary. Preserve needed outputs before restarting; retained in-memory Evidence and Unknown history will not carry over. Add session to REA_MCP_TOOL_FAMILIES in the environment that starts REA and restart the server; then call open_binary with the chosen local path.";
|
||||
@@ -162,3 +162,64 @@ cliTest(
|
||||
},
|
||||
120_000,
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"traces a retained empty literal endpoint by its preserved value",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-empty-endpoint-seed-");
|
||||
await writeFile(
|
||||
join(root, "main.js"),
|
||||
'export const load = async () => (await fetch("")).json();\n',
|
||||
);
|
||||
const analyzed = await cli.run({
|
||||
arguments: [
|
||||
"analyze-javascript-application",
|
||||
root,
|
||||
"--artifact-format",
|
||||
"directory",
|
||||
"--json",
|
||||
],
|
||||
timeoutMs: 60_000,
|
||||
});
|
||||
expect(analyzed.exitCode, analyzed.stderr).toBe(0);
|
||||
const application = toolContract(
|
||||
"analyze_javascript_application",
|
||||
).outputSchema.parse(analyzed.json);
|
||||
const graph = javascriptApplicationAnalysisResultSchema.parse(
|
||||
application.normalized_result,
|
||||
).semantic_graph;
|
||||
// Analysis keeps the complete literal endpoint, here the empty string.
|
||||
const retained = new Set(
|
||||
graph.nodes
|
||||
.filter(
|
||||
(node) =>
|
||||
(node.kind === "request" || node.kind === "response") &&
|
||||
node.properties.endpoint === "",
|
||||
)
|
||||
.map((node) => node.node_id),
|
||||
);
|
||||
expect(retained.size).toBeGreaterThan(0);
|
||||
const request = join(root, "endpoint-query.json");
|
||||
await writeFile(
|
||||
request,
|
||||
JSON.stringify({
|
||||
application,
|
||||
query: {
|
||||
seed: { kind: "endpoint", value: "" },
|
||||
direction: "forward-influence",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const traced = await cli.run({
|
||||
arguments: ["trace-javascript-semantics", request, "--json"],
|
||||
timeoutMs: 60_000,
|
||||
});
|
||||
expect(traced.exitCode, traced.stdout).toBe(0);
|
||||
const trace = javaScriptSemanticTraceResultSchema.parse(
|
||||
toolContract("trace_javascript_semantics").outputSchema.parse(traced.json)
|
||||
.normalized_result,
|
||||
);
|
||||
expect(new Set(trace.seed_node_ids)).toEqual(retained);
|
||||
},
|
||||
120_000,
|
||||
);
|
||||
@@ -0,0 +1,95 @@
|
||||
import { access, mkdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
cliTest.for([
|
||||
{ marker: true, skill: true },
|
||||
{ marker: true, skill: false },
|
||||
{ marker: false, skill: true },
|
||||
])(
|
||||
"automatic setup excludes manual hosted connectors (marker: $marker, skill: $skill)",
|
||||
async ({ marker, skill }, { cli }) => {
|
||||
const home = await createTestTempDirectory("rea-automatic-clients-");
|
||||
if (marker) await mkdir(join(home, ".grokbot"));
|
||||
const run = () =>
|
||||
cli.run({
|
||||
arguments: [
|
||||
"setup",
|
||||
"--all-detected",
|
||||
"--yes",
|
||||
"--json",
|
||||
...(skill ? [] : ["--skill=false"]),
|
||||
],
|
||||
cwd: home,
|
||||
environment: { HOME: home, USERPROFILE: home, TMPDIR: home },
|
||||
});
|
||||
const first = await run();
|
||||
expect(first.exitCode, first.stdout).toBe(0);
|
||||
expect(first.json).toMatchObject({
|
||||
status: "ready",
|
||||
appliedActions: skill ? ["installed_skill"] : [],
|
||||
clients: {},
|
||||
doctor: { healthy: true, scope: { clients: [], skill } },
|
||||
});
|
||||
if (marker)
|
||||
expect(first.json).toMatchObject({
|
||||
clientStates: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
client: expect.objectContaining({ name: "grok_bot" }),
|
||||
detected: true,
|
||||
}),
|
||||
]),
|
||||
});
|
||||
const repeated = await run();
|
||||
expect(repeated.exitCode, repeated.stdout).toBe(0);
|
||||
expect(repeated.json).toMatchObject({
|
||||
status: "ready",
|
||||
plannedActions: [],
|
||||
appliedActions: [],
|
||||
doctor: { healthy: true, scope: { clients: [], skill } },
|
||||
});
|
||||
await expect(
|
||||
access(join(home, ".grokbot", "mcp.json")),
|
||||
).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cliTest.for([true, false])(
|
||||
"explicit Grok Bot setup retains manual instructions (all detected: %s)",
|
||||
async (allDetected, { cli }) => {
|
||||
const home = await createTestTempDirectory("rea-explicit-manual-client-");
|
||||
await mkdir(join(home, ".grokbot"));
|
||||
const result = await cli.run({
|
||||
arguments: [
|
||||
"setup",
|
||||
"--client",
|
||||
"grok_bot",
|
||||
"--skill=false",
|
||||
"--yes",
|
||||
"--json",
|
||||
...(allDetected ? ["--all-detected"] : []),
|
||||
],
|
||||
cwd: home,
|
||||
environment: { HOME: home, USERPROFILE: home, TMPDIR: home },
|
||||
});
|
||||
expect(result.exitCode, result.stdout).toBe(1);
|
||||
expect(result.json).toMatchObject({
|
||||
status: "needs_human",
|
||||
plannedActions: [],
|
||||
appliedActions: [],
|
||||
remediation: expect.stringContaining("hosted computer"),
|
||||
doctor: { scope: { clients: ["grok_bot"] } },
|
||||
});
|
||||
await expect(
|
||||
access(join(home, ".grokbot", "mcp.json")),
|
||||
).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -88,6 +88,7 @@ cliTest(
|
||||
"rea-explicit-detected-clients-",
|
||||
);
|
||||
await mkdir(join(home, ".codex"));
|
||||
await mkdir(join(home, ".grokbot"));
|
||||
const run = (selection: readonly string[], mode: string) =>
|
||||
cli.run({
|
||||
arguments: ["setup", ...selection, "--skill=false", "--json", mode],
|
||||
|
||||
@@ -677,6 +677,11 @@ it.runIf(unsupportedHost)(
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected.message).toContain("Linux x64");
|
||||
expect(projected.remediation.action).toContain("Linux x64");
|
||||
// The refusal names this host, not the target, which was never read.
|
||||
expect(projected.message).toContain(
|
||||
`this host is ${process.platform}/${process.arch}`,
|
||||
);
|
||||
expect(projected.message).toContain("The selected target was not read.");
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user