Compare commits

..
Author SHA1 Message Date
github-actions[bot]andGitHub 0f337cb398 Saving release notes 2026-10-13 11:39:49 +00:00
229 changed files with 1800 additions and 12147 deletions
-112
View File
@@ -1,112 +0,0 @@
---
name: rea-ci-tests-audit
description: Audit REA CI and test suites for correctness, fixture drift, false passes, and measured performance; use for suite or workflow investigations and efficiency changes, rather than ordinary feature testing or a single test failure.
---
# REA CI and tests audit
Establish which checks still catch real regressions and which costs can be
removed without losing that protection. The outcome may be a verified fix,
an evidence-backed recommendation, or an explicit decision to stop.
Follow the checkout's `AGENTS.md`, [testing guide](../../../docs/testing.md),
and [contribution rules](../../../CONTRIBUTING.md). Use their current commands
and pinned toolchain, rather than commands remembered from an earlier audit.
Preserve the requested scope: investigation alone does not authorize edits;
this skill does not grant permission to publish issues, push, or merge. Existing
user authorization still applies. Keep audit notes and raw results outside the
tracked tree, normally in a task-specific `/tmp` directory.
## Establish the executed baseline
Fetch when the user asks for latest main. Pin the baseline SHA and inspected
workflow revision; use an isolated worktree when changing or comparing code.
Do not modify another task's checkout. Record dirty changes and the exact
command, toolchain, host, selected tests, instrumentation, and prerequisites.
An old run is evidence for its executed revision, even if main advances.
Trace workflow triggers, changed-path selection, job dependencies, reusable
actions, package scripts, test discovery, artifact producers, and report gates
through to the commands that actually execute. Include scripts, generated data,
runtime registration, and subprocess boundaries missing from import graphs.
Check positive and negative selection paths: required files must be discovered
once per intended test configuration; deliberate platform/configuration repeats
are separate coverage. A missing explicit test or missing required report must
not turn green. Optional lanes may skip; preserve their stated prerequisites.
Use existing timing and failure artifacts to select suspicious families. Run
relevant checks to establish a baseline; reserve a full local suite for broad
CI diagnosis or uncertainty that focused checks cannot resolve. Do not rebuild
or rerun an unchanged complete suite after every documentation edit.
## Prove the failure or protection
For each candidate, state the expected observable behavior and its owner,
then construct the smallest reproduction against production behavior. Follow
the producer and affected consumers before deciding where to fix it.
- For drift, compare fixtures with current admission requirements, protocol
replies, named contracts, tool versions, and actual public entrypoints. A
plausible-looking mock can fail before the behavior its test claims to cover.
Search the whole affected fixture family, including other version strings.
Preserve intentionally malformed fixtures and test their intended rejection.
- For a suspicious green test, perturb the relevant behavior in an isolated
copy or inject a controlled failure. Verify that the existing assertion or
gate detects it. A test may merely repeat its mock, import abandoned code,
or hide a subprocess/checker failure. Restore mutations after the experiment.
- For shared setup or pruning, map distinct assertions to their retained
scenarios. Preserve malformed inputs, cancellation, cleanup failure,
permissions, capacity, identity, and meaningful concurrency interactions.
A faster run that skips a case or weakens its oracle is not an improvement.
- For provider coverage, separate fake-producer, boundary, and actual-engine
evidence. Inspect named skips and prerequisites; fixtures do not establish
real Ghidra, LLDB, browser, or other provider behavior.
Classify findings as reproduced defects, supported costs, hypotheses, or
unavailable evidence. Record the baseline command, observed output/exit status,
expected result, affected scope, and competing explanation. A failure on main
may be pre-existing; that does not by itself make it irrelevant to a CI audit.
Attribute PR failures using the executed revision and a clean baseline, not
just their resemblance to a previous failure. Retry only to test a specific
transient explanation; retain the original failure and stop when repetition
adds no evidence. A passing retry does not establish a root cause or a fix.
## Assess efficiency without losing the gate
Read [measurement and CI checks](references/measurement-and-ci.md) when
investigating slowness, caching, runner waits, shared artifacts, or log noise.
Compare equivalent completed work before claiming a speedup. Distinguish
setup/build time, test execution, coverage instrumentation, dependency waits,
and runner scheduling; state which costs remain unmeasured.
Prefer removing proven duplicate preparation, reusing immutable setup, and
reducing repeated subprocess work before shrinking correctness coverage.
Preserve independent sessions where lifecycle or concurrency is the oracle.
Keep live tests fresh; a cached previous pass cannot validate current host,
installed provider contents, permissions, or external state.
## Fix, verify, and hand off within scope
If implementation is authorized, make the smallest structural change at the
owning boundary and add a regression that fails on the baseline for the right
reason. Retest affected consumers and meaningful negative paths; verify cache
invalidation or discovery when those mechanisms change. Read the installed
Turbo documentation before changing its configuration or commands.
Run the repository's applicable checks and inspect complete current-head CI
results before an authorized merge. Preserve required coverage and discovery
gates. Do not suppress a failure, add blanket retries, raise timeouts, or delete
distinct assertions solely to make CI green. Changes to these controls need
their own demonstrated reason and retained failure detection.
Report each confirmed issue with exact revision, reproduction, consequence,
proposed correction, and validation limits. Report performance with workload
and measurements, not just a percentage. Use English issue/PR records when
requested; follow the user's assignment, commit identity, review, and merge
preferences without assuming them for other users. Open ready-for-review PRs
only when that lifecycle is authorized. Resolve necessary review findings and
check final main against the tested tree after merging.
Stop when the authorized work is verified, remaining benefits are unproven,
or further changes would trade correctness for speculative speed. List genuine
uncertainties without turning them into additional optimization rounds.
@@ -1,4 +0,0 @@
interface:
display_name: "REA CI and tests audit"
short_description: "Audit CI/test correctness and measured performance"
default_prompt: "Use $rea-ci-tests-audit to investigate CI and test correctness, drift, and performance before proposing changes."
@@ -1,65 +0,0 @@
# Measurement and CI checks
## Equivalent completed work
Record the source/workflow SHA, command, test identities and counts, skips,
assertions or output contract, exit status, coverage scope and thresholds,
toolchain/provider versions, host resources, and prepared artifacts. Inspect
both the child result and its report: a profiling wrapper may exit successfully
after the measured command failed. Focused tests can pass while global coverage
thresholds fail; describe both rather than calling the command green.
Measure wall time, user/system CPU, and peak process-family memory for relevant
completed tasks. Parent-only RSS misses workers and their children. On Linux,
summed RSS can double-count shared pages; report RSS/PSS semantics, sampling
interval, missing samples, and observed cleanup limits. Use equivalent host
tools elsewhere. State unknown costs, including agent tokens; fewer log bytes
alone prove neither fewer tokens nor faster tests.
Separate cold execution, warm build/file caches, and warm runtime caches.
Use repeated or alternating runs when variance affects the decision, with
enough observations to distinguish the proposed benefit from noise. Keep
workload, instrumentation, and host conditions comparable; record interference.
A run with fewer tests, early rejection, lost coverage, or a different runner
does not prove suite acceleration. CI-wide claims need end-to-end measurements,
not an extrapolation from one improved family.
## Workflow, caches, and shared artifacts
Trace each reused result from producer to consumers and identify its lifetime.
Check key/input coverage against the actual file inventory and result-affecting
configuration, environment, generated data, tool versions, and host state.
Exercise an affected input change to prove invalidation; exercise an irrelevant
change to support reuse where useful. Cache deterministic preparation and
static analysis only when their inputs fully describe the result.
Verify downloaded artifacts match the expected revision, platform, runtime,
and schema. Check restore misses, failed/partial producers, stale reports,
removed output files, and consumers after producer failures. Shared writers
need output ownership or locks; tests need isolated state, not an unnecessary
global lock. Worker counts do not bound children spawned within each test.
Preserve intentionally concurrent scenarios while bounding unrelated batches.
Check matrix expansion and all job pages before saying CI passed. Inspect
required gates after failed, cancelled, and skipped dependencies. `always()`
alone does not preserve failures; inspect the aggregation logic. Test/report
uploads should retain available diagnostics after failure, while absent or
malformed required results must fail closed. Do not demand an optional report
from a lane intentionally excluded by the selector.
## Runner waits and console noise
Use per-job and per-step timestamps where available. Workflow creation to job
start includes dependencies and other delays; it is not necessarily runner
queue time. Identify when the job became eligible before attributing waits to
runner capacity. If eligibility/queue timestamps are unavailable, report the
combined delay and unknown split. Compare equivalent lanes and runner types
across enough runs; a single busy runner is not a workflow regression.
For console reductions, retain checker exit codes, actionable errors, useful
summary counts, and complete machine-readable artifacts. Verify authored code
remains in the actual checker inventory when excluding generated outputs.
Introduce an authored error as a control; it must still fail. An ignore pattern
can hide real source as well as noise. Show that cached reports are restored,
stale successful reports cannot mask current failures, and report parse errors
remain failures. Measure readability or byte reduction separately from runtime.
@@ -46,7 +46,7 @@ rea setup --client codex --dry-run --json
Use the current supported client's ID: `claude_code`, `claude_desktop`, `codex`,
`cursor`, `gemini_cli`, `windsurf`, `devin`, `opencode`, `antigravity`,
`copilot_cli`, `commandcode`, `qwen_code`, `vscode`, `trae`, `grok_build`, `omp`, `pi`,
`hermes`, `qoder`, `droid`, `zcode`, `dsh`, or `grok_bot`. If unknown, inspect `doctor --json` before choosing a
`hermes`, `qoder`, `zcode`, `dsh`, or `grok_bot`. If unknown, inspect `doctor --json` before choosing a
scope. `grok_bot` uses a chat registration step rather than local `mcp.json`.
For unsupported clients, use version-pinned manual stdio registration or CLI.
@@ -224,13 +224,10 @@ context. PSP/stack/device state remains unmodeled. See [DOS guide](https://githu
For other headerless images (embedded firmware without ELF/Mach-O/PE/MZ
headers), open with `format: "raw-binary"`, `architecture` (`x86`, `x86_64`,
`arm`, `arm64`, or `6502`), and `entry_point` (`0x` hex). File offset 0 is mapped at
`arm`, or `arm64`), and `entry_point` (`0x` hex). File offset 0 is mapped at
that address. Hopper and IDA refuse this interpretation; Ghidra uses
`BinaryLoader` with a little-endian generic language. Big-endian, Thumb-only,
and Cortex-M variants are unsupported. `6502` selects NMOS 6502; the entry and
complete file mapping must fit within `0x0000..0xffff`. 65C02, 65816, cartridge
containers, bank switching, and mapper semantics are unsupported. Windows
Ghidra P0 admits raw images only for `6502`. See [raw binary guide](https://github.com/morluto/rea/blob/main/docs/raw-binary.md).
and Cortex-M variants are unsupported. See [raw binary guide](https://github.com/morluto/rea/blob/main/docs/raw-binary.md).
### Function annotations in Ghidra
-1
View File
@@ -597,7 +597,6 @@ jobs:
tests/boundary/filesystem/setupUnavailableProvider.test.ts
tests/boundary/filesystem/windowsNpxRegistration.test.ts
tests/acceptance/setup/homeDirectoryFallback.test.ts
tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts
tests/boundary/filesystem/webScriptManifestPaths.test.ts
tests/boundary/process/ProcessEvidence.host.test.ts
tests/acceptance/webModuleCli.test.ts
-19
View File
@@ -142,21 +142,6 @@ caches deterministic builds and static checks across Git worktrees. After a pack
Repository invariants live in [AGENTS.md](AGENTS.md). Keep provider-specific
protocols in adapters and shared CLI/MCP workflows in the application layer.
### Windows source checkouts
The published npm package includes the Windows x64 native addon and its
manifest. A source checkout does not: `native/windows/build/` is ignored by
Git, and neither `npm ci` nor `npm run build:cached` (or `npm run build`)
produces `rea-windows-x64.node` or `manifest.json` in that directory.
Windows workflows and tests that require native controls need these artifacts;
without them, those controls are unavailable even after a successful runtime build.
Follow the [Windows native build guide](native/windows/README.md#build) before
running those workflows from source. The artifact build requires Linux,
MinGW-w64, and `tar`; the build script does not run on Windows. See that guide
for the build and artifact-verification commands and the separate real Windows
verification prerequisites. Keep the generated addon and manifest uncommitted.
## Documentation website
The VitePress site uses the Markdown files in `docs/`. Run `npm run docs:dev`
@@ -202,10 +187,6 @@ CI diagnosis. Packaging/setup/distribution changes additionally require
`npm run verify:package` and `npm pack --dry-run`; provider changes require the
matching real-provider lane. State which host/provider workflows ran.
For CI/test correctness, drift, or performance investigations, use the
[`rea-ci-tests-audit` contributor skill](.agents/skills/rea-ci-tests-audit/SKILL.md).
It guides reproduction and comparable measurements before changing checks.
`check:pr` reuses build and static-check outputs while executing the test suite
fresh, including the fast Python producer checks. `docs:check` also runs the
onboarding example consistency suite for README-only changes.
+2 -387
View File
@@ -13,8 +13,6 @@ import java.net.UnixDomainSocketAddress;
import java.nio.channels.Channels;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
@@ -24,9 +22,7 @@ import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.MessageDigest;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.IdentityHashMap;
@@ -86,7 +82,6 @@ import ghidra.program.model.address.SegmentedAddress;
import ghidra.program.model.reloc.Relocation;
import java.io.ByteArrayOutputStream;
import ghidra.program.model.symbol.Namespace;
import ghidra.program.model.symbol.FlowType;
import ghidra.program.model.symbol.RefType;
import ghidra.program.model.symbol.Reference;
import ghidra.program.model.symbol.ReferenceIterator;
@@ -105,9 +100,6 @@ import ghidra.program.model.pcode.PcodeBlock;
import ghidra.program.model.pcode.PcodeBlockBasic;
import ghidra.program.model.pcode.Varnode;
import ghidra.program.model.pcode.VarnodeAST;
import ghidra.program.util.ContextEvaluatorAdapter;
import ghidra.program.util.SymbolicPropogator;
import ghidra.program.util.VarnodeContext;
public final class ReaGhidraBridge extends HeadlessScript {
private static final int MAX_HIGH_PCODE_OPS = 3000;
@@ -812,7 +804,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
return functionObservation(result, boundaryLimitations);
}
private JsonObject procedureCalls(JsonObject params, boolean callers) throws Exception {
private JsonArray procedureCalls(JsonObject params, boolean callers) throws Exception {
requireKeys(params, Set.of("document", "procedure"));
requireDocument(params);
Function function = resolveProcedure(requireString(params, "procedure"));
@@ -825,12 +817,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
for (Function item : ordered) {
result.add(canonicalAddress(item.getEntryPoint()));
}
return functionObservation(
result,
noReturnCallLimitations(
currentProgram.getListing().getInstructions(function.getBody(), true)
)
);
return result;
}
private JsonObject procedureInfo(JsonObject params) throws Exception {
@@ -1332,13 +1319,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
result.add("outgoing_references", outgoing);
result.add("referenced_strings", referencedStrings);
result.add("referenced_names", referencedNames);
boolean swift = swiftProgram();
boolean aarch64 = currentProgram.getLanguageID().getIdAsString().startsWith("AARCH64:");
SwiftStringLiteralDecode swiftLiterals = swift && aarch64 ? swiftStringLiterals(function) : null;
if (swiftLiterals != null) {
result.add("swift_string_literals", swiftLiterals.literals());
result.add("swift_string_literal_omissions", swiftLiterals.omissions());
}
result.add("basic_blocks", blocks);
result.add(
"native_api",
@@ -1358,20 +1338,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
limitations.add(
"Synthetic Ghidra entry-point references without actionable memory sources are omitted."
);
if (swift && aarch64) {
limitations.add(
"Swift String literals are decoded from constant register pairs at calls, returns and paired stores; literals stored one word at a time, chosen by conditional selects, passed through unresolved jumps, or built at runtime are not reported."
);
if (swiftLiterals.omissions().size() > 0) {
limitations.add(
"Swift String literal records listed in swift_string_literal_omissions could not be retained or decoded within this dossier's remaining heap budget; later symbolic flows remain unexamined."
);
}
} else if (swift) {
limitations.add(
"Swift String literal decoding is implemented for AArch64 register pairs only; no Swift literals were decoded for this architecture."
);
}
List<String> boundaryLimitations = noReturnCallLimitations(scan.instructions);
for (String limitation : boundaryLimitations) {
limitations.add(limitation);
@@ -3425,349 +3391,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
return result;
}
private boolean swiftProgram() {
for (MemoryBlock block : currentProgram.getMemory().getBlocks()) {
String name = block.getName();
if (name.startsWith("__swift5_") || name.startsWith("swift5_")) return true;
}
return false;
}
/**
* Swift passes a String as two words: small literals carry their bytes in the words and
* immortal large literals carry their count and a biased pointer to the bytes. Ghidra keeps
* neither as a data reference, so decode constant pairs where Swift consumes them: argument
* and return registers at calls, tail calls and returns, and both registers of one STP.
* The canonical empty string is a small literal with a zero count.
*/
private SwiftStringLiteralDecode swiftStringLiterals(Function function) throws Exception {
Register[] registers = new Register[31];
for (int index = 0; index < registers.length; index++) {
registers[index] = currentProgram.getRegister("x" + index);
}
TreeMap<String, JsonObject> observed = new TreeMap<>();
TreeMap<String, JsonObject> omitted = new TreeMap<>();
SwiftLiteralBudget budget = new SwiftLiteralBudget();
Map<String, Boolean> selectedValues = new HashMap<>();
SymbolicPropogator propagator = new SymbolicPropogator(currentProgram, false);
propagator.flowConstants(function.getEntryPoint(), function.getBody(), new ContextEvaluatorAdapter() {
@Override
public boolean evaluateContextBefore(VarnodeContext context, Instruction instruction) {
// Returning true ends only the current flow, so every queued flow checks the shared stop.
if (budget.stopped()) return true;
for (Register[] pair : swiftStringPairs(function, instruction, registers)) {
Long word = registerConstant(context, pair[0]);
Long object = registerConstant(context, pair[1]);
if (word == null || object == null) continue;
SwiftLiteralLayout layout = swiftLiteralLayout(word, object);
if (layout == null) continue;
// A selected word can combine with a path-dependent ordinary move. Cache the
// provenance question per site/register, charging retained cache state to the
// same dossier budget before allocating it.
boolean selected = false;
boolean provenanceOverCapacity = false;
for (Register register : pair) {
String provenanceKey = canonicalAddress(instruction.getAddress()) + "\u0000" +
register.getBaseRegister().getName();
Boolean value = selectedValues.get(provenanceKey);
if (value == null) {
if (!budget.takeState()) {
provenanceOverCapacity = true;
break;
}
value = selectedValue(function, instruction, register);
selectedValues.put(provenanceKey, value);
}
if (value) {
selected = true;
break;
}
}
if (selected) continue;
// Forks can reach one site with different constants, so the words are part of the key.
String key = canonicalAddress(instruction.getAddress()) + "\u0000" + pair[0].getName() +
"\u0000" + pair[1].getName() + "\u0000" + Long.toUnsignedString(word, 16) +
"\u0000" + Long.toUnsignedString(object, 16);
if (observed.containsKey(key) || omitted.containsKey(key)) continue;
// Every record draws on the budget; the first one that does not fit is kept as an
// omission and decoding stops, so later sites are never examined.
String value = layout.smallValue();
boolean overCapacity = provenanceOverCapacity || !budget.take(layout.byteLength());
if (!overCapacity && value == null) {
try {
value = swiftLargeLiteralText(layout);
} catch (OutOfMemoryError exhausted) {
overCapacity = true;
}
}
if (value == null && !overCapacity) continue;
JsonObject item = overCapacity
? swiftStringOmission(layout, word, object)
: swiftStringItem(value, layout, word, object);
item.addProperty("source_address", canonicalAddress(instruction.getAddress()));
JsonArray pairRegisters = new JsonArray();
pairRegisters.add(pair[0].getName());
pairRegisters.add(pair[1].getName());
item.add("registers", pairRegisters);
(overCapacity ? omitted : observed).put(key, item);
if (overCapacity) {
budget.stop();
return true;
}
}
return false;
}
}, false, monitor);
JsonArray literals = new JsonArray();
observed.values().forEach(literals::add);
JsonArray omissions = new JsonArray();
omitted.values().forEach(omissions::add);
return new SwiftStringLiteralDecode(literals, omissions);
}
private static List<Register[]> swiftStringPairs(
Function function,
Instruction instruction,
Register[] registers
) {
List<Register[]> pairs = new ArrayList<>();
if (instruction.getMnemonicString().equals("stp")) {
Register first = instruction.getRegister(0);
Register second = instruction.getRegister(1);
if (first != null && second != null && first.getBitLength() == 64 && second.getBitLength() == 64) {
pairs.add(new Register[] {first, second});
}
return pairs;
}
FlowType flow = instruction.getFlowType();
boolean returns = false;
for (PcodeOp operation : instruction.getPcode()) {
if (operation.getOpcode() == PcodeOp.RETURN) returns = true;
}
// A jump is a proven tail call only when it has resolved targets and all leave the
// body. A targetless computed jump can still dispatch inside the function.
Address[] targets = instruction.getFlows();
boolean tailCall = flow.isJump() && !flow.isConditional() && targets.length > 0 &&
Arrays.stream(targets).allMatch(target -> !function.getBody().contains(target));
if (!flow.isCall() && !returns && !tailCall) return pairs;
// A String occupies two consecutive registers among x0-x7, or x0 and x1 when returned.
int lastFirstRegister = returns ? 0 : 6;
for (int index = 0; index <= lastFirstRegister; index++) {
pairs.add(new Register[] {registers[index], registers[index + 1]});
}
return pairs;
}
private static Long registerConstant(VarnodeContext context, Register register) {
Varnode value = context.getRegisterVarnodeValue(register);
return value != null && context.isConstant(value) ? value.getOffset() : null;
}
// Search every predecessor path for the reaching definitions, following register copies,
// since a select copied into an argument register or defined before a branch forks just as
// independently as one written there directly.
private boolean selectedValue(Function function, Instruction use, Register register) {
ArrayDeque<Instruction> instructions = new ArrayDeque<>();
ArrayDeque<Register> targets = new ArrayDeque<>();
Set<String> visited = new HashSet<>();
for (Instruction predecessor : predecessors(function, use)) {
instructions.push(predecessor);
targets.push(register.getBaseRegister());
}
while (!instructions.isEmpty()) {
Instruction instruction = instructions.pop();
Register target = targets.pop();
if (!visited.add(canonicalAddress(instruction.getAddress()) + "\u0000" + target.getName())) continue;
boolean defines = false;
for (Object result : instruction.getResultObjects()) {
if (result instanceof Register written && written.getBaseRegister().equals(target)) defines = true;
}
Register next = target;
if (defines) {
PcodeOp[] operations = instruction.getPcode();
for (PcodeOp operation : operations) {
if (operation.getOpcode() == PcodeOp.CBRANCH && operation.getInput(0).isConstant()) return true;
}
Register source = copiedRegister(operations);
if (source == null) continue;
next = source.getBaseRegister();
}
for (Instruction predecessor : predecessors(function, instruction)) {
instructions.push(predecessor);
targets.push(next);
}
}
return false;
}
private List<Instruction> predecessors(Function function, Instruction instruction) {
List<Instruction> result = new ArrayList<>();
Address from = instruction.getFallFrom();
if (from != null) {
Instruction previous = currentProgram.getListing().getInstructionAt(from);
if (previous != null) result.add(previous);
}
for (Reference reference : currentProgram.getReferenceManager().getReferencesTo(instruction.getAddress())) {
if (!reference.getReferenceType().isFlow() || reference.getReferenceType().isCall()) continue;
if (!function.getBody().contains(reference.getFromAddress())) continue;
Instruction source = currentProgram.getListing().getInstructionAt(reference.getFromAddress());
if (source != null) result.add(source);
}
return result;
}
private Register copiedRegister(PcodeOp[] operations) {
if (operations.length != 1) return null;
int opcode = operations[0].getOpcode();
Varnode input = operations[0].getInput(0);
if ((opcode != PcodeOp.COPY && opcode != PcodeOp.INT_ZEXT) || input == null || !input.isRegister()) return null;
return currentProgram.getRegister(input);
}
/** Validate the ABI layout of a pair; a small literal is decoded here, a large one only located. */
private SwiftLiteralLayout swiftLiteralLayout(long word, long object) {
int discriminator = (int) (object >>> 60);
if (discriminator == 0xE || discriminator == 0xA) {
int count = (int) ((object >>> 56) & 0xF);
byte[] bytes = new byte[15];
for (int index = 0; index < 8; index++) bytes[index] = (byte) (word >>> (8 * index));
for (int index = 0; index < 7; index++) bytes[8 + index] = (byte) (object >>> (8 * index));
for (int index = count; index < bytes.length; index++) {
if (bytes[index] != 0) return null;
}
// Count zero is the canonical empty string (object 0xE000000000000000, word 0).
String value = swiftStringText(bytes, count, discriminator == 0xE);
return value == null ? null : new SwiftLiteralLayout("small", null, count, true, value);
}
int flags = (int) (word >>> 60);
long count = word & 0xFFFFFFFFFFFFL;
// Immortal literals: ASCII and NFC or neither, tail allocated, with a pointer biased by 32.
if ((object >>> 56) != 0x80 || (flags != 0xD && flags != 0x1) || ((word >>> 48) & 0xFFF) != 0) {
return null;
}
try {
Address address = currentProgram.getAddressFactory().getDefaultAddressSpace()
.getAddress((object & 0x00FFFFFFFFFFFFFFL) + 0x20);
MemoryBlock block = currentProgram.getMemory().getBlock(address);
// The bytes and their terminator must lie in one initialized, non-executable block,
// and Java arrays bound one read.
if (block == null || !block.isInitialized() || block.isExecute() ||
count >= block.getEnd().subtract(address) + 1 || count >= Integer.MAX_VALUE) {
return null;
}
return new SwiftLiteralLayout("large", address, count, flags == 0xD, null);
} catch (Exception unaddressable) {
return null;
}
}
private String swiftLargeLiteralText(SwiftLiteralLayout layout) {
int count = (int) layout.byteLength();
byte[] bytes = new byte[count + 1];
try {
if (currentProgram.getMemory().getBytes(layout.address(), bytes) != bytes.length) return null;
} catch (Exception unreadable) {
return null;
}
return bytes[count] == 0 ? swiftStringText(bytes, count, layout.ascii()) : null;
}
/**
* Every literal record of one dossier draws on half of the heap that was free when decoding
* began. Text is charged at the worst case per UTF-8 byte: two bytes of UTF-16 text, then up
* to six escaped JSON characters ("\\u0000") held as UTF-16 in Gson's growing writer buffer
* (up to twice its length), again in the returned String, and once more as UTF-8 on the
* socket. Each record also pays a fixed cost for its map key, JSON members, addresses, words
* and their serialized form, so many empty or small literals are bounded too. Retained
* provenance-cache entries draw on that same fixed-state allowance.
*/
private static final class SwiftLiteralBudget {
private static final long BYTES_PER_LITERAL_BYTE = 2 + 6 * 2 * 2 + 6 * 2 + 6;
private static final long BYTES_PER_RECORD = 4096;
private long remaining;
private boolean stopped;
SwiftLiteralBudget() {
Runtime runtime = Runtime.getRuntime();
remaining = Math.max(0, (runtime.maxMemory() - runtime.totalMemory() + runtime.freeMemory()) / 2);
}
boolean takeState() {
if (remaining < BYTES_PER_RECORD) return false;
remaining -= BYTES_PER_RECORD;
return true;
}
boolean take(long bytes) {
if (remaining < BYTES_PER_RECORD ||
bytes > (remaining - BYTES_PER_RECORD) / BYTES_PER_LITERAL_BYTE) {
return false;
}
remaining -= BYTES_PER_RECORD + bytes * BYTES_PER_LITERAL_BYTE;
return true;
}
void stop() {
stopped = true;
}
boolean stopped() {
return stopped;
}
}
// Swift String values may contain any scalar, including controls; JSON escapes them.
private static String swiftStringText(byte[] bytes, int length, boolean ascii) {
if (length < 0 || length > bytes.length) return null;
for (int index = 0; index < length; index++) {
if (ascii && bytes[index] < 0) return null;
}
try {
return StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(bytes, 0, length)).toString();
} catch (CharacterCodingException invalid) {
return null;
}
}
private static JsonObject swiftStringOmission(SwiftLiteralLayout layout, long word, long object) {
JsonObject item = new JsonObject();
item.addProperty("representation", layout.representation());
if (layout.address() == null) {
item.add("address", JsonNull.INSTANCE);
} else {
item.addProperty("address", canonicalAddress(layout.address()));
}
item.addProperty("byte_length", layout.byteLength());
item.addProperty("reason", "capacity");
item.add("words", swiftStringWords(word, object));
return item;
}
private static JsonObject swiftStringItem(
String value,
SwiftLiteralLayout layout,
long word,
long object
) {
JsonObject item = new JsonObject();
item.addProperty("value", value);
item.addProperty("representation", layout.representation());
if (layout.address() == null) {
item.add("address", JsonNull.INSTANCE);
} else {
item.addProperty("address", canonicalAddress(layout.address()));
}
item.add("words", swiftStringWords(word, object));
return item;
}
private static JsonArray swiftStringWords(long word, long object) {
JsonArray words = new JsonArray();
words.add("0x" + Long.toUnsignedString(word, 16));
words.add("0x" + Long.toUnsignedString(object, 16));
return words;
}
private static String exclusiveAddress(Address inclusive) {
Address next = inclusive.next();
if (next != null && next.getAddressSpace().equals(inclusive.getAddressSpace())) {
@@ -4307,14 +3930,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
private record AddressNameRequest(String inputAddress, Address address, String name) {}
private record FunctionEntry(Function function, InventoryItem item) {}
private record FunctionAnalysisObservation(JsonObject dossier, List<String> limitations) {}
private record SwiftStringLiteralDecode(JsonArray literals, JsonArray omissions) {}
private record SwiftLiteralLayout(
String representation,
Address address,
long byteLength,
boolean ascii,
String smallValue
) {}
private record InstructionScan(List<Instruction> instructions, boolean truncated) {}
private record SessionDescriptor(
String transport,
+2 -15
View File
@@ -1,8 +1,6 @@
import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.MemoryBlock;
import ghidra.program.model.mem.MemoryBlockSourceInfo;
/** Seed the caller-supplied raw-binary entry before default auto-analysis. */
public final class ReaGhidraPrepareRaw extends GhidraScript {
@@ -23,19 +21,8 @@ public final class ReaGhidraPrepareRaw extends GhidraScript {
);
}
Address entry = currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress(entryHex);
MemoryBlock block = entry == null ? null : currentProgram.getMemory().getBlock(entry);
boolean fileStart = false;
if (block != null) {
// Processor-defined zero page/stack blocks can precede the imported file.
for (MemoryBlockSourceInfo source : block.getSourceInfos()) {
if (source.getFileBytes().isPresent() && source.getFileBytesOffset() == 0 &&
entry.equals(source.getMinAddress())) {
fileStart = true;
break;
}
}
}
if (!fileStart) {
Address start = currentProgram.getMinAddress();
if (entry == null || !entry.equals(start)) {
throw new IllegalArgumentException(
"Raw binary import must map file offset 0 at the caller-supplied entry"
);
+2 -4
View File
@@ -98,10 +98,8 @@ section, symbol, mitigation or linkage views. The default `complete` detail and
CLI output are unchanged.
Complete results have a 32 MiB input, 64 MiB reply, 1 MiB combined diagnostics
and 30-second owned command deadline. `REA_PWNTOOLS_MAX_INPUT_BYTES` raises the
input bound alone, up to 1 GiB; the reply and deadline bounds are unchanged. The
reply budget applies to the decoder record; the CLI/MCP Evidence envelope adds
copies and encoding overhead. The
and 30-second owned command deadline. The reply budget applies to the decoder
record; the CLI/MCP Evidence envelope adds copies and encoding overhead. The
pinned MCP SDK defaults to a 10 MiB receive buffer. Large complete results need
a caller-configured `StdioClientTransport({ maxBufferSize: ... })` and a matching
server `REA_MCP_MAX_RESPONSE_BYTES`. See [MCP delivery budgets](mcp-contracts.md#tool-results).
+1 -22
View File
@@ -80,27 +80,6 @@ google-chrome \
REA does not launch, own, or terminate this browser. Use a dedicated profile and stop it yourself when the investigation is complete. Do not expose the debugging port on a non-loopback interface.
Other Chromium-family browsers, such as Chromium or Brave, work the same way
with the same two flags. On macOS, run the executable inside the application
bundle, for example
`"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"` or
`"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"`. Fedora installs
Chromium as `chromium-browser`.
REA needs the browser's HTTP discovery endpoint on a loopback port. A browser
driven over `--remote-debugging-pipe` or a private channel, for example by an
automation SDK, exposes no such endpoint, so `list_browser_targets` has nothing
to attach to. Connection failures report `endpoint_unreachable`; a reachable
HTTP service that returns a non-200 or non-JSON discovery response reports
`invalid_endpoint_response`.
Reproduce that flow in a Chromium-family browser started with
`--remote-debugging-port`.
In a minimal Linux container, headed Chromium under Xvfb can keep running while
its DevTools port never opens. Install the GLES, EGL, and Mesa DRI libraries
(on Fedora: `libglvnd-gles`, `mesa-libEGL`, and `mesa-dri-drivers`), then
restart the browser.
On Linux, a reachable but locked desktop keyring can stall Chrome before its
first navigation. For a dedicated investigation profile, add
`--password-store=basic` to bypass that keyring. Empty or placeholder frame URLs
@@ -228,7 +207,7 @@ commands fail closed. A correlated reply's session ID must match the command's
selected session. An optional method is unavailable only when the browser
rejects it as unknown; other command rejections retain their reported failure.
Network and console coverage starts only after REA attaches and enables the relevant CDP domains. `prior_activity_available` is always `false`; absence from these arrays is not evidence that an event never occurred. Scripts are different: scripts the page parsed before REA attached are still reported under `scripts`. A page that loaded before attach can therefore list its scripts while `network.requests` lacks the requests that fetched them. If CDP reuses a request ID without supplying redirect metadata, REA retains the predecessor observation and marks network coverage incomplete.
Network and console coverage starts only after REA attaches and enables the relevant CDP domains. `prior_activity_available` is always `false`; absence from these arrays is not evidence that an event never occurred. If CDP reuses a request ID without supplying redirect metadata, REA retains the predecessor observation and marks network coverage incomplete.
Source maps are not fetched by inspection. Bundle analysis fetches them only when `fetch_source_maps` is selected, with exact-origin redirect checks and no credentials, cookies, or referrer. Each fetch operation has a 30-second deadline and a 64 MiB aggregate streamed-response budget across redirects. A deadline or byte-limit failure remains explicitly unavailable and makes bundle completeness partial. Source-map decoding also bounds raw resolved identities to 64 MiB and aggregate inline evidence to 32 MiB. Failed maps preserve their failure context when it fits; if another context cannot fit, collection stops with the full requested count, the returned processed count, and an explicit unknown-results limitation. Retained mappings and original sources are returned inline. See [source-location limits](web-source-location.md) for the structural decoder bounds. Original source artifacts with `.ts`, `.tsx`, `.mts`, or `.cts` suffixes carry `text/typescript`; other source names retain `text/javascript`.
+1 -3
View File
@@ -174,9 +174,7 @@ rea analyze /absolute/path/to/program --provider ghidra --snapshot /absolute/pat
rea analyze /absolute/path/to/program --provider ghidra --snapshot /absolute/path/to/analysis/program.json
```
An exact CLI cache hit is normally read before starting a provider process.
On Windows, Ghidra first admits the selected source through its native snapshot
owner, so replay can start that provider.
An exact CLI cache hit is read before starting a provider process.
In MCP, `open_binary` accepts `snapshot_path` to import a snapshot atomically
for its matching target; an MCP provider may still start before returning a
cached result. `close_binary` accepts `snapshot_path` and optional
+2 -17
View File
@@ -165,14 +165,6 @@ For [Qoder](https://docs.qoder.com/cli/mcp-reference), setup writes
from `~/.qoder/skills` (following `QODER_CONFIG_DIR` when set), so setup
installs its skill copy there.
## Factory Droid
For [Factory Droid](https://docs.factory.com/harness/mcp), setup writes
`mcpServers.rea` into the user-level `~/.factory/mcp.json`. Entries carry
`command`, `args`, and `env`. Droid discovers personal skills from
`~/.factory/skills` ([skills](https://docs.factory.com/harness/skills)), so
setup installs its skill copy there.
## Trae
For [Trae](https://docs.trae.ai/ide/add-mcp-servers), setup writes
@@ -217,15 +209,8 @@ replaces) the REA entry in the home patch `$DSH_HOME/cordis.patch.yml`
patches and comments. `rea doctor --client dsh` checks that same entry id.
`rea uninstall` removes it by scanning supported clients; uninstall takes no
`--client` flag. The home patch applies to every Harness profile, so the
registration is written once and no per-profile edit is needed.
Setup, doctor and uninstall resolve `DSH_HOME` and `DSH_AGENTS_HOME` as Harness
does: unset, empty or whitespace-only values use `~/.dsh` and `~/.agents`,
respectively. Bare `~`, `~/` and `~\` expand to the user home; other relative
values resolve against the command's working directory. Nonblank values retain
their original spaces, and the agents root is independent of the Harness home.
Run REA and Harness from the same working directory when using relative overrides.
The written entry is:
registration is written once and no per-profile edit is needed. The written
entry is:
```yaml
- insert:
+2 -14
View File
@@ -18,26 +18,14 @@ pass `-- chat` to check plain chat with REA enabled. Qwen also reads the full
result that its client offloads, and Pi exercises default codemode execution.
Use `REA_VERIFY_RUNTIME_ROOT` to select a production-only installed REA package.
Receipts retain client versions, result digests, host coverage and owned-process
cleanup. These lanes use a deterministic loopback model and do not prove live
model-provider compatibility. Pi and Hermes remain POSIX-only. Qwen, Pi and Hermes
cleanup. These POSIX lanes use a deterministic loopback model, so they do not
prove live model-provider or native Windows compatibility. Qwen, Pi and Hermes
bind both `HOME` and `USERPROFILE` to the disposable account. Qwen and Pi discover
the shared personal skill through their default search paths without an explicit
skill setting; Hermes discovers it through the selected `HERMES_HOME`.
Set `REA_VERIFY_HERMES_STICKY_PROFILE=1` to exercise native Hermes selection of
a named sticky profile with `HERMES_HOME` still pointing to its root.
The Qwen lane also supports native Windows (verified with Qwen Code 0.25.1 and
Node 24.19.0 for both `chat` and `call`). With Node 24.18.0, the same `call`
journey reached full Evidence readback but the client aborted during exit with
`UV_HANDLE_CLOSING`; that run remains a failure, not a compatibility claim.
Select the npm install's
`node_modules/@qwen-code/qwen-code/cli-entry.js` with `REA_VERIFY_QWEN_COMMAND`,
because `execFile` cannot launch the `qwen.cmd` shim directly. A selected `.js`,
`.mjs` or `.cjs` entry point runs under the verifier's Node. Windows path checks
use native separators; persisted output is read through Qwen's real shell tool
with a shell-independent encoded Node payload. Windows receipts report process
lineage as unavailable when the verifier has no retained native Job Object lease.
`verify:gemini-client` requires an installed Gemini CLI (verified with
`@google/gemini-cli@0.63.0` on POSIX and native Windows); select it with
`REA_VERIFY_GEMINI_COMMAND`. A selected `.js`, `.mjs` or `.cjs` entry point runs
+1 -1
View File
@@ -111,7 +111,7 @@ MIPS header and ABI record facts are committed in the analysis profile so
interpretation changes invalidate profile-bound snapshots. The support-lane
identity is versioned independently of file identity. Evidence, lifecycle
outputs and saved snapshots preserve the `mips` family. Hopper and IDA adapters
do not implicitly gain MIPS support. Windows Ghidra P0 does not admit MIPS targets.
do not implicitly gain MIPS support. Windows Ghidra P0 remains PE x86/x86-64 only.
### Soft-float prototype
+3 -4
View File
@@ -131,10 +131,9 @@ overview merely because its source artifact is unchanged.
Function extents and instruction completeness refer to Ghidra's analyzed body.
An inferred no-return call can terminate that body before subsequent bytes.
Pseudocode, assembly, procedure info, instruction windows, callee and caller
reads, and dossiers report observed terminal call sites in Evidence limitations,
including resolved callee names, addresses, and FunctionManager flags when
available. Instruction windows
Pseudocode, assembly, procedure info, instruction windows, and dossiers report
observed terminal call sites in Evidence limitations, including resolved callee
names, addresses, and FunctionManager flags when available. Instruction windows
and dossiers also retain these warnings in their result limitations. An
unresolved callee or unestablished terminal-flow basis remains explicit. These
observations qualify the analysis; they do not prove that a no-return flag is
+2 -2
View File
@@ -11,8 +11,8 @@ ELF `EM_RISCV` (243) is retained as the provider-neutral `riscv` family. The
target also retains the raw ELF declarations that decide a lane: class, byte
order, `e_type`, and `e_flags`. Recognition does not mean every provider or
RISC-V variant is supported. Existing PE, managed and Mach-O architecture sets
are unchanged. Headerless inputs use the separate [raw-binary admission
boundary](raw-binary.md); raw RISC-V images are not supported.
are unchanged, and caller-supplied headerless raw-binary entry still accepts only
its existing four families.
The Ghidra lane admits little-endian ELF32 and ELF64 `ET_EXEC` images that
declare `EF_RISCV_FLOAT_ABI_DOUBLE` in `e_flags`, with no `EF_RISCV_RVE`,
-4
View File
@@ -88,8 +88,6 @@ REA copies the admitted binary into a private temporary directory, checks its di
One REA session owns one database. Stdio registrations default `IDA_MCP_MAX_WORKERS` to `1`; the explicit upstream `--max-workers 1` also bounds a supervisor. Database size and engine memory depend on the target and upstream; REA does not impose an engine memory limit. The original binary is never executed or overwritten. Optional `workspaceRoot` selects an existing absolute temporary-directory parent on the REA host. Ensure it has space for the input copy and IDA working files.
When a completed `idb_open` response reports success or is otherwise ambiguous, but no active owned private worker can be verified, REA retains the private workspace even if `idb_list` appears empty. A later close can retry verification if the original owned worker becomes discoverable. A completed explicit `idb_open` rejection retains the existing no-worker cleanup path. This is a local admission/ownership safeguard and **does not** provide a process-termination receipt or change the legacy reaped-worker protocol.
Headless cleanup uses the upstream database lifecycle. Abruptly terminating REA, its proxy, or the supervisor does not establish worker release. Prefer `close_binary` for MCP sessions; after an interrupted process, inspect upstream session inventory and retained workspace diagnostics before removing working files. This adapter does not claim Windows Job Object containment for the upstream detached workers.
## Connect an agent once
@@ -128,8 +126,6 @@ For guided registration, set `REA_IDA_MCP_CONFIG` before `rea setup --client <cl
All profiles support procedure/string inventory and literal or regex search, procedure resolution, pseudocode, assembly, raw function instructions, resolved callees, incoming address references, and a function dossier. Inventories and modern pseudocode/assembly follow upstream pagination. Upstream limits on modern xrefs and callees are retained as explicit truncation limitations. In the modern headless profile, targets that are not proven x86-only use processor-neutral `callgraph` edges instead of the x86 instruction checks in upstream `callees` (mrexodia/ida-pro-mcp#549). REA requests depth zero: the pinned upstream emits the root's outgoing edges without scanning child functions, then REA batches function-name lookup for those addresses. The graph covers recognized function destinations only; imports and other non-function targets remain unknown. A server without `callgraph` returns an empty relationship list with an explicit coverage limitation. Graph and path workflows retain those limitations alongside observed edges. A failed or inconsistent graph reply remains an error. A transport-level truncated preview is rejected rather than reported as complete evidence; the error preserves upstream retrieval guidance.
The modern headless profile also supports `resolve_containing_procedure` and `list_documents`. An address resolves through upstream `lookup_funcs` to the canonical entry of the function that contains it; the body stays unavailable like other IDA identities. When no function contains it, REA reads the database-scoped minimal `survey_binary` and reports `not_in_procedure` for an address inside one of its segments (ends are exclusive) and `outside_segments` otherwise. A missing, truncated or inconsistent survey fails the read instead of guessing a reason. `list_documents` returns the single admitted program path, so `inspect_address_context` and `trace_feature` need no explicit document. `trace_feature` marks a trace truncated, with a residual unknown for that address, when upstream caps the references it returned. The legacy and native profiles do not declare these operations.
The legacy profile additionally supports direct callers. Its producer may report call-site or interior addresses; REA resolves the reported function symbol to its canonical entry. The modern and native profiles do not prove direct callers from its code/data xref labels, so that capability is unavailable and the dossier marks it unknown.
Complete body ranges, typed reference edges, CFG, referenced data, source locals, comment kinds, and database revision are unavailable. Empty unsupported dossier facets mean **unknown**, not observed absence. `binary_overview`, whole-symbol inventory, mutation, debugging, arbitrary Python forwarding, and requested GUI navigation are outside this adapter's coverage. Check session availability before composing a workflow that needs those facets.
+1 -5
View File
@@ -283,7 +283,6 @@ hosted connector; see [its configuration](client-configuration.md#other-client-s
| Pi | `pi` |
| Hermes | `hermes` |
| Qoder | `qoder` |
| Factory Droid | `droid` |
| Trae | `trae` |
| ZCode | `zcode` |
| DeepSeek Harness | `dsh` |
@@ -513,10 +512,7 @@ and `set_addresses_names` name a function entry or any other mapped address
session metadata edits leave executable bytes unchanged and are discarded on close. GUI controls
require Hopper; Windows P0 remains read-only.
Windows P0 admits native x86 and x86-64 PE applications and DLLs, plus explicitly
selected [raw NMOS 6502 images](raw-binary.md#nmos-6502) with a caller-supplied
entry at file offset 0, on fixed local NTFS volumes. Other raw CPU families remain
unsupported on Windows.
Windows P0 admits native x86 and x86-64 PE applications and DLLs on fixed local NTFS volumes.
The npm package bundles native Job Object ownership, protected private runtime
DACLs, and handle-based path admission; no separate addon installation is needed.
See the [Windows Ghidra P0 guide](windows-ghidra-p0.md) for verified scope and
-19
View File
@@ -208,25 +208,6 @@ arrays, and later explicit data properties and methods replace earlier object
spread references. A known array prefix does not expose values from a following
spread. Accessors, custom or uncertain iteration, unknown computed keys, and
unknown spread lengths retain conservative uncertainty.
Getter-local aliases of `this`, including direct destructuring and assignments,
retain their receiver when a returned shared reference is written. These writes
invalidate the earlier literal instead of claiming the getter executed or the
new value is known. Straight-line alias reads retain their capture order;
uncertain control flow remains conservative. A binding outside the getter or
`this` in a nested ordinary function is not attributed to the getter's receiver.
Private reads keep receiver projections separate from the private value's path.
Receiver projections follow known data fields initialized from `this` or earlier
self-referencing fields; static fields retain their declaring class identity.
An alias cycle that repeatedly selects deeper children retains escape uncertainty
at its recovered roots rather than claiming a fixed number of loop iterations.
Writes through an object getter's returned receiver retain that uncertainty
when the object is an array element, nested inside mixed object/array
containers, or assigned to a previously declared identifier. Saving the getter
result in an alias does not make its old field values immutable. Known array
indices include holes; an element after a spread has only a minimum possible
index. Unrelated properties and known array prefixes remain precise, and a
getter read without a write does not invalidate the receiver's fields.
Nested callable returns are not assigned to their parent callable. Projected
graph observations carry source ranges but never source text.
-22
View File
@@ -147,28 +147,6 @@ opposite observation's selection unknown, and application graphs report their
selected scope as partial. Neither makes an absence claim about the whole
assembly. Older member Evidence without `selection` remains readable.
### Edge target declaring types
`member_refs[].parent_name`, `call_edges[].target_declaring_type`, and
`field_accesses[].field_declaring_type` name the type that declares each
target. Referenced spec signatures validate encoded type coordinates against
the metadata tables, including nested type arguments and custom modifiers.
TypeRef resolution scopes are checked transitively with cycle detection;
readable names survive malformed scopes as partial evidence.
TypeDef and TypeRef names use the namespace-qualified `full_name`
form, and nested types keep bare names. A generic-instantiation TypeSpec
parent names its generic type, such as ``System.Collections.Generic.List`1``.
Other well-formed TypeSpec forms and ModuleRef or MethodDef parents stay
`null`. A `method-spec` call edge reports the name and declaring type of the
generic method it instantiates. A field access is named only by a field or a
field-signature MemberRef. TypeSpec and MethodSpec blobs are decoded with the
member-signature grammar, and a MethodSpec must instantiate a generic method
with as many type arguments as it declares. Malformed or unsupported rows and
blobs, and arity mismatches, make coverage partial. A readable MethodSpec
method column that names a method, or a TypeRef whose resolution scope is
invalid, keeps its name.
Older member Evidence without these fields reads them as `null`: unknown.
## Analysis objective
Start by identifying the artifact with `inspect_managed_artifact`, then inspect
+2 -5
View File
@@ -476,11 +476,8 @@ the tool fails with a `debugger-attach-denied` reason.
Install a Ghidra 12.1.x release and the 64-bit full JDK it declares, then configure REA to
use them. Ghidra analysis supports Linux x64/arm64 and macOS x64/arm64; each installation requires
the matching native decompiler. Experimental Windows x64 P0 admits native
x86 and x86-64 PE applications and DLLs, plus explicitly selected
[raw NMOS 6502 images](raw-binary.md#nmos-6502) with a caller-supplied load address,
on fixed local NTFS volumes. Other raw CPU families remain unsupported on Windows.
The boundary uses bundled Job Object ownership, protected runtime DACLs, and
handle-based path admission. See [Windows Ghidra P0](windows-ghidra-p0.md) and
x86 and x86-64 PE applications and DLLs on local NTFS using bundled Job Object ownership,
protected runtime DACLs, and handle-based path admission. See [Windows Ghidra P0](windows-ghidra-p0.md) and
[issue #527](https://github.com/morluto/rea/issues/527).
On Linux and macOS, `annotate_native_function` atomically edits a function name
and entry comments in the ephemeral database, returning refreshed analysis
+10 -53
View File
@@ -25,12 +25,10 @@ rea inspect-native-load-image /absolute/path/to/ecu.bin --target-format raw-bina
}
```
`architecture` is one of `x86`, `x86_64`, `arm`, `arm64`, or `6502`. `entry_point` is a
`architecture` is one of `x86`, `x86_64`, `arm`, or `arm64`. `entry_point` is a
hexadecimal address of the form `0x…`. The complete file is mapped starting at
that address, and Ghidra seeds an `entry` function there before auto-analysis.
ARM and AArch64 entries must be 4-byte aligned; x86 and 6502 entries may be byte-aligned.
For 6502, the entry and complete mapped file must fit within `0x0000..0xffff`;
images that cross the 16-bit boundary are rejected before provider startup.
ARM and AArch64 entries must be 4-byte aligned; x86 entries may be byte-aligned.
`open_binary` and an open `binary_session` return the canonical `entry_point`
for raw targets so callers can verify the selected mapping directly.
Omitted `format` keeps header-based detection; architecture and entry without
@@ -39,13 +37,12 @@ Omitted `format` keeps header-based detection; architecture and entry without
Ghidra imports a private snapshot with `BinaryLoader` and these little-endian
generic languages and compiler specifications:
| architecture | language_id | compiler_spec_id |
| ------------ | -------------------- | ---------------- |
| x86 | `x86:LE:32:default` | `gcc` |
| x86_64 | `x86:LE:64:default` | `gcc` |
| arm | `ARM:LE:32:v7` | `default` |
| arm64 | `AARCH64:LE:64:v8A` | `default` |
| 6502 | `6502:LE:16:default` | `default` |
| architecture | language_id | compiler_spec_id |
| ------------ | ------------------- | ---------------- |
| x86 | `x86:LE:32:default` | `gcc` |
| x86_64 | `x86:LE:64:default` | `gcc` |
| arm | `ARM:LE:32:v7` | `default` |
| arm64 | `AARCH64:LE:64:v8A` | `default` |
Compiler specifications select analysis conventions; they do not identify the
original compiler or ABI of a headerless target. Ghidra's x86 languages do not
@@ -60,51 +57,11 @@ The mapped start is independent of Ghidra's program image base, which remains
`0x0` for BinaryLoader. Ghidra may report additional language-defined entry
points, including unmapped ARM vectors. Load-image verification requires the
caller-supplied entry and preserves those other entries as observations.
For 6502, BinaryLoader maps the file before adding its language-defined zero-page
and stack blocks. Conflicting default blocks are skipped; low-address files stay
in the default address space rather than an overlay. Non-conflicting uninitialized
blocks remain observations, not file-backed bytes.
Independent raw verification streams the complete private snapshot digest and
extent with bounded memory and supports cancellation during the read.
It also requires the source-backed block to be initialized, loaded, and
non-overlay in the bound language's default address space. Contradictory mapping
observations are retained with `mismatch`, even when the source hashes agree.
Hopper and IDA refuse raw-binary targets. Windows Ghidra P0 admits native PE
applications/DLLs and explicitly selected raw 6502 images. Other raw CPU families
remain outside the Windows boundary. Native path admission, private DACLs, Job
Object ownership, immutable snapshots, and the no-mutation policy still apply.
## NMOS 6502
```bash
rea function /absolute/path/to/image.bin 0x8001 --target-format raw-binary --architecture 6502 --entry-point 0x8001 --provider ghidra --json
rea inspect-native-load-image /absolute/path/to/image.bin --target-format raw-binary --architecture 6502 --entry-point 0x8001 --provider ghidra --json
```
Select only headerless NMOS 6502 code with a known entry at file offset 0.
The built-in Ghidra processor supplies static instructions, functions,
pseudocode, and references. Evidence preserves `architecture: "6502"`, the
selected language, entry, file digest, and verified source mapping. Register and
calling conventions are analysis conventions, not a recovered source ABI.
This lane does not interpret iNES headers, cartridge mappers, bank switching,
or other console-specific memory layouts. 65C02 and 65816 programs need different
processor semantics and are not admitted as those variants. REA does not infer
the CPU from the file suffix, guess the reset vector, or emulate the target.
`npm run verify:ghidra:6502` assembles and links the source-owned fixture with
`ca65` and `ld65` (tested from cc65 tag `V2.19`, whose tools report `V2.18`).
Set `REA_CA65` and `REA_LD65` to explicit executable paths if needed. The lane
checks real CLI/MCP load-image parity, mapped bytes, decoded calls, decompilation,
invalid mappings, unchanged source bytes and runtime cleanup.
Low-memory checks include a file spanning the zero-page/stack boundary at `0xff`
and a complete 64-KiB image at `0x0`, with verified default-space mappings through
both CLI and MCP. On Windows, the lane also checks that a caller-selected
junction path is rejected at snapshot admission through CLI and MCP, with
unchanged source bytes and complete runtime cleanup. Windows requires the
matching native addon; the POSIX provider path has not been exercised by the
Windows run.
Hopper and IDA refuse raw-binary targets. Windows Ghidra P0 continues to admit
native PE applications and DLLs only; raw-binary targets are unsupported.
## Limitations
+3 -7
View File
@@ -72,13 +72,9 @@ debugger context is `observed`.
Initial Linux x64 limits: 32 MiB input, 64 MiB decoder reply, 1 MiB diagnostics per
stage, 30 seconds owned wall deadline per stage, 3 GiB address-space soft ceiling
and 30 seconds CPU soft ceiling. `REA_PWNTOOLS_MAX_INPUT_BYTES` raises the input
bound alone, up to 1 GiB, and returns `provider_unavailable` when it is not a
decimal integer in range; the reply, diagnostic and deadline bounds stay fixed, so a
large core that outruns them still fails explicitly instead of partially. Tighter
inherited soft/hard limits are retained; hard limits are not raised. Complete
oversized results fail explicitly. These process limits do not constitute a sandbox
for caller-supplied engine code.
and 30 seconds CPU soft ceiling. Tighter inherited soft/hard limits are retained;
hard limits are not raised. Complete oversized results fail explicitly. These
process limits do not constitute a sandbox for caller-supplied engine code.
Resource exit statuses need matching private failure-branch markers. Unmarked
launcher failures retain their actual status/output and unknown resource cause.
+1 -13
View File
@@ -682,9 +682,7 @@ use `--second-procedure rea_fixture_subtract --second-marker='- 13'`.
The lane invokes the production CLI dispatcher and connects the pinned MCP
client SDK to the production REA server. It verifies function Evidence and
CLI/MCP parity, inventory/search, pseudocode, instructions, xrefs, malformed
input, original-input preservation, and lifecycle cleanup. The headless
profile also checks containing procedures, the admitted program as the
document, address context, and feature tracing. For headless
input, original-input preservation, and lifecycle cleanup. For headless
analysis it confirms that the owned database IDs disappear from upstream
discovery and private workspaces are removed. For attached analysis it confirms
the existing GUI target remains reachable with the same input identity.
@@ -734,16 +732,6 @@ truncation constraint, while leading-zero coordinates still resolve correctly.
It has the same Ghidra/JDK prerequisites as the MZ lane. Neither lane claims DOS
runtime or PC-98 device execution.
`npm run verify:ghidra:6502` uses `ca65` and `ld65` to build the source-owned
NMOS 6502 fixture from `tests/conformance/ghidra/6502.s` and `6502.cfg`. Set
`REA_CA65` and `REA_LD65` for non-PATH installations (tested with cc65 `V2.19`).
With an installed Ghidra 12.1.4 and full JDK, it verifies real CLI/stdio MCP
mapping parity, exact source bytes, functions, a direct call reference,
decompilation, 16-bit overflow rejection without replacing the active session,
and runtime cleanup. Windows additionally requires the matching native addon;
its verifier lineage remains explicitly unavailable outside the provider's
retained Job Object, as in the other Windows lanes.
`npm run verify:ghidra:raw` uses a generated ARM little-endian headerless image
through the compiled CLI and stdio MCP server. With the same Ghidra/JDK
prerequisites, it checks preparation and bridge startup, verified load mappings,
+3 -11
View File
@@ -17,16 +17,14 @@ additional permission flag or degraded mode is required.
- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+;
- an operator-installed Ghidra 12.1.x distribution (verified with 12.1.4);
- a 64-bit full JDK inside that installation's declared Java range (JDK 21 or newer, with no maximum, for current 12.1 releases);
- an explicit native, non-managed x86 or x86-64 PE application/DLL, or a
[raw NMOS 6502 image](raw-binary.md#nmos-6502) with a caller-supplied entry; and
- an explicit native, non-managed x86 or x86-64 PE application or DLL; and
- the 25 read-only Ghidra inventory, memory, and function-analysis operations.
Loaded memory reads and file offsets preserve Ghidra's observed source mappings.
PE load-image inspection returns an explicit unsupported attestation status.
Independent verification covers admitted DOS MZ, explicit COM, and explicit
[raw-binary imports](raw-binary.md) on their supported hosts, including the
admitted raw NMOS 6502 mapping on Windows. Windows P0 admission remains limited
to native PE and raw NMOS 6502 images. For x64 PE
[raw-binary imports](raw-binary.md) on their supported hosts. This does not
extend Windows P0 admission beyond native PE. For x64 PE
NativeAOT RTR 9.1, the read-only snapshot parser can report metadata through
`inspect_native_load_image` and `inspect_native_data_type` without a JAR or
database mutation. A real Windows-host NativeAOT Ghidra run is still required
@@ -37,12 +35,6 @@ state, annotations, other target architectures and formats, and Hopper-only
operations remain unavailable through this provider. Managed PE/CLI inspection
uses its separate execution-free provider.
Raw 6502 imports retain the same native filesystem and process ownership
requirements. The complete image must fit in the 16-bit address space; no
console-specific cartridge loader, CPU variant, or bank-switching model is
implied. Run `npm run verify:ghidra:6502` with caller-installed `ca65`/`ld65`
to check the source-built fixture through real CLI and MCP workflows.
REA does not install or upgrade Ghidra, Java, Python, npm, Node.js, Hopper, or a
compiler. The adapter uses the packaged Java `HeadlessScript`; Python and
PyGhidra are not prerequisites. Users do not build the native addon.
@@ -94,9 +94,6 @@ def main():
if undeclared:
raise ValueError('Declare inherited provider settings explicitly in --providers: '
+ ', '.join(undeclared) + '. Values are not logged.')
source_auth_home = (Path(os.environ['CODEX_HOME']) if 'CODEX_HOME' in os.environ
else Path.home() / '.codex')
source_auth = source_auth_home / 'auth.json'
args.output.mkdir(parents=True, exist_ok=False, mode=0o700)
runtime_files, runtime_digest = runtime_fingerprint(repository)
(args.output / 'runtime-files.json').write_text(json.dumps(runtime_files))
@@ -106,6 +103,7 @@ def main():
workspace = profile / 'workspace'
for directory in [trial_codex_home, account, workspace]:
directory.mkdir(mode=0o700)
source_auth = Path(os.environ.get('CODEX_HOME', str(Path.home() / '.codex'))) / 'auth.json'
auth = trial_codex_home / 'auth.json'
started = time.monotonic()
timeout = False
@@ -71,9 +71,7 @@ class ContextCaptureTests(unittest.TestCase):
self.assertNotIn(marker, text)
self.assertIn('caller-selected evidence and paths', text)
self.assertIn('original-response', text)
# Windows mode bits do not describe access control; this is a POSIX assertion.
if os.name == 'posix':
self.assertEqual((output / 'codex-tool-context.jsonl').stat().st_mode & 0o777, 0o600)
self.assertEqual((output / 'codex-tool-context.jsonl').stat().st_mode & 0o777, 0o600)
def test_missing_rollouts_are_unknown_and_malformed_lines_remain_partial(self):
with tempfile.TemporaryDirectory() as directory:
@@ -126,7 +124,6 @@ class ContextCaptureTests(unittest.TestCase):
patch.object(agent.subprocess, 'check_output', return_value='v24.18.0\n'), \
patch.object(agent.subprocess, 'Popen', return_value=child), \
patch.object(agent, 'capture_tool_context', side_effect=OSError('fixture capture failure')), \
patch.object(agent.Path, 'home', side_effect=AssertionError('Explicit CODEX_HOME must not resolve the default home')), \
patch('builtins.print'):
with self.assertRaisesRegex(OSError, 'fixture capture failure'):
agent.main()
@@ -1,128 +0,0 @@
# Windows reconciliation of recorded POSIX paths
Follow-up to [#2422](https://github.com/morluto/rea/pull/2422), including
@Aurantium-47's diagnosis of the host-dependent relative-path operation.
## Source pins and real data
The initial reproduction and passive producer used main
fa4b8288655188426222773ead2ccbdaf76334fd. Before branching, main was updated
to 8b181716aca2cb467be499cba226abfb5ae19141. The owning matcher and its
affected reconciliation consumers were unchanged between those revisions.
The newer baseline again reproduced both errors on the real Windows host.
Input: npm 11.16.0's published lib/utils/validate-lockfile.js, 1,023 bytes,
SHA-256 2295e2dac1c5816e1eae2d417804960ebbef17b64a0a44db4f3483ce6e6d9526.
The installed copy was independently checked against the exact module in the
[official registry package](https://registry.npmjs.org/npm/11.16.0).
The 2,945,850-byte tarball matched its published SHA-512 SRI before reading
that member; no archive extraction or installation occurred, and the tarball
was not retained.
Two actual Linux filesystem directories, Runtime/App and Runtime/app,
held byte-identical module copies and had distinct directory identities.
A controlled Node 24.18.0 process loaded the public module and ran its real
function against 587 locked dependency records: compatible trees returned zero
errors, and one controlled version mismatch returned one error. A passive REA
Inspector capture recorded 114 loaded scripts, including the selected module.
The provider did not evaluate, pause, resume or read source. The owned process
exited normally. Function execution is established by the harness receipt;
REA's passive script metadata establishes loaded status.
Production static analysis inspected both directories plus an independent
Static/public-module copy. The latter prevents implicit root matching from
hiding the explicit mapping result. Complete static/runtime Evidence was then
consumed by the actual **Windows** CLI and SDK 2.3.1 stdio MCP server, using
Windows 11 10.0.22631.0 and official Windows x64 Node 24.18.0.
No Electron GUI, IDA or Ghidra engine was used.
## Correctness and provenance
| Windows reconciliation | Baseline | Candidate |
| -------------------------------------- | ------------------------------ | ----------------------------------- |
| Exact POSIX input root | Matched, artifact path | Same complete normalized result |
| Case-mismatched POSIX input root | Incorrectly matched and loaded | Unknown; static load states unknown |
| Exact explicit POSIX mapping | Matched, operator file mapping | Same complete normalized result |
| Case-mismatched explicit POSIX mapping | Incorrectly matched and loaded | Unknown; static load states unknown |
All four CLI and same-connection MCP normalized results agree. The input
Evidence, recorded runtime facts and caller-selected root spellings are
preserved. Rejected location membership does not establish that a module was
absent or unexecuted. The operation remains offline and performs no target or
filesystem inspection.
The shared matcher now chooses posix.relative after identifying POSIX input;
Windows coordinates continue to use win32.relative. Both location matching
and active-layer scope use this owner. The obsolete host-default import is
removed; no downstream repair or new path abstraction is introduced.
The related suite passed 102 tests: 15 workflow tests, 85 CI scope tests and
two curated-path checks. Four simulated Electron Evidence controls cover
POSIX mismatch/exact matching, Windows case-insensitive matching, the resulting
asset load state and retained mapping spelling. They are service regressions,
not real Electron-provider verification. The existing workflow test file is
registered in both the Windows execution lane and its package-change scope.
## Delivery and resource observations
Probes ran serially at BelowNormal priority with one CPU and 512 MiB Node heap
settings for CLI/server/client work. Heap settings do not bound aggregate memory.
Four corrected CLI exit-time observations reported 7.343–15.827 seconds CPU and
322,308–324,280 KiB peak working set, excluding the probe parent. Wall times
ranged from 7.785 to 17.601 seconds. There is no paired resource baseline.
One complete MCP trial connected in 18,296 ms and discovered 189 tools in
875 ms (2,588,336 serialized bytes). The four subsequent same-connection calls
took 879, 320, 353 and 422 ms. Their serialized replies were 918,133 / 913,765 /
918,571 / 914,171 bytes; inputs totalled 681,312 bytes. Discovery plus these
inputs and replies totalled 6,934,288 bytes, excluding initialization, framing
and notifications. This is delivery volume, not model token usage. Actual
model context and total model task cost were not measured.
A process-generation sampler recorded 233 observations over 25,671 ms:
15,797 ms aggregate observed CPU as a lower bound and a peak sampled working-set
sum of 491,597,824 bytes (about 469 MiB). Exited-process tails can be missed;
shared pages can be counted twice. This excludes the observer and is not an
exact physical-memory peak. The server wrote four structured completion records
to stderr; tool replies and normalized results passed the recorded checks.
Cold/warm caches and observer differences were not normalized. No latency,
CPU, memory or model-token improvement is claimed. The fix adds no lookup I/O,
engine invocation, retry or cache. Complete default replies remain available;
only compact selected findings and hashes were sent to the investigating agent.
## Main integration replay
Runtime cb0d8a9fef2b907cff2ba27f1e017493e1173eb2, rebased onto main
73dd8d6ed, again passed the 101 related tests and all four real Windows CLI/MCP
cases using the unchanged older producer Evidence. Positive results and original
runtime/layer facts remain identical. Typecheck, lint, dependency and metadata
checks passed; all five changed files passed formatting independently.
The full format gate at that main revision failed on the unchanged upstream
GhidraDoctor.test.ts and GhidraSnapshotCache.test.ts files. The PR does not
repair those unrelated baseline files. Resource figures above refer to the
earlier candidate; the integration replay does not establish a speedup.
Review found that owning only the Windows test path would still skip that lane
on a later implementation-only edit. The matcher implementation is now also
registered in package scope. A source-only classifier case verifies that it
selects both Windows package and JavaScript recovery lanes; 85 scope checks and
two curated-path checks pass. Runtime code and the real-data checks are unchanged.
The original production fix merged as
[1a1b67c88](https://github.com/morluto/rea/commit/1a1b67c88) while this follow-up
was open. The follow-up was then rebased onto main fb28ec153 and contains only
the stronger workflow assertions, Windows lane ownership, scope classification
and this verification record. The updated 131 related tests, two representative
Ghidra boundary tests, seven IDA input-thinning tests, 24 semantic-structure
tests, 16 required-lane checks, 39 release-checkpoint tests and 11 dyld CLI
tests passed after the rebase (230 total).
Upstream a0317cf73 corrected the invalid FAT fixture and consolidated unrelated
duplicated helpers; e05d3399f aligned the older parameter-provenance assertion
with the upstream semantics. Those changes, the threshold change and the new
CI qualification tiers are part of main, outside this follow-up's diff.
Upstream 747e72991 restored release preflight coverage for reusable workflow
jobs. The independently reviewed, fully green #2755 fixture correction merged
as fb28ec153 and restored real x64 dyld CLI admission without altering the
original same-named arm64e assertions.
No production source is duplicated by the follow-up.
-1
View File
@@ -174,7 +174,6 @@
"verify:ghidra:seeds": "npm run build:cached && node scripts/verify/ghidra/seeds.mjs",
"verify:ghidra:mips": "npm run build:cached && node scripts/verify-real-ghidra-mips.mjs",
"verify:ghidra:riscv": "npm run build:cached && node scripts/verify-real-ghidra-riscv.mjs",
"verify:ghidra:6502": "npm run build:cached && node scripts/verify-real-ghidra-6502.mjs",
"verify:ghidra:mips-shared": "npm run build:cached && node scripts/verify-real-ghidra-mips-shared.mjs",
"verify:ghidra:mips-relocatable": "npm run build:cached && node scripts/verify-real-ghidra-mips-relocatable.mjs",
"verify:ghidra:swift": "npm run build:cached && node scripts/verify-real-ghidra-swift.mjs",
+525
View File
@@ -0,0 +1,525 @@
:robot: I have created a release *beep* *boop*
---
## [6.5.0](https://github.com/morluto/rea/compare/rea-agents-6.4.0...rea-agents-6.5.0) (2026-10-13)
###   BREAKING CHANGES
* **runtime:** attached fields in JavaScript runtime, browser, and Electron target discovery and inspection results may be null when discovery does not report attachment state.
* **sourcemaps:** Browser source-map original and mapping rows expose null for unavailable source names instead of inventing an unknown-source string. New index fields are optional when reading older retained evidence.
* **analysis:** get_call_graph normalized_result now contains { graph, limitations }; traversal-depth groups are under graph.
* **mcp:** binary_session no longer includes server_identity.catalog.tools; tool_availability and provider capabilities use grouped facts documented in docs/mcp-contracts.md.
### Features
* add Frida instrumentation support ([#1359](https://github.com/morluto/rea/issues/1359)) ([4a4ae9b](https://github.com/morluto/rea/commit/4a4ae9b2b9712f7ac16f2d23c71b5c29b0c23d2f))
* **analysis:** add opt-in retained inventory and WASM views ([#2122](https://github.com/morluto/rea/issues/2122)) ([58fb372](https://github.com/morluto/rea/commit/58fb372071e56da9c5df0e37593ee3f0da6b64bc)), closes [#2016](https://github.com/morluto/rea/issues/2016)
* **android:** add opt-in package summaries and retained manifest views ([f254f5a](https://github.com/morluto/rea/commit/f254f5ac2b804b353a711264bb178f43fdf07867))
* **browser:** inspect retained aioquic qlog JSON captures ([bb154d1](https://github.com/morluto/rea/commit/bb154d1067b0fc250452b7d468d0b0523031a16f))
* **captures:** discover installed protocol and field definitions ([#2496](https://github.com/morluto/rea/issues/2496)) ([9c34c09](https://github.com/morluto/rea/commit/9c34c091b3b8c47f8bd18d6f0321efde2dedb92a))
* **captures:** inspect recorded packets with bounded TShark delivery ([#2318](https://github.com/morluto/rea/issues/2318)) ([f611d8c](https://github.com/morluto/rea/commit/f611d8ce722cd36345e9ff83b9669ef2e7fe57a9))
* **clients:** register Qoder CLI during setup ([#1572](https://github.com/morluto/rea/issues/1572)) ([1097693](https://github.com/morluto/rea/commit/109769319a4a3a5f85e8a43bb419d04aefff66a4))
* **clients:** register Trae during setup ([#1573](https://github.com/morluto/rea/issues/1573)) ([bfa805d](https://github.com/morluto/rea/commit/bfa805decf83744bd3ecb0d1402667cf3ed5c3b2))
* **clients:** register ZCode during setup ([#1564](https://github.com/morluto/rea/issues/1564)) ([51d6c87](https://github.com/morluto/rea/commit/51d6c87cdbf5f11eaf3cb1862befe704a81827c5))
* **evm:** add retained interface summary delivery ([#2308](https://github.com/morluto/rea/issues/2308)) ([f76163c](https://github.com/morluto/rea/commit/f76163c2467db623f9de0bda757c9d8610ca9565)), closes [#2016](https://github.com/morluto/rea/issues/2016)
* **fiddler:** inspect native SAZ captures ([a2d8b94](https://github.com/morluto/rea/commit/a2d8b94dd50dab8d966239dd63ab84718aa8b4b4))
* **flutter:** identify Dart snapshot builds and inspect AOT payloads by pure APK parsing ([#1582](https://github.com/morluto/rea/issues/1582)) ([e4d3e13](https://github.com/morluto/rea/commit/e4d3e1399d9cd4932653e7015a1bf8589e56029d))
* **ghidra:** admit bounded PSP ELF through installed Allegrex ([#1599](https://github.com/morluto/rea/issues/1599)) ([e4407db](https://github.com/morluto/rea/commit/e4407dbed5dc24851f47532f5e3d75d5627d3e7d))
* **ghidra:** admit relocatable MIPS32r2 ELF objects ([#1950](https://github.com/morluto/rea/issues/1950)) ([290d301](https://github.com/morluto/rea/commit/290d301ec770955f313c2d48678ed0ae3b9f8a73))
* **ghidra:** admit RISC-V ELF analysis through a pinned generic lane ([#2151](https://github.com/morluto/rea/issues/2151)) ([0baad58](https://github.com/morluto/rea/commit/0baad58e606f12a8dc6cef06253f85350378e5b1))
* **ghidra:** admit soft-float MIPS32r2 ELF with integer-register float storage ([#1949](https://github.com/morluto/rea/issues/1949)) ([5e219de](https://github.com/morluto/rea/commit/5e219de44134d34f5a07a9cd1911fb2295bf1d88))
* **ghidra:** admit verified MIPS16 shared ELF images ([e9fe643](https://github.com/morluto/rea/commit/e9fe6437a7da381f0aa5aa77571dcf03317122e7)), closes [#1851](https://github.com/morluto/rea/issues/1851)
* **ghidra:** apply validated analyzer overrides before analysis ([9878bb7](https://github.com/morluto/rea/commit/9878bb7296c13c2b4838be76f6128e5fc31eda3c))
* **go:** inspect embedded binary build metadata ([#1363](https://github.com/morluto/rea/issues/1363)) ([a1b5d51](https://github.com/morluto/rea/commit/a1b5d51a8dfb7fff6272d91270c16b95555c2643))
* **harmony:** project HarmonyOS package inventory evidence ([#1447](https://github.com/morluto/rea/issues/1447)) ([f2150eb](https://github.com/morluto/rea/commit/f2150eb9a7ed76748238009ca6666efa52de5271))
* **ida:** support optional native IDA Free MCP in attached mode ([#1544](https://github.com/morluto/rea/issues/1544)) ([d1aaeee](https://github.com/morluto/rea/commit/d1aaeee400e0d9911d5c53339ca3ac42dccbbba3))
* **javascript:** analyze selected application sources ([85c1d25](https://github.com/morluto/rea/commit/85c1d2587a7d42c6e2108405c802fe7f7b046c15)), closes [#1848](https://github.com/morluto/rea/issues/1848)
* **javascript:** recover code-split bundles from multiple inputs ([#1686](https://github.com/morluto/rea/issues/1686)) ([12c4160](https://github.com/morluto/rea/commit/12c416005f9d2abf438f4df4447972424d781b65))
* **javascript:** resolve exact package import aliases ([#1862](https://github.com/morluto/rea/issues/1862)) ([d28b41e](https://github.com/morluto/rea/commit/d28b41ea4e93bb4c4d360a3bf2291ca83bb2b378))
* **managed:** add retained metadata overview and pages ([#2413](https://github.com/morluto/rea/issues/2413)) ([82cfcee](https://github.com/morluto/rea/commit/82cfcee19f18b0eeae9a330ba04e0411cdf85225))
* **managed:** select declaring types in inspect_managed_members ([a149d18](https://github.com/morluto/rea/commit/a149d183efd08b0ad7c2a35c99f2394d905621d5)), closes [#1878](https://github.com/morluto/rea/issues/1878)
* **mcp:** add opt-in paging for retained unknown heads ([9a18251](https://github.com/morluto/rea/commit/9a18251426f891e6a24856b715ef8002ab8b9610))
* **mcp:** derive an API surface view from retained web network captures ([#1891](https://github.com/morluto/rea/issues/1891)) ([880a603](https://github.com/morluto/rea/commit/880a603fe89db2132fd05a35b33a8c459673beda))
* **native:** admit raw binaries with caller-supplied architecture and entry ([#1868](https://github.com/morluto/rea/issues/1868)) ([7c207c7](https://github.com/morluto/rea/commit/7c207c79512f6baa70f4ed9d7d5334ab2d62f3ac))
* **native:** expose typed incoming references at exact addresses ([#1715](https://github.com/morluto/rea/issues/1715)) ([cdd9fe7](https://github.com/morluto/rea/commit/cdd9fe716774c9e5abd121b70e9fd5e1eb91d2b8))
* **pcap:** add bounded capture summaries through CLI and MCP ([9581e06](https://github.com/morluto/rea/commit/9581e067ad306aab842024e47f2dd18ac8056859))
* **server:** frame analysis-target content as untrusted data ([#1908](https://github.com/morluto/rea/issues/1908)) ([85e1242](https://github.com/morluto/rea/commit/85e124253ac93dd443be6bbcee0939d525b90d4e))
* **setup:** register DeepSeek Harness through setup, doctor and uninstall ([#1929](https://github.com/morluto/rea/issues/1929)) ([d9d2a17](https://github.com/morluto/rea/commit/d9d2a173819ce8e2e7f09eb584ec6ff7b4ee3480))
* **signals:** decode recordings with bounded sigrok output ([#2339](https://github.com/morluto/rea/issues/2339)) ([91b8f58](https://github.com/morluto/rea/commit/91b8f587cd495c509946e69076525eb19dc1ca65))
* **signals:** discover installed decoders and option help ([#2487](https://github.com/morluto/rea/issues/2487)) ([4276aaf](https://github.com/morluto/rea/commit/4276aaf18bb607cdd0182280f76cf50fcd110c3c))
* **sqlite:** inspect offline snapshots with selective evidence ([#2271](https://github.com/morluto/rea/issues/2271)) ([aeee507](https://github.com/morluto/rea/commit/aeee5074d124414239b6dd440dc79c26bcbd58a4))
* **target:** explain static WASM inspection after native rejection ([1f17ac8](https://github.com/morluto/rea/commit/1f17ac82282dfda1ae2dc87d99e66a8a039c0259)), closes [#1852](https://github.com/morluto/rea/issues/1852)
* **update:** update Bun global installations ([#1735](https://github.com/morluto/rea/issues/1735)) ([e4027e4](https://github.com/morluto/rea/commit/e4027e4afccc6316cd9cfc27c7b0427e66ceb91f))
* **wasm:** add opt-in first-call summary delivery ([#2193](https://github.com/morluto/rea/issues/2193)) ([0b71453](https://github.com/morluto/rea/commit/0b7145361ec02d27260eb1f5bc0be955c9374a96))
* **wasm:** inspect local artifacts with WABT ([#1533](https://github.com/morluto/rea/issues/1533)) ([ffaf2c1](https://github.com/morluto/rea/commit/ffaf2c12f1793c7c451e66dc09c3074544a4c086))
* **website:** add a persistent light and dark theme switch ([4773c15](https://github.com/morluto/rea/commit/4773c152bc5f299fa682575bb8d1efea7587c7bc))
* **website:** add multilingual editions and translation workflow ([a2d3338](https://github.com/morluto/rea/commit/a2d3338e90a3a11385e2f63bedaa9750612f8719))
* **website:** follow the system dark mode and fix small-screen text ([#1894](https://github.com/morluto/rea/issues/1894)) ([12ee785](https://github.com/morluto/rea/commit/12ee785e5e61132898adecf9424ce6706a2e754a))
### Bug Fixes
* **adb:** admit canonical device states and preserve malformed output ([e1cd292](https://github.com/morluto/rea/commit/e1cd292aa6e8f5f13441d408c66126782c8dbb0d))
* **adb:** count requested permissions that the device marks restricted ([#2359](https://github.com/morluto/rea/issues/2359)) ([15463bd](https://github.com/morluto/rea/commit/15463bdbe605b9dd11da5dbba031f99cd1ec9190))
* **adb:** count UTF-8 bytes in logcat total_bytes ([#2212](https://github.com/morluto/rea/issues/2212)) ([ec2d251](https://github.com/morluto/rea/commit/ec2d251c0ad8382e0665546be5ec8f5f1c175962))
* **adb:** evaluate the full activity start report ([c89972c](https://github.com/morluto/rea/commit/c89972c8777d0114566fe76a060849a18bdd2659)), closes [#2225](https://github.com/morluto/rea/issues/2225)
* **adb:** preserve device paths and prevent destination collisions ([55b6148](https://github.com/morluto/rea/commit/55b61485e9f7568b69597eef8696ff6abfe53cfd))
* **adb:** preserve directory identities and share byte capture ([3bdc873](https://github.com/morluto/rea/commit/3bdc8730bd98e073522bcb8ba86527be402c0cbe))
* **adb:** preserve existing bugreport archives ([#2221](https://github.com/morluto/rea/issues/2221)) ([d80b55e](https://github.com/morluto/rea/commit/d80b55e11bc47c6f999dac7dcb8382219d65739a))
* **adb:** reject unsafe host file names when pulling one device file ([#1962](https://github.com/morluto/rea/issues/1962)) ([1d4c4eb](https://github.com/morluto/rea/commit/1d4c4eb0308b4cb5761a91533edeb33fcfb442f2))
* **adb:** share actionable provider configuration refusals ([c42232f](https://github.com/morluto/rea/commit/c42232f566d55fe7191763649755148c768eef9f))
* **adb:** share host configuration refusals with apktool ([#1954](https://github.com/morluto/rea/issues/1954)) ([39418ea](https://github.com/morluto/rea/commit/39418ea173763ca55d4f116992ee903bcaeb1898))
* align MCP archive contracts and documentation ([#2478](https://github.com/morluto/rea/issues/2478)) ([a86deec](https://github.com/morluto/rea/commit/a86deecac5fa8434a4f50482f86b0c6ab486a936)), closes [#2454](https://github.com/morluto/rea/issues/2454)
* **analysis:** expose provider image base ([#2083](https://github.com/morluto/rea/issues/2083)) ([81cca2e](https://github.com/morluto/rea/commit/81cca2e0e3ebd98c21f41d7ad63d6a00e4cbc3f9))
* **analysis:** preserve producer identity and partial evidence ([6fb3a34](https://github.com/morluto/rea/commit/6fb3a345fe952b8e8f59221aec993acc37ccbf31))
* **analysis:** preserve relationship coverage across evidence and replay ([9c85114](https://github.com/morluto/rea/commit/9c8511409c0706f9979c2b68dd56aaaf9f21e191))
* **analysis:** scope call-graph caveats to their observed relationships ([5dfb9d0](https://github.com/morluto/rea/commit/5dfb9d050c2cc44a61b183dae765bef4d22a53b7))
* **android:** bound inline search Evidence with retained inventory provenance ([#1890](https://github.com/morluto/rea/issues/1890)) ([7a0140b](https://github.com/morluto/rea/commit/7a0140b4aa43276368058ab98826b77e040ea14f))
* **android:** preserve decoded values and duplicate class inventories ([0677468](https://github.com/morluto/rea/commit/0677468877465c8dfbe25c72f142a7b5bc8a3212)), closes [#1978](https://github.com/morluto/rea/issues/1978)
* **android:** preserve executable static initializer evidence ([#1876](https://github.com/morluto/rea/issues/1876)) ([efbc25f](https://github.com/morluto/rea/commit/efbc25fbea1b6ade2a284a4916644c791f83bf49)), closes [#1846](https://github.com/morluto/rea/issues/1846)
* **android:** reject malformed UTF-8 at the JADX protocol boundary ([fe8d0bd](https://github.com/morluto/rea/commit/fe8d0bdd36fe12bc0836468d1a702a34fbdc219b))
* **apktool:** accept --no-strings CLI option ([#2707](https://github.com/morluto/rea/issues/2707)) ([11f8b76](https://github.com/morluto/rea/commit/11f8b7625788d747eaadd59af6efdf88587ed058))
* **apktool:** name host requirements in configuration refusals ([#1946](https://github.com/morluto/rea/issues/1946)) ([7cdb798](https://github.com/morluto/rea/commit/7cdb7981c3612bc42bd0153fddd9c33b639918ed))
* **apktool:** run Windows batch launchers through cmd.exe ([#1777](https://github.com/morluto/rea/issues/1777)) ([ae95e68](https://github.com/morluto/rea/commit/ae95e68ba5fa95cf2ec0453b50ffd217cabf5359)), closes [#1774](https://github.com/morluto/rea/issues/1774)
* **apple:** decode UIKit keyed-archive outlet connections ([8d4d631](https://github.com/morluto/rea/commit/8d4d631606c701aac150cf009d3b80ae4a7ae7d5))
* **apple:** derive dyld environment findings from each traced slice ([#2733](https://github.com/morluto/rea/issues/2733)) ([52d4ce6](https://github.com/morluto/rea/commit/52d4ce6ced1c3b6ae7570a5df7f4499cd65a1182)), closes [#2661](https://github.com/morluto/rea/issues/2661)
* **apple:** join recognized keys from NIBArchive field maps ([#2248](https://github.com/morluto/rea/issues/2248)) ([fd985db](https://github.com/morluto/rea/commit/fd985dbc5b9435918732c0cc1825060af2a3a47e))
* **apple:** limit already-loaded reuse to rpath dependencies ([#2292](https://github.com/morluto/rea/issues/2292)) ([60f771a](https://github.com/morluto/rea/commit/60f771a11e9889ce69cce6ad631b6b186ac742dd))
* **apple:** preflight XML plist nesting before keyed archive decoding ([#2161](https://github.com/morluto/rea/issues/2161)) ([7711b00](https://github.com/morluto/rea/commit/7711b0000592aa80c7b0412004150313f78feabc))
* **apple:** preserve archive signatures and metadata byte identity ([dc038d1](https://github.com/morluto/rea/commit/dc038d16bba9046afd28970de9c47f6259a43524))
* **apple:** preserve ARM64E X1 architecture identity ([f598bab](https://github.com/morluto/rea/commit/f598bab488e38c378d69346fa3c106f9e18529ed))
* **apple:** preserve slice identity and macOS boundary diagnostics ([#1772](https://github.com/morluto/rea/issues/1772)) ([5c29565](https://github.com/morluto/rea/commit/5c2956587a1b0d431f72e10df07a1a6363fa066c))
* **apple:** validate build-version tool records within cmdsize ([#1784](https://github.com/morluto/rea/issues/1784)) ([817aef4](https://github.com/morluto/rea/commit/817aef406861c2ce3f87ad3c912ea6c235bcdf12))
* **artifacts:** authenticate file subjects across provider and view boundaries ([3c57e1a](https://github.com/morluto/rea/commit/3c57e1a8b21b379fca55032d177078e1d8426e64))
* **artifacts:** bind directory reads to captured file state ([918713a](https://github.com/morluto/rea/commit/918713af376e307ecf24d75e6a0dd9d1b544545b))
* **artifacts:** bound archive decompression by source size ([#2144](https://github.com/morluto/rea/issues/2144)) ([c59fbf5](https://github.com/morluto/rea/commit/c59fbf5e9c3b856d042f02cb18ad4de077eaeab5))
* **artifacts:** close mounted directory resources before DMG detach ([2260887](https://github.com/morluto/rea/commit/2260887f2092be6a3d9c79509e4ae4a3e52fce7f))
* **artifacts:** compare leaf inventories using explicit coverage ([ed485a3](https://github.com/morluto/rea/commit/ed485a3dc8cc3a4baf8cabe01acb7aa0490b73b3))
* **artifacts:** compare package signatures without ASCII folding ([#2069](https://github.com/morluto/rea/issues/2069)) ([75a4a14](https://github.com/morluto/rea/commit/75a4a14fa05d352762ffaefd3cd2e16515842b03))
* **artifacts:** count a changed partial-inventory root once ([#1854](https://github.com/morluto/rea/issues/1854)) ([ee5275c](https://github.com/morluto/rea/commit/ee5275cd9fae8db64f945a0c8b59e1c19ac402c3)), closes [#1844](https://github.com/morluto/rea/issues/1844)
* **artifacts:** distinguish Java classes from universal Mach-O ([921bb95](https://github.com/morluto/rea/commit/921bb9575505065f6cfd57b14d97d80d0f238546)), closes [#2317](https://github.com/morluto/rea/issues/2317)
* **artifacts:** honor cancellation at the output publication boundary ([e7882b3](https://github.com/morluto/rea/commit/e7882b34f4ac4a26f903c5f49264acc2c19bb562))
* **artifacts:** mark Windows host executables by extension ([#1823](https://github.com/morluto/rea/issues/1823)) ([91f843b](https://github.com/morluto/rea/commit/91f843bf38b0e4ec8576f41f3140488b4ee8ff32)), closes [#1819](https://github.com/morluto/rea/issues/1819)
* **artifacts:** mark Windows launchable files as host executables ([#1824](https://github.com/morluto/rea/issues/1824)) ([e8491e0](https://github.com/morluto/rea/commit/e8491e023a255f790ae9850225084d7495e9ade0)), closes [#1819](https://github.com/morluto/rea/issues/1819)
* **artifacts:** preserve decode coverage and cleanup ownership ([a73a55f](https://github.com/morluto/rea/commit/a73a55f53cc2b2e5b7618effe6ecb5cee6e0cee2))
* **artifacts:** preserve emitted ZIP entry identity ([e36c82b](https://github.com/morluto/rea/commit/e36c82becdd613787bcc59903f8b5fe14eb2f105))
* **artifacts:** preserve inventory uncertainty in application projections ([d8ceb3f](https://github.com/morluto/rea/commit/d8ceb3f276a65c181b146f68c0729ff4b6f9fa61))
* **artifacts:** preserve NIB field and class identities ([#2106](https://github.com/morluto/rea/issues/2106)) ([f8e9579](https://github.com/morluto/rea/commit/f8e95799f941e9e55e0d47841896b401bab67d29))
* **artifacts:** preserve physical ZIP member identities ([#1865](https://github.com/morluto/rea/issues/1865)) ([bcd350c](https://github.com/morluto/rea/commit/bcd350c365030a5d210208c0a8dc238302c43303)), closes [#1860](https://github.com/morluto/rea/issues/1860)
* **artifacts:** preserve ZIP identities and partial managed metadata ([74d7da6](https://github.com/morluto/rea/commit/74d7da6ced0f869936ee0316b00616931b088066))
* **artifacts:** read ASCII control bytes in legacy ZIP names as ASCII ([#2365](https://github.com/morluto/rea/issues/2365)) ([9928f35](https://github.com/morluto/rea/commit/9928f357fb9c420e6384e155d74586dfe37d4533))
* **artifacts:** read every ZIP package family extraction is asked to open ([#2493](https://github.com/morluto/rea/issues/2493)) ([7017c4d](https://github.com/morluto/rea/commit/7017c4d68cab49aed998283e09069d9d791e1b0b))
* **artifacts:** reject malformed serialized keys and integer values ([9435c7a](https://github.com/morluto/rea/commit/9435c7aae279ba184937c0dfeb713fd574ba3f29))
* **artifacts:** retain ASAR resources through cleanup failures ([eb97da3](https://github.com/morluto/rea/commit/eb97da33881e3d1472d97a7d788ee35944fb6b07))
* **artifacts:** retain read cleanup ownership through CLI and MCP shutdown ([b955b82](https://github.com/morluto/rea/commit/b955b825fc002a4bb408f2910b78768c566c2cd1))
* **artifacts:** retry transient DMG verification refusals ([bb54082](https://github.com/morluto/rea/commit/bb540826d65316c164d2de8ba4ef10f81a8ad073))
* **browser:** bound CDP discovery response size and lifetime ([951b095](https://github.com/morluto/rea/commit/951b095e11adf9faa9c4a27e212260060aa6b095))
* **browser:** compare only observed HTTP response facts ([486b567](https://github.com/morluto/rea/commit/486b56724c4eaba50719177bcb9eae84d6907297))
* **browser:** compare PNG IHDR bytes without ASCII folding ([#2099](https://github.com/morluto/rea/issues/2099)) ([fdd8f82](https://github.com/morluto/rea/commit/fdd8f826023c568937d237c2f97d855de283a9b8))
* **browser:** exclude sensitive binary lengths from historical captures ([#2477](https://github.com/morluto/rea/issues/2477)) ([68ecf3d](https://github.com/morluto/rea/commit/68ecf3df8c666fdbe1ced8089c64340af614d1c3))
* **browser:** isolate script URLs with empty query or fragment ([#2074](https://github.com/morluto/rea/issues/2074)) ([9b780c4](https://github.com/morluto/rea/commit/9b780c47bca039939dd5f73b78541f3ea566a7dd))
* **browser:** keep elapsed capture timing independent of wall-clock changes ([6bd2b28](https://github.com/morluto/rea/commit/6bd2b28161411f70ed5dc72e42c2736a1aab758b))
* **browser:** preserve attached service-worker policies and ownership ([221c856](https://github.com/morluto/rea/commit/221c85648108e9846d9920b0964dafa7f108d581))
* **browser:** preserve body API provenance through script export ([966f7cc](https://github.com/morluto/rea/commit/966f7cc84b4a8bb0043641cb64d7a7aaf8715e05))
* **browser:** preserve uncertainty in malformed network observations ([a8d4984](https://github.com/morluto/rea/commit/a8d4984404b611e60b88bc42289c0ba07eee4578))
* **browser:** redact encoded declared secrets consistently ([f09bb8a](https://github.com/morluto/rea/commit/f09bb8a99773f79c7306b8799c86f856fe2c0d4b))
* **browser:** report mismatched event family selection as unknown in capture comparison ([#2239](https://github.com/morluto/rea/issues/2239)) ([0043d9b](https://github.com/morluto/rea/commit/0043d9b134c0a353e8bf85c0bde4492275f1f720))
* **browser:** retain effective source-map response urls ([062723e](https://github.com/morluto/rea/commit/062723e44e937c1e1df865e9ae16184b41e70ae8))
* **browser:** retain module resolver cleanup ownership ([a82e37c](https://github.com/morluto/rea/commit/a82e37c8065cc200a0ca3e14e7c4f85e4e468580))
* **build:** preserve LF text checkouts on Windows ([#1807](https://github.com/morluto/rea/issues/1807)) ([031d778](https://github.com/morluto/rea/commit/031d7781f0f6c99e5f14e67f56cb89a88d6af3eb))
* **build:** preserve resource controls in strict Turbo tasks ([#1809](https://github.com/morluto/rea/issues/1809)) ([2e22330](https://github.com/morluto/rea/commit/2e223300375101558f90b6e78ef9612c55626673))
* **build:** resolve rolldown from the npm registry in package-lock.json ([#1986](https://github.com/morluto/rea/issues/1986)) ([f889a69](https://github.com/morluto/rea/commit/f889a69fb9efe42dd0120c96ff44b4298c1c7c6c))
* **bun:** report the real runtime and enforce worker memory budgets on Windows ([#1730](https://github.com/morluto/rea/issues/1730)) ([66c2157](https://github.com/morluto/rea/commit/66c21572defc97fe277cad49d3173bd4073d1f58))
* **call-path:** preserve relationship coverage from analysis ([#2001](https://github.com/morluto/rea/issues/2001)) ([ed6af7e](https://github.com/morluto/rea/commit/ed6af7eb8cf052c52fec8bfdc5d509377cb30a1a))
* **captures:** name the violated HAR schema constraint ([#2708](https://github.com/morluto/rea/issues/2708)) ([b69d20a](https://github.com/morluto/rea/commit/b69d20a300dba57783a810146f25dacb69f9b8a4))
* **checks:** order artifact-backed lint after compilation ([0d5033a](https://github.com/morluto/rea/commit/0d5033ad12e12abe78fc3c0ba81622b410d22aa1))
* **ci:** align FAT fixtures and consolidate duplicated workflow helpers ([a0317cf](https://github.com/morluto/rea/commit/a0317cf732b3addefad5ac3b234db1e044e3351d))
* **ci:** allow cold Chrome startup in runtime fixtures ([f941968](https://github.com/morluto/rea/commit/f941968eb1860dd41efc2a5201b35cecca42301d))
* **ci:** ignore generated website translation staging ([#2511](https://github.com/morluto/rea/issues/2511)) ([1883149](https://github.com/morluto/rea/commit/18831491cfb5cb98abbffce0cb04dae12a90fe28)), closes [#2508](https://github.com/morluto/rea/issues/2508)
* **ci:** keep cancellation ownership in the calling workflow ([db3c9f0](https://github.com/morluto/rea/commit/db3c9f084bacad34f620ba678924564e20f441f9))
* **ci:** reject incomplete verification plans ([bd2c202](https://github.com/morluto/rea/commit/bd2c202dd930548194cdbc55c27a797f22279db9))
* **ci:** stop aggregate jobs when verification is cancelled ([2e45b4d](https://github.com/morluto/rea/commit/2e45b4d7a5b3228fe8b283def6f047797561bee1))
* **cli:** await browser scenario cleanup on interrupts ([aaeadf2](https://github.com/morluto/rea/commit/aaeadf2d4c4e3f0d0581af2dca33c5bfdc2bcb0e))
* **cli:** normalize empty Windows profile before imports ([#2482](https://github.com/morluto/rea/issues/2482)) ([fa4b828](https://github.com/morluto/rea/commit/fa4b8288655188426222773ead2ccbdaf76334fd))
* **cli:** preserve failure semantics across full-output formats ([570c40c](https://github.com/morluto/rea/commit/570c40cde55d31e455ef9bc484b7b908658ded58))
* **cli:** report an invalid REA_LOG_LEVEL as configuration ([#2140](https://github.com/morluto/rea/issues/2140)) ([3159732](https://github.com/morluto/rea/commit/315973259732544a597b8b17d25c69735910e72e)), closes [#2135](https://github.com/morluto/rea/issues/2135)
* **cli:** retain browser and Inspector input correction details ([#2230](https://github.com/morluto/rea/issues/2230)) ([916ac97](https://github.com/morluto/rea/commit/916ac97bf9f7b976982e438ceb856a9692576cb1))
* **cli:** retain correction details across adjacent browser commands ([#2294](https://github.com/morluto/rea/issues/2294)) ([bb26622](https://github.com/morluto/rea/commit/bb266226707135ad9f181e6c3afd6af0ef62fb22))
* **cli:** retry retained browser scenario cleanup ([4daea94](https://github.com/morluto/rea/commit/4daea941bfeeb426886f2d048aed7f3cb2dbf97b))
* **cli:** reuse firmware and web module input constraints ([48c352b](https://github.com/morluto/rea/commit/48c352b6bfa34c820a530272cd36600158c37b1e))
* **config:** reject NUL bytes in Hopper loader arguments ([#2474](https://github.com/morluto/rea/issues/2474)) ([a07ceae](https://github.com/morluto/rea/commit/a07ceaec36346a7c73e66efe35c8af4654029c84))
* **contracts:** mark foreign execution and device mutation destructive ([#1904](https://github.com/morluto/rea/issues/1904)) ([0b7fc40](https://github.com/morluto/rea/commit/0b7fc40eeef03b06de08d886572c2862331d8947))
* **contracts:** reject OS-invalid local paths in binary and artifact tools ([#1925](https://github.com/morluto/rea/issues/1925)) ([976c2c0](https://github.com/morluto/rea/commit/976c2c083eee1fdd9e89a1a720f768a6ee6df277))
* **dev:** preserve command arguments in exclusive runners ([fd95880](https://github.com/morluto/rea/commit/fd9588089d089e7cb02d12178e34407d6f889f22)), closes [#2319](https://github.com/morluto/rea/issues/2319)
* **docs:** synchronize disclaimer links across translations ([d864ad5](https://github.com/morluto/rea/commit/d864ad54717d92fe4b512ce1170717d9904c1f00))
* **docs:** validate translated README website destinations ([c9136ee](https://github.com/morluto/rea/commit/c9136eef716aa8d32e0301189fbf5a511ff62d5d))
* **domain:** preserve derived byte counts and uncertain identity ([7a2cd1e](https://github.com/morluto/rea/commit/7a2cd1ea0a63e8c44166860b7ee9c7a1aa43492b))
* **domain:** preserve retryable capability failures through redaction ([#2494](https://github.com/morluto/rea/issues/2494)) ([d73ed6f](https://github.com/morluto/rea/commit/d73ed6fce24f6d1372c80319cd41d29d66666159))
* **electron:** attribute unresolved paths to selected inputs ([b7169c8](https://github.com/morluto/rea/commit/b7169c8b747cbc032913b31f008a3b81c77b6219)), closes [#2272](https://github.com/morluto/rea/issues/2272)
* **elf:** centralize fixed header validation across consumers ([34a4bb6](https://github.com/morluto/rea/commit/34a4bb6508006fa113463ef20ed799a2994e9ee8))
* **errors:** stop recommending retries for non-retryable failures ([#1869](https://github.com/morluto/rea/issues/1869)) ([fb94d9d](https://github.com/morluto/rea/commit/fb94d9dde0ccda83ae520ac6fb22b1d283562cf3))
* **evaluation:** authenticate delivered Evidence citations ([844fa52](https://github.com/morluto/rea/commit/844fa52772c1533f7358a71a764da780503775a3))
* **evaluation:** share completed transcript facts across graders ([1447fd2](https://github.com/morluto/rea/commit/1447fd2f990c5d9820337dfd4b8a170f8cad17cd))
* **evidence:** admit bundle imports across session transitions ([#2303](https://github.com/morluto/rea/issues/2303)) ([1230381](https://github.com/morluto/rea/commit/12303812a8d70f198c99c7aeb0df3aaa8166543c))
* **evidence:** authenticate identities and retain extraction provenance ([6bae774](https://github.com/morluto/rea/commit/6bae774328724403255590aa2ddad4d7a2aa727f))
* **evidence:** enforce managed identity and cancel bundle reads ([3bcc68c](https://github.com/morluto/rea/commit/3bcc68c037e9415c78093cb286069c1fcca6a2f8))
* **evidence:** normalize unknown revisions and preserve mutation intent ([#1733](https://github.com/morluto/rea/issues/1733)) ([2165045](https://github.com/morluto/rea/commit/21650455a9c90a26e725c3505a3c17054cc4516c)), closes [#1723](https://github.com/morluto/rea/issues/1723) [#1724](https://github.com/morluto/rea/issues/1724)
* **evidence:** preserve call coverage in function comparisons ([049132a](https://github.com/morluto/rea/commit/049132ab43fa962d31774f2b258c15a71217148b)), closes [#2222](https://github.com/morluto/rea/issues/2222)
* **evidence:** preserve literal keys and response body ownership ([d1dcebc](https://github.com/morluto/rea/commit/d1dcebce2c0820469ed63e33497a14258971b710))
* **evidence:** preserve trace uncertainty and partial provider observations ([e997085](https://github.com/morluto/rea/commit/e9970855ba16ba711816ac30212c5ca17ae085cd))
* **evidence:** reject NUL bytes in subject and snapshot paths ([#1873](https://github.com/morluto/rea/issues/1873)) ([fa9a7e5](https://github.com/morluto/rea/commit/fa9a7e5ec026c5a4e8426f0f36c2c4555be9c9f0))
* **evidence:** reject unsupported metadata before authentication ([acf75e6](https://github.com/morluto/rea/commit/acf75e6700ace28bb1db25e54e56673ac15e0ed3))
* **evidence:** retain unreadable PE and Darwin process facts ([4a4d36f](https://github.com/morluto/rea/commit/4a4d36f881861bbab3c734b0dd459f805766caec))
* **evidence:** validate closure projections and browser body coverage ([aada0ca](https://github.com/morluto/rea/commit/aada0ca8126bf14c60750a4ccd4a79667770a1e4))
* **filesystem:** enforce symlink and decoded-text admission ([ec0b081](https://github.com/morluto/rea/commit/ec0b081371f118e6e5e042fbef3479359c672ddb))
* **firmware:** name missing extractor dependencies in partial extraction ([#1879](https://github.com/morluto/rea/issues/1879)) ([1769417](https://github.com/morluto/rea/commit/176941700118cc33c86e07373cd6950f30548386))
* **formats:** preserve high bits in Windows signature checks ([ac57c88](https://github.com/morluto/rea/commit/ac57c88efe64c3125850b54d3d833227e6b8ec51))
* **frida:** name the argument each Frida input error rejects ([#2217](https://github.com/morluto/rea/issues/2217)) ([86de439](https://github.com/morluto/rea/commit/86de439ba4c9297f721abc60c715fe30fb6ff3df)), closes [#2213](https://github.com/morluto/rea/issues/2213)
* **frida:** reject an unreadable script file before starting a session ([#2211](https://github.com/morluto/rea/issues/2211)) ([c9aecde](https://github.com/morluto/rea/commit/c9aecde580d26d4e7745c2623a4efa327a27367e))
* **ghidra:** derive installation admission from Java property semantics ([2187e16](https://github.com/morluto/rea/commit/2187e16f149a7a7b512a4cb45d5f468cc04091fe))
* **ghidra:** encode Java probe output as UTF-8 ([#2206](https://github.com/morluto/rea/issues/2206)) ([59be866](https://github.com/morluto/rea/commit/59be866719e8fe8ec060515044d427a3b0a57649))
* **ghidra:** keep JVM warnings out of LaunchSupport output ([#1698](https://github.com/morluto/rea/issues/1698)) ([e20994a](https://github.com/morluto/rea/commit/e20994a8ea10609d420e958bd8a1268b0bcd8899))
* **ghidra:** recover stale clients on same-target reopen ([#2014](https://github.com/morluto/rea/issues/2014)) ([4573e8c](https://github.com/morluto/rea/commit/4573e8cf35de822bfc5feabcc89ec3717c247c8c))
* **ghidra:** report progress while startup is pending ([#1877](https://github.com/morluto/rea/issues/1877)) ([29d2624](https://github.com/morluto/rea/commit/29d2624b3475586356c27f518bf6b823030268fb))
* **ghidra:** resolve file offsets in decompile ([#2101](https://github.com/morluto/rea/issues/2101)) ([2995ec6](https://github.com/morluto/rea/commit/2995ec6910ead327829378f5ab1b9172ecce33fc))
* **ghidra:** retain launcher command outcome in launch failure diagnostics ([#2011](https://github.com/morluto/rea/issues/2011)) ([d4e8ef7](https://github.com/morluto/rea/commit/d4e8ef74999666cdb735b49eb8fea367f8f86489))
* **ghidra:** share Java properties semantics with PSP extension admission ([b953f96](https://github.com/morluto/rea/commit/b953f96c0aae27e1876c317a8f7db1d6f292b8ff))
* **go:** accept file-backed unmapped Mach-O debug segments ([7fb097d](https://github.com/morluto/rea/commit/7fb097d72c9931dd5dfae481ad2b9d3bb6029f15))
* honor empty URL query and fragment markers ([#2091](https://github.com/morluto/rea/issues/2091)) ([af29247](https://github.com/morluto/rea/commit/af29247a7873ba8badec204a4a59f9a2b3a880b6))
* **hopper:** let failed one-shot commands exit while retaining a lease ([#1763](https://github.com/morluto/rea/issues/1763)) ([e8528ce](https://github.com/morluto/rea/commit/e8528cedcdfb1ca1d769304afdc353936e8e0d70))
* **hopper:** preserve observed process failures across requests ([#1927](https://github.com/morluto/rea/issues/1927)) ([601814e](https://github.com/morluto/rea/commit/601814ed0e4a0c57d6e045da0e6cf8ee2f29bb7f))
* **hopper:** preserve startup failures and recover denied launches ([069308b](https://github.com/morluto/rea/commit/069308bbe6d73b1d87038b43c9121658325042a9)), closes [#1808](https://github.com/morluto/rea/issues/1808)
* **hopper:** stop Linux demo children when the REA owner exits ([#2499](https://github.com/morluto/rea/issues/2499)) ([c105391](https://github.com/morluto/rea/commit/c105391db53e2e2ab8b8624d5f3870c14aae3637))
* **hopper:** wait for late bridges during cleanup ([#1866](https://github.com/morluto/rea/issues/1866)) ([cf7477e](https://github.com/morluto/rea/commit/cf7477eb4642bc07228fa33ccc4c87656e6e3003))
* **ida:** declare GUI navigation for attached legacy analysis ([#1764](https://github.com/morluto/rea/issues/1764)) ([cf69207](https://github.com/morluto/rea/commit/cf692072b56903b60426e4b301b3c48fd31ee0fd)), closes [#1712](https://github.com/morluto/rea/issues/1712)
* **ida:** preserve native coverage and verify returned values ([#2324](https://github.com/morluto/rea/issues/2324)) ([37b7550](https://github.com/morluto/rea/commit/37b7550ecc916acc8619fa72e5c05af45702733f))
* **ida:** preserve non-x86 call edges and workflow uncertainty ([#1906](https://github.com/morluto/rea/issues/1906)) ([0ae38e1](https://github.com/morluto/rea/commit/0ae38e1e31414c67a3be09231d208f0726c035f8))
* **ida:** recover cleanup after worker sessions are reaped ([12fa8fd](https://github.com/morluto/rea/commit/12fa8fd9f986dbd4ff58165c3ec71a46dc8a8a3b))
* **ida:** report a procedure IDA cannot resolve as invalid input ([#1785](https://github.com/morluto/rea/issues/1785)) ([d3e9ee5](https://github.com/morluto/rea/commit/d3e9ee5505251f760666bb8e1e6c295b6d6aedb2))
* **ida:** stop the private input of a universal Mach-O at the end of the selected slice ([#2291](https://github.com/morluto/rea/issues/2291)) ([c6acd19](https://github.com/morluto/rea/commit/c6acd19b6d842699ab807d9526dc762c743686f4))
* **ida:** thin the selected FAT slice into the private headless input ([#2201](https://github.com/morluto/rea/issues/2201)) ([10400fd](https://github.com/morluto/rea/commit/10400fdb57e4226c5ce15af17968a664f35498ce)), closes [#2139](https://github.com/morluto/rea/issues/2139)
* **ida:** wait out a transient busy worker health in headless sessions ([#1778](https://github.com/morluto/rea/issues/1778)) ([0bdc7a4](https://github.com/morluto/rea/commit/0bdc7a40ac171a05971b287f2aab0376e2f14358))
* **install:** describe automatic setup and guard truncated downloads ([#1902](https://github.com/morluto/rea/issues/1902)) ([d802304](https://github.com/morluto/rea/commit/d802304c542b20bc86e22cc492718e89a8a5579b))
* **install:** honor redirected setup diagnostics ([8b18171](https://github.com/morluto/rea/commit/8b181716aca2cb467be499cba226abfb5ae19141))
* **io:** consolidate managed snapshots and retain cleanup ownership ([98d8470](https://github.com/morluto/rea/commit/98d8470d36cfbf7cd25820a4ed430aebae20c0c5))
* **io:** retain cleanup ownership and honor cancellation ([d8c76b6](https://github.com/morluto/rea/commit/d8c76b629639bda4405afa9ae2f7676b694f2f10))
* **javascript:** apply loader-owned module lookup rules ([c0904b4](https://github.com/morluto/rea/commit/c0904b4c1aabfccf2f0ab12f44960a0a1fbbbfae))
* **javascript:** bound self-alias mutation paths that never prune ([e365f37](https://github.com/morluto/rea/commit/e365f3715f0e9856218f0b2878bfe2144c821d9a))
* **javascript:** bound value evaluation through nested closure aliases ([#1941](https://github.com/morluto/rea/issues/1941)) ([d59ab6d](https://github.com/morluto/rea/commit/d59ab6d977d5b9376af2fdd141b4f65460bb9a9f))
* **javascript:** centralize module syntax and separate erased type dependencies ([1b95302](https://github.com/morluto/rea/commit/1b953021eadf4cac09e20be388d79536903633b3))
* **javascript:** distinguish file URL paths from query identity ([f43fc02](https://github.com/morluto/rea/commit/f43fc0247527aebb8efc7d9db04276fe2d489323))
* **javascript:** follow actual receivers in nested intrinsic calls ([#1912](https://github.com/morluto/rea/issues/1912)) ([9d8e42b](https://github.com/morluto/rea/commit/9d8e42bdd6f1bf8a984da1e06e1d76a1dbb9a7ed))
* **javascript:** follow bundler namespace interop for Electron APIs ([#1827](https://github.com/morluto/rea/issues/1827)) ([6ac5c36](https://github.com/morluto/rea/commit/6ac5c36503492d3b6da4fcf2a5dee87aff60fc8c))
* **javascript:** honor package exports before sibling files ([3ad5782](https://github.com/morluto/rea/commit/3ad5782dfdc8864879a887dc63fef682ffeb5839))
* **javascript:** index repeated graph observations before final sorting ([6d5d71f](https://github.com/morluto/rea/commit/6d5d71fab5e135c687f7aa2f797da19b4c2a1c1e)), closes [#1748](https://github.com/morluto/rea/issues/1748)
* **javascript:** keep TypeScript substitution out of runtime paths ([f8513a4](https://github.com/morluto/rea/commit/f8513a42dc44451a621127f06edde5d38bc93f69))
* **javascript:** normalize package URLs before array selection ([#1916](https://github.com/morluto/rea/issues/1916)) ([369cd3d](https://github.com/morluto/rea/commit/369cd3dfdc47709cc34d01c50f645a885c70ded7))
* **javascript:** parse explicit CommonJS wrapper syntax ([#2475](https://github.com/morluto/rea/issues/2475)) ([6a04f92](https://github.com/morluto/rea/commit/6a04f922e16ad4d171ba0c07e32238efdaf43e2d))
* **javascript:** preserve and resolve complete source-map URLs ([#1788](https://github.com/morluto/rea/issues/1788)) ([ce845ba](https://github.com/morluto/rea/commit/ce845ba428e9b23d6b74eae1e25e17c37532adac))
* **javascript:** preserve cancellation through recovery publication cleanup ([eaa9616](https://github.com/morluto/rea/commit/eaa961658578522cecfbba7c7be1acf1c64da65a))
* **javascript:** preserve execution and invocation provenance ([b8fab74](https://github.com/morluto/rea/commit/b8fab7429771bb17a7ca0297661de3c9935212af))
* **javascript:** preserve graphs within parent capacity ([#1921](https://github.com/morluto/rea/issues/1921)) ([0485cd3](https://github.com/morluto/rea/commit/0485cd3bce29c977bae05a4fdce5436f3e3d7913)), closes [#1911](https://github.com/morluto/rea/issues/1911)
* **javascript:** preserve import-equals declaration and export evidence ([#2062](https://github.com/morluto/rea/issues/2062)) ([86fab11](https://github.com/morluto/rea/commit/86fab11fbe000a04c0810699419e99e50476e424))
* **javascript:** preserve loader-specific package ancestry ([9f67e44](https://github.com/morluto/rea/commit/9f67e44e5d451d705bae9a579ad9802d5c9f097c))
* **javascript:** preserve namespace re-export relationships ([#1833](https://github.com/morluto/rea/issues/1833)) ([37a9eb9](https://github.com/morluto/rea/commit/37a9eb9d65a088209f962e5094f6bc67eacbbaf9))
* **javascript:** preserve package and source-map boundary identity ([#1939](https://github.com/morluto/rea/issues/1939)) ([14935e4](https://github.com/morluto/rea/commit/14935e4d470be318fdee442d279d6acb7d441119))
* **javascript:** preserve POSIX runtime path case on Windows ([#2422](https://github.com/morluto/rea/issues/2422)) ([1a1b67c](https://github.com/morluto/rea/commit/1a1b67c8885e9ff33f7a06ed2e027ebfdb83e084))
* **javascript:** preserve TypeScript loader and dependency semantics ([e4532b5](https://github.com/morluto/rea/commit/e4532b537663026c3e5b784380eecd009c959a36))
* **javascript:** preserve unknown module-alias provenance ([c7a890d](https://github.com/morluto/rea/commit/c7a890d12987d9b2d496119c1fbf1a88c0fc298e))
* **javascript:** prove namespace interop before attributing Electron APIs ([#1857](https://github.com/morluto/rea/issues/1857)) ([9b3e598](https://github.com/morluto/rea/commit/9b3e59804db71072ed023f109342f96063c57b45))
* **javascript:** report available parent result budget in coverage limits ([#1915](https://github.com/morluto/rea/issues/1915)) ([72489a9](https://github.com/morluto/rea/commit/72489a9445299f695d793f004fed88e1bac0a557))
* **javascript:** report JSON status for imported package manifests ([914ba83](https://github.com/morluto/rea/commit/914ba833ded5f8604345de4e4f6d306de0a38c4e))
* **javascript:** resolve bundled exports by binding identity ([5ab0cbb](https://github.com/morluto/rea/commit/5ab0cbb48ea5a4f11542eac44479b9af48f1dabd)), closes [#2171](https://github.com/morluto/rea/issues/2171)
* **javascript:** resolve module-relative Worker URL references ([#2104](https://github.com/morluto/rea/issues/2104)) ([653067d](https://github.com/morluto/rea/commit/653067d799591ce2b8fdc84e99c160ac17e2860f))
* **javascript:** resolve relative ESM URLs as exact files ([783fcb0](https://github.com/morluto/rea/commit/783fcb06a4bd0444be214822fed60a3786932782))
* **javascript:** retain compiler-dependent module resolution facts ([c923fb0](https://github.com/morluto/rea/commit/c923fb0dc9462e302b84a15770131ff899f89455))
* **javascript:** retain completed facts through cancellation ([#1934](https://github.com/morluto/rea/issues/1934)) ([b6651cb](https://github.com/morluto/rea/commit/b6651cb99fba8fec3fd3efcb6794ae32b8a1525d))
* **javascript:** retain distinct chunk registration callback metadata ([#2737](https://github.com/morluto/rea/issues/2737)) ([0cf224e](https://github.com/morluto/rea/commit/0cf224e76214abbcf47274aa936d1b5cad28db2f))
* **javascript:** retain guards without duplicating observed IPC calls ([42b0ed1](https://github.com/morluto/rea/commit/42b0ed126df7d5b908ac369715378d5e519051b7))
* **javascript:** retain unbound CommonJS semantic imports ([3a17882](https://github.com/morluto/rea/commit/3a17882cc469515e562df9ecc7d154f30a5a9205))
* **javascript:** retain wrapped isolated-world bridge exposures ([1c37914](https://github.com/morluto/rea/commit/1c37914306b3ff257d7a0360f11c9bf2a3f2cac7))
* **javascript:** share source-map resolution and preserve path uncertainty ([3ae826c](https://github.com/morluto/rea/commit/3ae826ceba6f25b08bce50bcb51001209bc16b57))
* **javascript:** share TypeScript emitted import resolution ([1dfb933](https://github.com/morluto/rea/commit/1dfb933e47db0156cf1cb61f28715b81e740c57b))
* **javascript:** trace wrapped Electron IPC paths in large bundles ([#1874](https://github.com/morluto/rea/issues/1874)) ([87b9793](https://github.com/morluto/rea/commit/87b9793c53caf70a9f3a516214856899ca2d7cf8))
* **jeb:** confine an invalid REA_JEB_MCP_URL to the JEB tools ([#2145](https://github.com/morluto/rea/issues/2145)) ([df2c08b](https://github.com/morluto/rea/commit/df2c08b546d4f40103e11b8b31dbfc3d41ee687b)), closes [#2143](https://github.com/morluto/rea/issues/2143)
* **managed:** admit ReadyToRun images built for non-Windows hosts ([#1985](https://github.com/morluto/rea/issues/1985)) ([0d11f71](https://github.com/morluto/rea/commit/0d11f71e0dc5cf95d3bea093355f7edc09089a68))
* **managed:** bind row budget charges to metadata identities ([#1897](https://github.com/morluto/rea/issues/1897)) ([23d50ea](https://github.com/morluto/rea/commit/23d50ea3bec186b4459d36c04854a92276accdf1)), closes [#1878](https://github.com/morluto/rea/issues/1878)
* **managed:** bound transient CIL by decoded instruction count ([#1822](https://github.com/morluto/rea/issues/1822)) ([554cb45](https://github.com/morluto/rea/commit/554cb45f7ff967884c9d1fdf706c20803d76a6df))
* **managed:** charge retained member facts instead of decoding bounds ([#1768](https://github.com/morluto/rea/issues/1768)) ([3078729](https://github.com/morluto/rea/commit/30787297d49bf93e67f62a9ec42f896188d57a20))
* **managed:** charge transient CIL by decoded instructions ([554cb45](https://github.com/morluto/rea/commit/554cb45f7ff967884c9d1fdf706c20803d76a6df))
* **managed:** decode generic, array, fnptr, and modifier signatures ([#1835](https://github.com/morluto/rea/issues/1835)) ([1f531fc](https://github.com/morluto/rea/commit/1f531fc257f71731624cbd6b595208adacf59108))
* **managed:** distinguish .NET Standard targets from runtimes ([81dc64e](https://github.com/morluto/rea/commit/81dc64e3cd13dc8bfcac8898386717ece9721da1))
* **managed:** honor metadata ExtraData before table decoding ([#1893](https://github.com/morluto/rea/issues/1893)) ([fb43dd9](https://github.com/morluto/rea/commit/fb43dd9716fe113effa14a03b603b72ca7d9097c))
* **managed:** index attribute constructor owners once per layout ([#1943](https://github.com/morluto/rea/issues/1943)) ([c2cf7dc](https://github.com/morluto/rea/commit/c2cf7dcb86e11350214f82e5c51de7ca6ee3d27a))
* **managed:** preserve negative zero in CIL operands ([#2437](https://github.com/morluto/rea/issues/2437)) ([6c816af](https://github.com/morluto/rea/commit/6c816af11975e8e801adb2924c4e3c116c72ebba))
* **managed:** report ExportedType rows in managed member inspection ([#1828](https://github.com/morluto/rea/issues/1828)) ([ed1519d](https://github.com/morluto/rea/commit/ed1519ddee02739f57136e356786f64c7f74fed0))
* **managed:** report malformed PE layouts as invalid input ([#2194](https://github.com/morluto/rea/issues/2194)) ([90e37f4](https://github.com/morluto/rea/commit/90e37f4152990be721784006a820a3794920213c))
* **managed:** resolve member ownership through pointer tables ([#1886](https://github.com/morluto/rea/issues/1886)) ([a95795c](https://github.com/morluto/rea/commit/a95795c6ed3cb149e03460f4c818ad12e0c670d9))
* **managed:** share first-match member list ownership ([#1955](https://github.com/morluto/rea/issues/1955)) ([7d92d93](https://github.com/morluto/rea/commit/7d92d93a550f3be221d7cf1da07c37ea1770e922))
* **managed:** share first-match member ownership indexes ([52e76c5](https://github.com/morluto/rea/commit/52e76c5d39b58d1685bb5e22bd40d19524dfa2cb))
* **managed:** validate CLI type selections with the member contract ([#2125](https://github.com/morluto/rea/issues/2125)) ([60259a9](https://github.com/morluto/rea/commit/60259a91d9a58cbd313ad7f3d321acc2deb65d2e)), closes [#2121](https://github.com/morluto/rea/issues/2121)
* **mcp:** cancel evidence imports before session mutation ([e5699d2](https://github.com/morluto/rea/commit/e5699d23b173bbd0b68e3ab177d0b7583d6df345)), closes [#1746](https://github.com/morluto/rea/issues/1746)
* **mcp:** express inspect_analysis_view selector constraints in the contract schema ([#1705](https://github.com/morluto/rea/issues/1705)) ([a8a9c4d](https://github.com/morluto/rea/commit/a8a9c4d8173b63fb42d343a8ca043927970ea4ce)), closes [#1695](https://github.com/morluto/rea/issues/1695)
* **mcp:** join owned doctor shutdown without signaling saved PIDs ([7fc0b5e](https://github.com/morluto/rea/commit/7fc0b5ec9c2532bbf789d7783b7a0cab6fc054d8))
* **mcp:** keep prompt completion within ready provider sessions ([07b36c1](https://github.com/morluto/rea/commit/07b36c148946f46b99a4ce04e55726e97d0ee03a))
* **mcp:** keep shared optional providers open across discarded probes ([#2205](https://github.com/morluto/rea/issues/2205)) ([7c9c1eb](https://github.com/morluto/rea/commit/7c9c1ebdb77662f6276ef2f7250e7df414bd03ca)), closes [#2195](https://github.com/morluto/rea/issues/2195)
* **mcp:** preserve target provenance on retained partial errors ([8fc2043](https://github.com/morluto/rea/commit/8fc2043eb8bcedbf34dce064f977e455438635d6))
* **mcp:** provide native evidence paging continuation ([1449e8e](https://github.com/morluto/rea/commit/1449e8e05138bf0c86d5fe6f4c93122b4b93b2b1)), closes [#1849](https://github.com/morluto/rea/issues/1849)
* **native:** accept universal binaries with LIB64 x86_64 slices ([#1982](https://github.com/morluto/rea/issues/1982)) ([4b653fe](https://github.com/morluto/rea/commit/4b653fe6ad1d25a2d9a69d5e93610380c0c09a34))
* **native:** clarify Mach-O offset origins and reject partial ELF tables ([8bd5ba2](https://github.com/morluto/rea/commit/8bd5ba2d234cbd9a5bb9dd5daeaa191c3a1cad24)), closes [#1973](https://github.com/morluto/rea/issues/1973)
* **native:** honor Mach-O chained pointer locations and library ordinals ([#1709](https://github.com/morluto/rea/issues/1709)) ([3b0ea59](https://github.com/morluto/rea/commit/3b0ea590fcc876fa017946f4cec009d653f02b38))
* **native:** name the ownership helper prerequisite in command failures ([#2506](https://github.com/morluto/rea/issues/2506)) ([20f51d7](https://github.com/morluto/rea/commit/20f51d7286b1759e42f99c0512c73230052dda86))
* **native:** name the retained pwntools process group in cleanup resources ([#2281](https://github.com/morluto/rea/issues/2281)) ([4303dcd](https://github.com/morluto/rea/commit/4303dcdcf68af4d71d777ffff75cbd4f17a8877d)), closes [#2274](https://github.com/morluto/rea/issues/2274)
* **native:** preserve literal line breaks in signing identifiers ([#2107](https://github.com/morluto/rea/issues/2107)) ([ec3db37](https://github.com/morluto/rea/commit/ec3db374574060fcc4fa6b215fceaea1bd4ca3f3))
* **native:** preserve universal Mach-O container identity ([b38429c](https://github.com/morluto/rea/commit/b38429cb6a0de8f4bb5ae01633e24abe2d7cde46))
* **native:** raise default PE resource entry budget to 16384 ([#1703](https://github.com/morluto/rea/issues/1703)) ([951c2aa](https://github.com/morluto/rea/commit/951c2aae3be1c357df9f23558d8c09680e64c2cb))
* **native:** rank Mach-O slices by parsed CPU subtype ([c02fb0e](https://github.com/morluto/rea/commit/c02fb0ea8055f98def2b12ad557aed0faefd3639))
* **native:** reject ambiguous otool load-command framing ([108b5f9](https://github.com/morluto/rea/commit/108b5f94254281639badd25b1f448cca35dc89a2))
* **native:** reject required otool fields truncated before their value ([#1995](https://github.com/morluto/rea/issues/1995)) ([adde8a8](https://github.com/morluto/rea/commit/adde8a80921b199ddd7105998f3760d345b468fc))
* **native:** retain failed command cleanup owners ([d9b4ae1](https://github.com/morluto/rea/commit/d9b4ae182d60dfd7fd5ba5bb539716413cbfdca6))
* **native:** retain failed pwntools process and runtime cleanup ([#1708](https://github.com/morluto/rea/issues/1708)) ([11fe5a1](https://github.com/morluto/rea/commit/11fe5a13f8aee87d0c0e036a9f497407e085fe48))
* **native:** retain signature snapshot cleanup ownership ([ebc22d9](https://github.com/morluto/rea/commit/ebc22d99d62225505c0a84f6e39823ba4ef2f829))
* **native:** select one slice when universal Mach-O slices share a CPU type ([#2040](https://github.com/morluto/rea/issues/2040)) ([96fc84f](https://github.com/morluto/rea/commit/96fc84f1fb7c8bc69f66cd899896f4de9061fead))
* order remaining outputs independently of host locale ([#1867](https://github.com/morluto/rea/issues/1867)) ([e4ac7bb](https://github.com/morluto/rea/commit/e4ac7bbfc2f8c631ba1c60f507d85253e122d4c8))
* **pcap:** name the host tool to configure in capability refusals ([#2507](https://github.com/morluto/rea/issues/2507)) ([bb96afd](https://github.com/morluto/rea/commit/bb96afdbe32f5318d57fcb00099191c5a7712f53))
* **pcap:** order evidence ties by code point instead of host locale ([#2509](https://github.com/morluto/rea/issues/2509)) ([f6556ad](https://github.com/morluto/rea/commit/f6556ad2bc102d80223f5b434136f546dbd2bbab))
* **plist:** decode formatted XML dictionary keys ([f1924cf](https://github.com/morluto/rea/commit/f1924cf9c1910d5d29d11e7bf634b51af8d738cf))
* **process:** accept a trace identifier the comparison result can represent ([#2488](https://github.com/morluto/rea/issues/2488)) ([2c6a4fa](https://github.com/morluto/rea/commit/2c6a4fac4ff3477ac14f3d780894c5b390880607))
* **process:** add an opt-in Swift module cache ([#2252](https://github.com/morluto/rea/issues/2252)) ([47a51b0](https://github.com/morluto/rea/commit/47a51b08f4d79f808c1b74de79c3852aa853df53))
* **process:** aggregate incomplete trace-spec diagnostics ([857a1b7](https://github.com/morluto/rea/commit/857a1b7b04da4488db2b224c270ad78998c6b6c7))
* **process:** align capture Evidence provider admission ([713db4d](https://github.com/morluto/rea/commit/713db4dc2665c964707cb0e937e8794cbe73534e))
* **process:** bound retained trace ordering state ([63e796d](https://github.com/morluto/rea/commit/63e796d50ea323ae8dcfef80f86c8154bb106790))
* **process:** cancel run-token reads during sampling ([09b6828](https://github.com/morluto/rea/commit/09b68285166bef2b7633402c8adf7eb25823b47e))
* **process:** observe already-due trailing callbacks before settling the capture journal ([#1700](https://github.com/morluto/rea/issues/1700)) ([ed084b3](https://github.com/morluto/rea/commit/ed084b3dbb13ec9dee1adc8f5b76e784b52c00bb)), closes [#1697](https://github.com/morluto/rea/issues/1697)
* **process:** preserve invalid runtime path selections as input errors ([cecc6ec](https://github.com/morluto/rea/commit/cecc6ecbffe84689a173d1634f342e046b0c8ceb))
* **process:** preserve monotonic capture timing and observed wall dates ([9049dba](https://github.com/morluto/rea/commit/9049dbaf0c3df32357419f9899965df2b611f493))
* **process:** preserve nested path identities during normalization ([#2468](https://github.com/morluto/rea/issues/2468)) ([b9ee921](https://github.com/morluto/rea/commit/b9ee921560dcf4590c2b13a1cc92c49c93e10675))
* **process:** reject deep trace cycles without recursive expansion ([10935b7](https://github.com/morluto/rea/commit/10935b7b1136accab2a43b308ab380749da0f7ae))
* **process:** report single empty settlement observation as unverifiable ([#2232](https://github.com/morluto/rea/issues/2232)) ([7724edb](https://github.com/morluto/rea/commit/7724edbdd3fa0acd94c8210a30dceba501fcdbe8))
* **process:** report the Swift compiler outcome with bounded diagnostics ([#1999](https://github.com/morluto/rea/issues/1999)) ([091cb81](https://github.com/morluto/rea/commit/091cb81fc3ca2b8ec96df7a534934388a6b882cd))
* **process:** retain owned command cleanup diagnostics ([#1907](https://github.com/morluto/rea/issues/1907)) ([38539a2](https://github.com/morluto/rea/commit/38539a2f4a522243d6fc369ae34ef30e2a2f80cc)), closes [#1856](https://github.com/morluto/rea/issues/1856)
* **process:** return private runtime ownership at allocation ([c22111e](https://github.com/morluto/rea/commit/c22111eca2c4bc84a084f12711f405b3e7422a40))
* **process:** use monotonic time for provider execution budgets ([d9e9fb2](https://github.com/morluto/rea/commit/d9e9fb2273543caab4eb21d896c33d462ada6971))
* **progress:** measure delivery intervals with a monotonic clock ([ed881c6](https://github.com/morluto/rea/commit/ed881c6d2ef6a8adba0c2a241ebc9ab4603b7868))
* **providers:** retain native parsing and compiler failure diagnostics ([b5ededd](https://github.com/morluto/rea/commit/b5ededd0f9c28d0f696a205103066928117368ee))
* **readiness:** validate stage evidence references once ([02cedc3](https://github.com/morluto/rea/commit/02cedc37961a5d230b134f330c148322cd04bf37))
* **reconstruction:** resolve latest verifier observations without input-order bias ([1f7f808](https://github.com/morluto/rea/commit/1f7f808c4f3bb5eca6b13bcc71d5cceb2ace6614))
* **reference:** report sloppy scripts, PHP, C# and enclosing Git facts ([#1770](https://github.com/morluto/rea/issues/1770)) ([7b15762](https://github.com/morluto/rea/commit/7b15762d09acf3d4ffb937f55ec1b54e7db5fbfa))
* **reference:** verify symlink identity around target reads ([3d83135](https://github.com/morluto/rea/commit/3d8313598785359c424d7305f503cb69fc8639a6))
* **runtime:** preserve unknown debugger attachment state ([e55ef72](https://github.com/morluto/rea/commit/e55ef7213515ba204c3d88a4487b4ee6a2773b06))
* **screenshot:** reject invalid PNG header dimensions ([#2472](https://github.com/morluto/rea/issues/2472)) ([e74e4e9](https://github.com/morluto/rea/commit/e74e4e951bf59c42b25779b45d1c7b8dde6b618f))
* **scripts, install:** honor test:fast "without build" promise and orient Node.js engine mismatches ([#2026](https://github.com/morluto/rea/issues/2026)) ([69e097a](https://github.com/morluto/rea/commit/69e097a8ca256ab97ee4b2883739447784a5f90e))
* **scripts:** pass MCP callTool timeouts as request options ([#2387](https://github.com/morluto/rea/issues/2387)) ([c462a4e](https://github.com/morluto/rea/commit/c462a4ebd3a10e59253bcf38353a0914b176738b))
* **session:** attribute selected-provider capabilities to the resolved identity ([#2013](https://github.com/morluto/rea/issues/2013)) ([2ff850d](https://github.com/morluto/rea/commit/2ff850de68dc9b05c6faf2bbfd26d56bc2d5d27c)), closes [#2009](https://github.com/morluto/rea/issues/2009)
* **session:** invalidate snapshots before metadata write dispatch ([1c64fe3](https://github.com/morluto/rea/commit/1c64fe3eff48679c6880f42d2f0a4af46686ade7))
* **session:** verify retained client liveness on open fast path ([e59db2f](https://github.com/morluto/rea/commit/e59db2f5deefb1c5e74f01e19631f6d75cfaa25a))
* **setup:** configure an empty Hermes config.yaml ([#1983](https://github.com/morluto/rea/issues/1983)) ([43f9c39](https://github.com/morluto/rea/commit/43f9c39dfbdfaa6e5bc260b037a506cd1f56fe6d))
* **setup:** honor config roots and display terminal dry-run plans ([2397137](https://github.com/morluto/rea/commit/23971376158e7815edcbe6c04a8df7c9504650dc))
* **setup:** ignore empty home directory selections ([c339f9e](https://github.com/morluto/rea/commit/c339f9ebe7bd1f30608d6ccb7175da523dd3c485)), closes [#2325](https://github.com/morluto/rea/issues/2325)
* **setup:** name each skill directory in the uninstall summary ([#2118](https://github.com/morluto/rea/issues/2118)) ([5190843](https://github.com/morluto/rea/commit/5190843f8f09ac17cbf79561bd1f708c509cda78)), closes [#2111](https://github.com/morluto/rea/issues/2111)
* **setup:** name invalid client config shape errors clearly ([#2157](https://github.com/morluto/rea/issues/2157)) ([b1bf4a7](https://github.com/morluto/rea/commit/b1bf4a70d44191348ca20a91a52fb663da4daed0))
* **setup:** name structural problems in client configurations ([#2154](https://github.com/morluto/rea/issues/2154)) ([51367ca](https://github.com/morluto/rea/commit/51367cae6f4e6ef9b9dcb28dc21fe6c92d1dd6f8)), closes [#2152](https://github.com/morluto/rea/issues/2152)
* **setup:** pin Hopper's macOS publisher identity before install ([#2481](https://github.com/morluto/rea/issues/2481)) ([650237e](https://github.com/morluto/rea/commit/650237efb01b3c4a154799dbbedb2af1084f2312))
* **setup:** preserve Cordis ownership across uncertain patch structure ([21eff3b](https://github.com/morluto/rea/commit/21eff3bae10f58711a35ad44f86e9f96f9bb4790))
* **setup:** preserve current recovery snapshots and decoding failures ([f6775a3](https://github.com/morluto/rea/commit/f6775a36694a6a9c620f4248638e8056a6cbcb66))
* **setup:** preserve skill files and report rollback failures ([c240768](https://github.com/morluto/rea/commit/c2407689cb04df082e8db0b6afa8f8d022f6bcd1))
* **setup:** preserve unrelated Hermes YAML line formatting ([58e91e1](https://github.com/morluto/rea/commit/58e91e14bd5659dd57acbb91e56b5fc6a6963c80))
* **setup:** register a stable launcher when run through bunx ([#1731](https://github.com/morluto/rea/issues/1731)) ([d412def](https://github.com/morluto/rea/commit/d412def7225a556e6d2140176dc9ab4bd34af0f3))
* **setup:** remove emptied skill directories on uninstall ([#2182](https://github.com/morluto/rea/issues/2182)) ([d350357](https://github.com/morluto/rea/commit/d35035780a643e38301b8eaea03f01bb506e9df9))
* **setup:** retain regular-file cleanup ownership through workflows ([dcfc5d4](https://github.com/morluto/rea/commit/dcfc5d403ac997c2b08bee4753e57a1a4a8a8538))
* **setup:** retain symlinked data roots during uninstall ([#1821](https://github.com/morluto/rea/issues/1821)) ([7e2f79a](https://github.com/morluto/rea/commit/7e2f79ab45a23ab844f89fb93c9cdf202230c73f))
* **setup:** verify independently selected skill destinations ([bb50813](https://github.com/morluto/rea/commit/bb508134efbba392a404b651d6ec05be8b7eac1e))
* **snapshot:** bind replayed subjects to the selected target path ([c002b89](https://github.com/morluto/rea/commit/c002b89e2d8130a3c07e25e5fd604876be18c703))
* **snapshot:** classify non-object roots as invalid content ([#2685](https://github.com/morluto/rea/issues/2685)) ([9cf49c7](https://github.com/morluto/rea/commit/9cf49c7a784ace9b79e4b4c1022c9fbf2b3f77e1))
* **sourcemap:** retain named originals beside unknown source names ([83a4445](https://github.com/morluto/rea/commit/83a4445f440fab978ad9376b4260acae3f5fb189))
* **sourcemaps:** preserve nullable source declarations ([ee50ed1](https://github.com/morluto/rea/commit/ee50ed121e38cc6e1a2ff0dc8b789f15f71c5402))
* **target:** recognize binary plists by magic under any extension ([#2133](https://github.com/morluto/rea/issues/2133)) ([672bdea](https://github.com/morluto/rea/commit/672bdea21fd943688e081d209a993e234c45507f)), closes [#2129](https://github.com/morluto/rea/issues/2129)
* **targets:** project invalid raw-binary loads as caller input ([#2004](https://github.com/morluto/rea/issues/2004)) ([edf4624](https://github.com/morluto/rea/commit/edf46248f2a44b4e5776518489fee09d0fc4516b))
* **targets:** recognize Windows headers after short DOS stubs ([c9fed50](https://github.com/morluto/rea/commit/c9fed50691ae014890e91a72534773d2fb58ce44))
* **targets:** remove path advice for format refusals ([f28f338](https://github.com/morluto/rea/commit/f28f3382836f8dd8629c25b1bf5455d29b28d41d)), closes [#1979](https://github.com/morluto/rea/issues/1979) [#1852](https://github.com/morluto/rea/issues/1852)
* **test:** audit static Wasm and Harmony tool wording ([#1917](https://github.com/morluto/rea/issues/1917)) ([dfe3117](https://github.com/morluto/rea/commit/dfe31172ca0a090be3f40bc134f54d69c08e31d8))
* **test:** wait for Hopper reconnect listener readiness ([#1885](https://github.com/morluto/rea/issues/1885)) ([d3e83b0](https://github.com/morluto/rea/commit/d3e83b089943e2941dace3546a068a59bd0a335e))
* **uninstall:** label YAML configuration syntax failures correctly ([94153f1](https://github.com/morluto/rea/commit/94153f1c757e07a6d092e00903aa8b125d4a0afa))
* **update:** resolve Bun global directories before changing cwd ([e90acae](https://github.com/morluto/rea/commit/e90acaea674f5f7f9f17ea472104afdc1422ada6))
* **update:** share parsed client activation with maintenance ([b045df1](https://github.com/morluto/rea/commit/b045df100e1489e08a36d6ff04cea7feecf1332c))
* **wasm:** accept unpadded WABT section rows ([#1963](https://github.com/morluto/rea/issues/1963)) ([9eee709](https://github.com/morluto/rea/commit/9eee709f0cc4064808579c225814898a475ca388))
* **wasm:** inspect modules whose custom section names contain NUL ([#1981](https://github.com/morluto/rea/issues/1981)) ([1febc22](https://github.com/morluto/rea/commit/1febc22907f1cc8035dd1450616233121bd7f19d))
* **wasm:** preserve debug module names in WABT output ([#1967](https://github.com/morluto/rea/issues/1967)) ([b777e5d](https://github.com/morluto/rea/commit/b777e5d4d8a7b60edad7d5c0eaa56408a4b3f610))
* **wasm:** preserve leading BOM in custom section names ([#2102](https://github.com/morluto/rea/issues/2102)) ([abc8c6d](https://github.com/morluto/rea/commit/abc8c6dadf4f11b67de1cc2ee96bb55047416d71))
* **wasm:** stop appending WABT remediation to unrelated runtime-root failures ([#2084](https://github.com/morluto/rea/issues/2084)) ([ea2f94d](https://github.com/morluto/rea/commit/ea2f94d4de2241306158fecdd75f96cd34566694))
* **web:** reject NUL bytes in web tool local path inputs ([499ac18](https://github.com/morluto/rea/commit/499ac180115de7c9de3f1adc55159ffa6e9bb40e))
* **website:** clarify translated evidence and runtime observations ([f1f5582](https://github.com/morluto/rea/commit/f1f5582ce84f54e529826fef7af3e201f52248d9))
* **website:** clarify translated terminology and RTL flows ([a956275](https://github.com/morluto/rea/commit/a956275d8d0249c16dad282a3fb5d7da48fb2d6f))
* **website:** incorporate multilingual contributor refinements ([d981a58](https://github.com/morluto/rea/commit/d981a58d7f5344c3ab2117b6384aece4d1c35480))
* **website:** keep omitted language selectors free of whitespace ([8583a19](https://github.com/morluto/rea/commit/8583a190c0178374132c888622d693fed1f88a18))
* **website:** prioritize getting started across language editions ([8cbd57c](https://github.com/morluto/rea/commit/8cbd57cafaf1cfacbe832bae593589cf211442ef))
* **windows:** bring the real Windows Ghidra verifiers up to date ([#1863](https://github.com/morluto/rea/issues/1863)) ([ea40f71](https://github.com/morluto/rea/commit/ea40f717b26392000f989a8b500f6774b5e85091))
* **windows:** preserve caller affinity only within one processor group ([#2202](https://github.com/morluto/rea/issues/2202)) ([ab62ee9](https://github.com/morluto/rea/commit/ab62ee9dcf98e4717d0b3445330f7cfdfe50c9f4))
* **windows:** preserve transient runtime admission failures ([#2467](https://github.com/morluto/rea/issues/2467)) ([1724077](https://github.com/morluto/rea/commit/172407787f1842dedbcd27b5385177595f786ce4))
* **windows:** retain failed native inspection ownership ([#1826](https://github.com/morluto/rea/issues/1826)) ([0b4ce83](https://github.com/morluto/rea/commit/0b4ce83618bb800627264ae6221e2e35140417b5))
* **workflows:** retain staged output ownership and cancellation semantics ([7285e15](https://github.com/morluto/rea/commit/7285e15839a4fecf9940683699b781a60ccc4775)), closes [#1968](https://github.com/morluto/rea/issues/1968)
### Performance Improvements
* **browser:** parse captured module URLs once per lookup ([#1829](https://github.com/morluto/rea/issues/1829)) ([ca11162](https://github.com/morluto/rea/commit/ca11162d027a0d3e3b5a65a54825b00d5689fb80))
* **ci:** share bytecode across fresh Apple artifact subprocesses ([ba7df3f](https://github.com/morluto/rea/commit/ba7df3f0f7ee63e6350d5fbdddc8ed945a6b141d))
* **cli:** bundle runtime entries to reduce Windows startup I/O ([#1996](https://github.com/morluto/rea/issues/1996)) ([d93fbda](https://github.com/morluto/rea/commit/d93fbda390c6b8b2741c552bd194b50c3e08e0c1))
* **cli:** defer command-specific analysis implementations ([a4dd4e8](https://github.com/morluto/rea/commit/a4dd4e8e8700ce350e1a0c612ebd61b91ca5e585))
* **cli:** defer command-specific modules until dispatch ([a82d14e](https://github.com/morluto/rea/commit/a82d14e4db4dca989ffa2e7dbe78fd43551da256))
* **domain:** avoid duplicate readiness bundle validation ([29ff887](https://github.com/morluto/rea/commit/29ff887f6558cf00c0d6392889b890ed8a702a6f))
* **evidence:** authenticate and seal imported bundles in one pass ([b123b2e](https://github.com/morluto/rea/commit/b123b2e51cc7d6d2157525664be2df88fb1ba3bd))
* **evidence:** fuse streamed JSON token handling ([#1694](https://github.com/morluto/rea/issues/1694)) ([13e03e9](https://github.com/morluto/rea/commit/13e03e94363a007353fcb5dfedec764de12fc927))
* **evidence:** reuse authenticated comparisons through aggregation ([26bffb9](https://github.com/morluto/rea/commit/26bffb94bf80355c35e22a98a8fc629c6210307e))
* **evidence:** reuse authenticated file snapshots during session import ([#1721](https://github.com/morluto/rea/issues/1721)) ([966dc3a](https://github.com/morluto/rea/commit/966dc3acde2880a0f94b2658e2a02180d505cafa))
* **evidence:** reuse authenticated unknown mutation snapshots ([042dcaa](https://github.com/morluto/rea/commit/042dcaa1ee66f1b8668f08b193f38ab01bceaacb))
* **flutter:** add opt-in retained AOT summary and string pages ([23de4f7](https://github.com/morluto/rea/commit/23de4f77c74dd299d45cab880c36321d802b21fe))
* **frida:** evict captured messages without copying the whole queue ([#2628](https://github.com/morluto/rea/issues/2628)) ([6a05248](https://github.com/morluto/rea/commit/6a05248d464387c0ce4ec1577b4c1b24d3d355c7))
* **ghidra:** compile bundle activator with bridge scripts ([#1931](https://github.com/morluto/rea/issues/1931)) ([0f4992f](https://github.com/morluto/rea/commit/0f4992fb9e5bfe1fafe75c35b7f20ad411595573))
* **javascript:** index source-map ignore lists per leaf ([#1905](https://github.com/morluto/rea/issues/1905)) ([898d509](https://github.com/morluto/rea/commit/898d5096f94fe1ed3ea7d091455e638cc7d23bfd))
* **javascript:** reuse collected callable identities for returns ([d3088c3](https://github.com/morluto/rea/commit/d3088c3795f6c37cb3b6aec0bb0c371dd0640ee8))
* **javascript:** skip nested callables during fingerprint traversal ([#1871](https://github.com/morluto/rea/issues/1871)) ([097f0d5](https://github.com/morluto/rea/commit/097f0d54afceb3ce61dbaa24dd6736658d1fce06))
* **javascript:** skip the repeated-prefix scan on first capture visits ([#1789](https://github.com/morluto/rea/issues/1789)) ([41544fb](https://github.com/morluto/rea/commit/41544fb952aef84fcf6ff5d9a5284cf2b2985053))
* **mcp:** bundle internal modules to reduce Windows startup I/O ([76e4153](https://github.com/morluto/rea/commit/76e4153d346b14ef194222bbeef240cf8a16dc99))
* **mcp:** compact repeated session discovery facts ([#1988](https://github.com/morluto/rea/issues/1988)) ([6395e8d](https://github.com/morluto/rea/commit/6395e8d3007d5ee453477460e7ee1c51cff76be0))
* **mcp:** keep Playwright and type-only imports off MCP startup ([#1691](https://github.com/morluto/rea/issues/1691)) ([0ecf3f2](https://github.com/morluto/rea/commit/0ecf3f2438ec96a6b543942869808018e8d84452))
* **mcp:** reduce Windows startup module resolution ([#1814](https://github.com/morluto/rea/issues/1814)) ([76e4153](https://github.com/morluto/rea/commit/76e4153d346b14ef194222bbeef240cf8a16dc99))
* **native:** add opt-in Mach-O summary delivery and retained pages ([#2634](https://github.com/morluto/rea/issues/2634)) ([bf34d0d](https://github.com/morluto/rea/commit/bf34d0d2fc3cc22076f49d682e74a0b71aa1db18))
* **native:** run independent Mach-O slice commands concurrently ([#2466](https://github.com/morluto/rea/issues/2466)) ([2c7240c](https://github.com/morluto/rea/commit/2c7240cf9d8231e16ad01de7678af9dbd01bab46))
* **snapshot:** restore large legacy caches within live memory bounds ([092de8f](https://github.com/morluto/rea/commit/092de8f4b336b537579bd48d7d30135b7114371d))
* **test:** batch compatible client profile lifecycles ([0153b4d](https://github.com/morluto/rea/commit/0153b4d95bdf38eddee91d5ce08181030fb955b4))
* **test:** reuse CLI boundary worker module graphs ([f178bf1](https://github.com/morluto/rea/commit/f178bf1c00139e1b7fe2d14fcfe1f558fe08fbe2))
* **test:** reuse the CI scope Git baseline ([ef5f9aa](https://github.com/morluto/rea/commit/ef5f9aa2d7769b32d9f88211812ae11456a88267))
* **tests:** consolidate TypeScript dialect producer checks ([01762b1](https://github.com/morluto/rea/commit/01762b10c6a40370996e6dfa3a96ffcb27148f97))
* **test:** share stdio analysis for comparison fixtures ([43dafb5](https://github.com/morluto/rea/commit/43dafb5abe60ee21be4f6a6b6de482cd7a5bab0a))
* **tests:** remove timed output and synchronize browser fixtures ([693ed33](https://github.com/morluto/rea/commit/693ed332b58d05192f55b8192d2c0c4bc4f4824a))
* **tests:** reuse stateless workflows and immutable fixtures ([5f4c7ff](https://github.com/morluto/rea/commit/5f4c7ff6df0735d5a4461b5d0e6f2e466b888be0))
### Code Refactoring
* **evm:** bind offline analysis through a static descriptor ([ad08510](https://github.com/morluto/rea/commit/ad08510a113d7a9b8d92fb1e385757880e469869))
* **javascript:** reuse validated source-map decoding ([f64790b](https://github.com/morluto/rea/commit/f64790bfc32db330b2be5bdff61af555a9d26067))
* **mcp:** trust validated inputs and tighten contract fixtures ([2646150](https://github.com/morluto/rea/commit/264615059ce6c89badcc7288e8c67bb8dd1ce0dd))
* prune obsolete provider and verification code ([e198793](https://github.com/morluto/rea/commit/e19879328a11fe38b49b6b133a8ebde908ea980e))
### Documentation
* add responsible-use disclaimer ([4a5aec8](https://github.com/morluto/rea/commit/4a5aec8ad6d54444b2a2c2313fbc506b17ff91aa))
* **agents:** require brittleness review before resolving threads ([0365454](https://github.com/morluto/rea/commit/0365454a573eb52c7089320fd74bfd4a673b78ae))
* **agents:** require related brittleness cleanup in bug fixes ([0d8b728](https://github.com/morluto/rea/commit/0d8b728d2470a9b51239d61986944bacaf9a8708))
* celebrate 110,000 GitHub stars <‰ ([#2042](https://github.com/morluto/rea/issues/2042)) ([0d78c99](https://github.com/morluto/rea/commit/0d78c99c11efee66570772efe6eb88dfd0e1c694))
* celebrate 170,000 GitHub stars <‰ ([#2462](https://github.com/morluto/rea/issues/2462)) ([9188698](https://github.com/morluto/rea/commit/91886982fccc50a9f7f31d4f320b25ffbcff35af))
* celebrate 80,000 GitHub stars <‰ ([#1752](https://github.com/morluto/rea/issues/1752)) ([6ff4d4a](https://github.com/morluto/rea/commit/6ff4d4a0500e5f9fb7601e1c5ae5dbbd85796d91))
* celebrate 90,000 GitHub stars <‰ ([#1858](https://github.com/morluto/rea/issues/1858)) ([5876779](https://github.com/morluto/rea/commit/5876779013726964deef748120c48f5fa0155546))
* clarify ownership and interpretation boundaries ([64b71b1](https://github.com/morluto/rea/commit/64b71b180a88a2df1b0b61e803c1c322ee59c5e1))
* clarify responsible use and preserve third-party notices ([#2082](https://github.com/morluto/rea/issues/2082)) ([974f11d](https://github.com/morluto/rea/commit/974f11d010570c0a18423cd1cc93e9a5203df1b6))
* **cli:** add a managed assembly inspection workflow ([#2037](https://github.com/morluto/rea/issues/2037)) ([538f8cc](https://github.com/morluto/rea/commit/538f8cc7224dba44b728cf6c000920f911957fc4))
* correct drift and reorganize English guides ([#2207](https://github.com/morluto/rea/issues/2207)) ([7ae3c50](https://github.com/morluto/rea/commit/7ae3c50a9d49c480035855de9fcc4484befacd92))
* define contribution scope and simplify proposal forms ([ac6ef95](https://github.com/morluto/rea/commit/ac6ef95e12335bdc054ccaaa96c32474cf8789ea))
* define performance priorities for analysis workflows ([#2245](https://github.com/morluto/rea/issues/2245)) ([8e00452](https://github.com/morluto/rea/commit/8e004522f232ec698c77536235853088a851e6b5))
* **ghidra:** verified macOS arm64 native decompiler build recipe ([#2112](https://github.com/morluto/rea/issues/2112)) ([5f79c85](https://github.com/morluto/rea/commit/5f79c852af8e53a56f58266a9dcf84314c197164))
* **ghidra:** verify and document the macOS arm64 native decompiler build ([5f79c85](https://github.com/morluto/rea/commit/5f79c852af8e53a56f58266a9dcf84314c197164))
* **i18n:** celebrate 80,000 GitHub stars in translated READMEs ([#1758](https://github.com/morluto/rea/issues/1758)) ([953a0ce](https://github.com/morluto/rea/commit/953a0ce5bd3a8b44051eb9c16d8c9a20cad29e7c))
* **install:** explain Claude Code upfront tool loading on third-party endpoints ([#2029](https://github.com/morluto/rea/issues/2029)) ([d024928](https://github.com/morluto/rea/commit/d0249282c64882329a6acab8f3b477552da53499)), closes [#2019](https://github.com/morluto/rea/issues/2019)
* preserve the native guide title anchor ([#2224](https://github.com/morluto/rea/issues/2224)) ([ae41265](https://github.com/morluto/rea/commit/ae412654d5947f3841c191e92f3ad6b73d6f894b))
* **readiness:** align required stage identifier and checks with evaluator ([#2200](https://github.com/morluto/rea/issues/2200)) ([85c5477](https://github.com/morluto/rea/commit/85c5477930d504cfd8b72334b48255d8e746a85e))
* **readme:** remove CI badges from translations ([3298219](https://github.com/morluto/rea/commit/3298219d8cd905897ec9a96bf639602a273d4a9b))
* **readme:** remove CI status badge ([b07e54a](https://github.com/morluto/rea/commit/b07e54a10e66319d90815313fae93350257e1655))
* **setup:** confirm published 6.4.0 client support ([57c5aca](https://github.com/morluto/rea/commit/57c5acac2e13fd7f0e3832bc147c7ceda8c314f2))
* shorten responsible-use notices across languages ([fa87dd2](https://github.com/morluto/rea/commit/fa87dd2aac3eaeb49875c54e93ec18c98cc92e4d))
* sync French, Indonesian and Italian READMEs with English ([#1861](https://github.com/morluto/rea/issues/1861)) ([96547d3](https://github.com/morluto/rea/commit/96547d373fcdc672058b29774d6c634f4e9cc1bd))
* **website:** add essay on reverse engineering models ([fe496bd](https://github.com/morluto/rea/commit/fe496bd1cf589938c372646ddef8ca9701417bf0))
* **website:** highlight the setup guide on the homepage ([#1714](https://github.com/morluto/rea/issues/1714)) ([8be7ad4](https://github.com/morluto/rea/commit/8be7ad496c9a52a9986c4d3f6596de279b52df7e))
* **website:** improve model essay transitions and narrative flow ([ca07373](https://github.com/morluto/rea/commit/ca07373e44e19e0c8357ac49e460c90d87fcc22c))
* **website:** publish model essay in all supported languages ([3d78a57](https://github.com/morluto/rea/commit/3d78a57043eb6b92812c71bcb6d83753c3d48036))
* **website:** simplify reconstruction evidence in model essay ([b06751b](https://github.com/morluto/rea/commit/b06751b703181527a320226c4ab7e4cecd5dcea0))
* **website:** use straight quotes in model essay ([806ff57](https://github.com/morluto/rea/commit/806ff575676e447c7ba349b6fd185e6e8e02ee5d))
### Tests
* **analysis:** verify real preset snapshot and cleanup boundaries ([#2349](https://github.com/morluto/rea/issues/2349)) ([1dd20ce](https://github.com/morluto/rea/commit/1dd20ce1ff4ab66c74350929577e753aa17a9006)), closes [#2016](https://github.com/morluto/rea/issues/2016)
* **android:** verify duplicate class inventory behavior on empty discovery queries ([#1984](https://github.com/morluto/rea/issues/1984)) ([6611bcf](https://github.com/morluto/rea/commit/6611bcf6e86630c775c24e6afa00d7239c64e70e))
* **apple:** consolidate keyed archive integer verification ([#1933](https://github.com/morluto/rea/issues/1933)) ([3b490a7](https://github.com/morluto/rea/commit/3b490a743fd56a8a27b1356f7b2d1284db8848ef))
* **apple:** keep the same-named slice dyld fixture openable on x64 hosts ([#2755](https://github.com/morluto/rea/issues/2755)) ([fb28ec1](https://github.com/morluto/rea/commit/fb28ec1537e2d02aaca96325483c849c5c8ade6f))
* **apple:** skip ibtool-compiled NIB tests when Xcode ibtool is unavailable ([#2070](https://github.com/morluto/rea/issues/2070)) ([7bc57e1](https://github.com/morluto/rea/commit/7bc57e1e64915a26c32f89fb53d0321193a37160))
* **architecture:** consolidate import boundary verification ([2976fac](https://github.com/morluto/rea/commit/2976fac30d4088c1f1ba383261eb3eff5e95bce6))
* bound CLI validation subprocess concurrency ([ff57b75](https://github.com/morluto/rea/commit/ff57b75e226028767b86aa3d4271c2c2f56342db))
* **ci:** stabilize JADX recovery and ignore generated reports ([#1668](https://github.com/morluto/rea/issues/1668)) ([0a6b46d](https://github.com/morluto/rea/commit/0a6b46d5b0299f76c0d268f7b70395f9b6fbd68e))
* classify filesystem and process workflows by boundary ([c2b56e3](https://github.com/morluto/rea/commit/c2b56e3e1c9619beab15de8e1f200422473caf30))
* classify filesystem application workflows as boundaries ([804c287](https://github.com/morluto/rea/commit/804c287e441dcaa3876429f026ac89f6482f9699))
* **cli:** align reference import failure envelopes ([8224aeb](https://github.com/morluto/rea/commit/8224aeba9dd41f29ea6f8ef7ad7efa3f45fd4a6e))
* **cli:** cache subprocess compilation within each worker ([6bcfeb0](https://github.com/morluto/rea/commit/6bcfeb0e55a6fcc3ed586e5744f2aa1c270cac31))
* **cli:** consolidate development runner workflow coverage ([16f5ded](https://github.com/morluto/rea/commit/16f5ded4468d7100d3640f874f33ddbaa953a787))
* **cli:** consolidate schema coverage and assert input refusals ([beb80a0](https://github.com/morluto/rea/commit/beb80a07bafc0847600eb635e0323068a386c811))
* **clients:** verify native Claude Code integration on Windows ([#1926](https://github.com/morluto/rea/issues/1926)) ([c32d846](https://github.com/morluto/rea/commit/c32d84622fb3952823c6cfc25c557aec08c1788c)), closes [#1923](https://github.com/morluto/rea/issues/1923)
* **clients:** verify native Gemini CLI integration on Windows ([#1936](https://github.com/morluto/rea/issues/1936)) ([04089d5](https://github.com/morluto/rea/commit/04089d5bec11d6e0411e7c35d0168bbae63d7e7d))
* **cli:** preserve Windows runtime paths and failure diagnostics ([79156e5](https://github.com/morluto/rea/commit/79156e5bed1d72b83ade0483246f457b337b7e59))
* **docs:** type README drift scenarios as fixed pairs ([6e38529](https://github.com/morluto/rea/commit/6e38529333a7a0704690a087c7cb054fb7385d25))
* **evidence:** cover workflow question whitespace normalization ([#1739](https://github.com/morluto/rea/issues/1739)) ([b2da21a](https://github.com/morluto/rea/commit/b2da21abe11ac43a4cc24b233bd52ce10995e4a1))
* **evm:** verify real worker cancellation and cleanup recovery ([#2404](https://github.com/morluto/rea/issues/2404)) ([ae80513](https://github.com/morluto/rea/commit/ae8051302b5834322fa00cfa31c968ae74ab6ed9))
* **evm:** verify static descriptor bindings before use ([#2427](https://github.com/morluto/rea/issues/2427)) ([4f4f6cd](https://github.com/morluto/rea/commit/4f4f6cd7b5d7a400b90772a138d7b9f942c2a556))
* **ghidra:** require pending multi-HI16 coverage in the real MIPS relocatable lane ([#1974](https://github.com/morluto/rea/issues/1974)) ([eaf88ac](https://github.com/morluto/rea/commit/eaf88aca2af2b8ad7a277c09d0b45f6e2de8ac4a))
* **ghidra:** restore complete installation fixtures across CI boundaries ([18d56f5](https://github.com/morluto/rea/commit/18d56f5321c3c5d5be40c19bcb0a0f2b25c2d459))
* **hopper:** align startup fixtures with cleanup and lease contracts ([b3dec00](https://github.com/morluto/rea/commit/b3dec005bcb89b5afe6e0aa41e5251f8a3a31de7))
* **hopper:** repair Linux demo setup and native verification ([#2638](https://github.com/morluto/rea/issues/2638)) ([141d209](https://github.com/morluto/rea/commit/141d2095f5c6faaf2d3c9b81b0f0f992cacec79c))
* **ida:** expect direction in combined call coverage ([4dd44dd](https://github.com/morluto/rea/commit/4dd44ddf16c810c4bcaa16e0bdb3b09bfcb4e33a))
* **javascript:** classify artifact workflows as boundary tests ([dc303a8](https://github.com/morluto/rea/commit/dc303a8ee8a2ceaae37e4da4ac4ed64035793d59))
* **javascript:** consolidate module lookup regressions ([#2031](https://github.com/morluto/rea/issues/2031)) ([783fcb0](https://github.com/morluto/rea/commit/783fcb06a4bd0444be214822fed60a3786932782))
* **javascript:** expect unknown parameter provenance ([e05d339](https://github.com/morluto/rea/commit/e05d3399f19f25fdf04285a02b293418256b26dc))
* **javascript:** verify CommonJS file URL boundaries via CLI and MCP ([#2061](https://github.com/morluto/rea/issues/2061)) ([6ce5903](https://github.com/morluto/rea/commit/6ce5903e7a37252156ec847c4c8874410a4d8f3f))
* **managed:** use the current attribute fixture contract ([6407718](https://github.com/morluto/rea/commit/6407718f3020a34cc087163492e3f9a465e2e841))
* **mcp:** align completion checks with provider readiness ([fe4b94a](https://github.com/morluto/rea/commit/fe4b94a661054cef7603a3f07aae71f1df0a701a))
* **mcp:** fix three MCP boundary suites on native Windows ([#2312](https://github.com/morluto/rea/issues/2312)) ([a97b713](https://github.com/morluto/rea/commit/a97b713f740932339a5f1036ad2213168c421270))
* **mcp:** preserve coverage in unchanged function fixtures ([3ee6ff4](https://github.com/morluto/rea/commit/3ee6ff43c1696a5a5269bd4aa5de72f7e5f40627))
* **mcp:** ready providers before live prompt completion checks ([#2137](https://github.com/morluto/rea/issues/2137)) ([621cee1](https://github.com/morluto/rea/commit/621cee192d582c87ca3d254d4cb22c9d209d3aea)), closes [#2123](https://github.com/morluto/rea/issues/2123)
* **mcp:** reuse advertised schema in inventory check ([#2469](https://github.com/morluto/rea/issues/2469)) ([a6efa4f](https://github.com/morluto/rea/commit/a6efa4fcefeffcf5ff5310b8154988b4be30c349))
* **native:** replay complete dylib-use provider output ([89f5e48](https://github.com/morluto/rea/commit/89f5e489adac69af2937fabc1a5ba283191d630a)), closes [#1970](https://github.com/morluto/rea/issues/1970)
* **native:** skip the DMG EACCES diagnostic as root ([#1584](https://github.com/morluto/rea/issues/1584)) ([262efe6](https://github.com/morluto/rea/commit/262efe6c54d8f179e9f9515d96025871ae45806a))
* **native:** skip the EACCES directory diagnostic as root ([#1394](https://github.com/morluto/rea/issues/1394)) ([40d9c7b](https://github.com/morluto/rea/commit/40d9c7b836a3aca5d589c0a65c7d02b3ea5cd36b))
* **native:** skip the inaccessible-directory diagnostic as root ([#1900](https://github.com/morluto/rea/issues/1900)) ([d0edeec](https://github.com/morluto/rea/commit/d0edeec1009f1149956ee2a95dc5941aff8f78b5))
* **native:** verify real PE and DOS entry views ([1770f38](https://github.com/morluto/rea/commit/1770f38cfc6cbe87491d0de8b8730c305c5cf8ea))
* **pcap:** run the missing-path refusal only where captures are admitted ([#2510](https://github.com/morluto/rea/issues/2510)) ([1991542](https://github.com/morluto/rea/commit/1991542e59f64d46269b3ff942be77a5ff06e88f))
* **perf:** distinguish Codex tool context and token accounting ([#2416](https://github.com/morluto/rea/issues/2416)) ([ee87111](https://github.com/morluto/rea/commit/ee87111009ace2ca4ef82c9aef09c82a37ca55f7))
* **perf:** observe owned counters at payload completion ([ca2d15b](https://github.com/morluto/rea/commit/ca2d15b3ee965bd7ab27204cdf4b96b117d148c1))
* **perf:** verify existing SQLite column selection on QGIS data ([#2489](https://github.com/morluto/rea/issues/2489)) ([45984a4](https://github.com/morluto/rea/commit/45984a4319c8b6dcf2b03e969fbd4330c8432394))
* **reference:** scope cleanup faults to the owned source file ([#2430](https://github.com/morluto/rea/issues/2430)) ([eeba618](https://github.com/morluto/rea/commit/eeba61826c1cd1c7be70f011fda0537baadd4179))
* **release:** validate only the selected preflight job ([747e729](https://github.com/morluto/rea/commit/747e7299141cec0b03774c427f579889d5088d20))
* reuse application analysis across comparison directions ([044c0a9](https://github.com/morluto/rea/commit/044c0a92d5c86faa9965669680935d4f552977b1))
* reuse identical JavaScript analysis fixtures ([42951bb](https://github.com/morluto/rea/commit/42951bbbbe8d98ebde1783450d130fdddb5ba889))
* reuse validators for identical MCP schemas ([85ccef3](https://github.com/morluto/rea/commit/85ccef34de1cddcf71037ccea93acdf709f94965))
* share native keyed archive integer fixture generation ([847a1a3](https://github.com/morluto/rea/commit/847a1a3f25f69e1b3808fb0a53e5f1415a7ed058))
* **sourcemap:** consolidate source map regressions ([#2034](https://github.com/morluto/rea/issues/2034)) ([83a4445](https://github.com/morluto/rea/commit/83a4445f440fab978ad9376b4260acae3f5fb189))
* **sourcemaps:** assert semantic evidence and actual output admission ([bd74c7a](https://github.com/morluto/rea/commit/bd74c7af2bec952c2eea7a17feedd05e6675b719))
* strengthen archive and resource cleanup assertions ([5ba1642](https://github.com/morluto/rea/commit/5ba1642a7d891a60493b2195e55149b0f3197fb4))
* strengthen public input and module lookup controls ([b7f563e](https://github.com/morluto/rea/commit/b7f563e15f854061dc1507896181b7b24ac3b5f0))
* **web:** remove duplicate path schema cases ([b5b7301](https://github.com/morluto/rea/commit/b5b73019f946e5edc0df506c9f8f4bb28afa6590))
### Continuous Integration
* balance test shards and reuse package download caches ([40b37ae](https://github.com/morluto/rea/commit/40b37aeee10e89d85ee8b7747cbdcd3ad1a34db4))
* bound core job execution time ([9f3f392](https://github.com/morluto/rea/commit/9f3f392eb8fe81060b44941ed754521984def3bd))
* finish main validation and balance complete test costs ([f0f0995](https://github.com/morluto/rea/commit/f0f09950f34d96e3b1c62fd04821af5e301f80c7))
* fix PR metadata triggers and defer expensive runner work ([3af8f84](https://github.com/morluto/rea/commit/3af8f847f0e35843f51622a136165dd6ee12cdd0))
* keep validation fresh and preserve failed-shard reports ([#2210](https://github.com/morluto/rea/issues/2210)) ([428eee0](https://github.com/morluto/rea/commit/428eee00fdd0406cdf2b734c9ea14a2bb02da279))
* narrow native-host lane selection and reuse build caches ([#2131](https://github.com/morluto/rea/issues/2131)) ([4f46eac](https://github.com/morluto/rea/commit/4f46eac453965efde3bc782aebfeec198551afc4))
* observe affected-owner test plans against full results ([fa0a9b2](https://github.com/morluto/rea/commit/fa0a9b2d328a07d8ec2924dc224d51c620981dcc))
* parallelize native verification and share test artifacts ([91f6840](https://github.com/morluto/rea/commit/91f6840c35e99e5f23612920d790a30d324ac115))
* raise duplication threshold to 3.5 percent ([11f1d95](https://github.com/morluto/rea/commit/11f1d952e79241b507e47c690eed00e0d5ffda30))
* reduce macOS runner contention ([3f4ea32](https://github.com/morluto/rea/commit/3f4ea3218929baba0c30c2cb7eb7e7a829a1fc21))
* reduce redundant compatibility and qualification work ([d13c9f8](https://github.com/morluto/rea/commit/d13c9f840d36a321a56917e75c48afcb8d72920e))
* reduce reporter fan-out and preserve shard history ([ab02f3c](https://github.com/morluto/rea/commit/ab02f3c8d665ead35391c94db74cd0fb0cfed0ed))
* reuse current-run builds across provider lanes ([490a285](https://github.com/morluto/rea/commit/490a285070268aee0ecd50001b9a9a83c1e313f0))
* reuse portable build cache in native package jobs ([8fa1175](https://github.com/morluto/rea/commit/8fa11751eaab94eaa5b8de8c1ecd143ef0066cd6))
* reuse portable Turbo outputs across verification runs ([a00c34d](https://github.com/morluto/rea/commit/a00c34dcb155e5993bda8ad0f51bf3b13e289760))
* scope timing helpers to portable verification ([c8d861f](https://github.com/morluto/rea/commit/c8d861f9764f8c07f0925dae0c6451de85fffd6a))
* separate fast feedback from full qualification ([0c7f4df](https://github.com/morluto/rea/commit/0c7f4dfbd9c1975eeedfecd96debb2e4263012d0))
* summarize static checks and retain complete reports ([#2242](https://github.com/morluto/rea/issues/2242)) ([d96550f](https://github.com/morluto/rea/commit/d96550f2f61647ffa150597de73363fa90c4e169))
---
This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
-2
View File
@@ -56,8 +56,6 @@ export const SCOPES = {
"tests/boundary/filesystem/setupUnavailableProvider.test.ts",
"tests/boundary/filesystem/windowsNpxRegistration.test.ts",
"tests/acceptance/setup/homeDirectoryFallback.test.ts",
"tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts",
"src/domain/javascript/javascriptRuntimeStaticCandidates.ts",
"tests/boundary/filesystem/webScriptManifestPaths.test.ts",
"tests/boundary/process/ProcessEvidence.host.test.ts",
"tests/acceptance/webModuleCli.test.ts",
-9
View File
@@ -1,9 +0,0 @@
export function assertFeatureTrace(
trace: unknown,
procedure: { readonly address: string },
): void;
export function findMappedNonFunction(
strings: readonly { readonly address: string }[],
resolveAt: (address: string) => Promise<unknown>,
): Promise<string | undefined>;
-52
View File
@@ -1,52 +0,0 @@
import assert from "node:assert/strict";
const NEGATIVE_REASONS = new Set(["not_in_procedure", "outside_segments"]);
/**
* Assert the `trace_feature` result of the real IDA lane for one selected
* procedure. The query is a substring search, so the trace may also match other
* procedures and strings; each reference is checked against its own matched
* target, and a source outside every function (a data reference) is valid.
*/
export function assertFeatureTrace(trace, procedure) {
assert.ok(
trace.matches.some(
({ type, address }) =>
type === "procedure" && address === procedure.address,
),
"The trace omitted the selected procedure's match.",
);
assert.equal(typeof trace.truncated, "boolean");
const matched = new Set(trace.matches.map(({ address }) => address));
for (const reference of trace.references) {
assert.ok(
matched.has(reference.target_address),
`A reference targets ${reference.target_address}, which is not a match.`,
);
const { containing_procedure: containing } = reference;
if (containing.found)
assert.equal(typeof containing.procedure.address, "string");
else assert.ok(NEGATIVE_REASONS.has(containing.reason));
}
}
/**
* Find a string that IDA maps outside every function. A string inside a
* function chunk (inline data in code) is valid and skipped, so the strings are
* tried in order. Any other answer must be "not in a procedure": IDA lists only
* mapped strings, so an unmapped answer is a containment defect and fails the
* lane. Returns the address, or undefined when every string is inside a function.
*/
export async function findMappedNonFunction(strings, resolveAt) {
for (const { address } of strings) {
const resolved = await resolveAt(address);
if (resolved.found) continue;
assert.equal(
resolved.reason,
"not_in_procedure",
`The listed string at ${address} resolved as ${resolved.reason}.`,
);
return address;
}
return undefined;
}
+17 -30
View File
@@ -8,7 +8,7 @@ import {
realpath,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve, sep } from "node:path";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { createHash } from "node:crypto";
import { exec } from "./lib/verify-package-core.mjs";
@@ -18,6 +18,10 @@ import {
toolContract,
} from "../dist/contracts/toolContracts.js";
assert(
process.platform !== "win32",
"The Qwen real-client verifier currently requires a POSIX host; native Windows is unverified.",
);
const repo = fileURLToPath(new URL("..", import.meta.url));
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
const lab = await mkdtemp(join(tmpdir(), "rea-qwen-client-"));
@@ -28,13 +32,6 @@ assert(
"Usage: node scripts/verify-qwen-client.mjs [chat|call]",
);
const command = process.env.REA_VERIFY_QWEN_COMMAND ?? "qwen";
// npm exposes only qwen.cmd on Windows. Resolve a selected JavaScript entry
// before the verifier switches to its disposable workspace.
const entryPoint = /\.(c|m)?js$/iu.test(command) ? resolve(command) : undefined;
const launch = (args) =>
entryPoint === undefined
? [command, args]
: [process.execPath, [entryPoint, ...args]];
const verifier = createVerifierRun();
const profile = join(lab, "selected profile");
const account = join(lab, "account");
@@ -57,18 +54,14 @@ const baseEnvironment = {
let clientVersion;
try {
clientVersion = (
await exec(...launch(["--version"]), {
await exec(command, ["--version"], {
env: baseEnvironment,
timeout: 30_000,
})
).stdout.trim();
} catch (cause) {
const windowsHint =
process.platform === "win32"
? " On Windows, select node_modules/@qwen-code/qwen-code/cli-entry.js."
: "";
throw new Error(
`Qwen client preflight failed for ${command}; install Qwen Code or set REA_VERIFY_QWEN_COMMAND.${windowsHint} ${String(cause)}`,
`Qwen client preflight failed for ${command}; install Qwen Code or set REA_VERIFY_QWEN_COMMAND. ${String(cause)}`,
);
}
const original = JSON.stringify(
@@ -100,8 +93,8 @@ for (const step of [
assert(
result.plannedActions.every(
(a) =>
a.target.startsWith(profile + sep) ||
a.target.startsWith(sharedSkills + sep),
a.target.startsWith(profile + "/") ||
a.target.startsWith(sharedSkills + "/"),
),
);
await writeFile(
@@ -170,14 +163,7 @@ const fixture = await createOpenAiModelFixture({
skillSeen ||= JSON.stringify(body.messages ?? []).includes(
"reverse-engineer-anything",
);
const messageText = (body.messages ?? [])
.map((message) =>
typeof message.content === "string"
? message.content
: (message.content ?? []).map((part) => part.text ?? "").join("\n"),
)
.join("\n");
selectedSkillLoaded ||= messageText.includes(
selectedSkillLoaded ||= JSON.stringify(body.messages ?? []).includes(
join(sharedSkills, "reverse-engineer-anything"),
);
const toolText = JSON.stringify(
@@ -257,7 +243,9 @@ const fixture = await createOpenAiModelFixture({
const invoke = tools.find((t) => t.name === "tool_call");
const skill = tools.find((t) => t.name === "skill");
const shell = tools.find((t) => t.name === "run_shell_command");
const persisted = messageText.match(/Full output saved to: ([^\r\n]+)/u);
const persisted = toolText.match(
/Full output saved to: ([^\\"\n]+)(?:\\n|\n)/u,
);
const call = (name, args) => ({
role: "assistant",
content: null,
@@ -280,14 +268,13 @@ const fixture = await createOpenAiModelFixture({
artifactPath = persisted[1];
assert(
(await realpath(artifactPath)).startsWith(
(await realpath(runtime)) + sep,
(await realpath(runtime)) + "/",
),
);
const script = `const fs=require('node:fs');const e=JSON.parse(fs.readFileSync(${JSON.stringify(artifactPath)},'utf8'));console.log(JSON.stringify({REA_CLIENT_FIXTURE:JSON.stringify(e.normalized_result).includes('REA_CLIENT_FIXTURE'),evidence_id:e.evidence_id,graph_nodes:e.normalized_result.graph.nodes.length}));`;
// Keep paths and JSON quotes out of shell syntax on Windows and POSIX.
const encodedScript = Buffer.from(script).toString("base64");
const quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'";
message = call(shell.name, {
command: `node -e "eval(Buffer.from('${encodedScript}','base64').toString('utf8'))"`,
command: `node -e ${quote(script)}`,
description:
"Read the complete saved REA Evidence and verify the fixture facts.",
});
@@ -342,7 +329,7 @@ const args = [
let response;
let failure;
try {
response = await exec(...launch(args), {
response = await exec(command, args, {
env,
cwd: workspace,
timeout: 120_000,
-479
View File
@@ -1,479 +0,0 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import {
mkdir,
mkdtemp,
readFile,
readdir,
rm,
symlink,
unlink,
writeFile,
} from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../dist/domain/evidence.js";
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
if (process.argv.length !== 2)
throw new Error("Usage: node scripts/verify-real-ghidra-6502.mjs");
if (process.env.GHIDRA_INSTALL_DIR === undefined)
throw new Error(
"Set GHIDRA_INSTALL_DIR to a bring-your-own Ghidra installation.",
);
const exec = promisify(execFile);
const run = createVerifierRun();
const root = fileURLToPath(new URL("..", import.meta.url));
const ca65 = process.env.REA_CA65 ?? "ca65";
const ld65 = process.env.REA_LD65 ?? "ld65";
const toolchain = {};
for (const [name, command] of Object.entries({ ca65, ld65 })) {
const version = await exec(command, ["--version"], { timeout: 10000 });
toolchain[name] = (version.stdout + version.stderr).trim();
}
await mkdir(join(root, "build"), { recursive: true });
const workspace = await mkdtemp(join(root, "build", "rea-6502-proof-"));
const runtime = join(workspace, "runtime");
await mkdir(runtime);
const path = join(workspace, "image.bin");
const object = join(workspace, "image.o");
const entrypoint = join(root, "scripts", "rea.mjs");
const env = {
...process.env,
TMPDIR: runtime,
TMP: runtime,
TEMP: runtime,
REA_LOG_LEVEL: "silent",
REA_ANALYSIS_PROVIDER: "ghidra",
REA_PROCESS_RUN_ID: run.run_id,
};
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({ name: "rea-real-6502-proof", version: "1" });
let stderr = "";
transport.stderr?.on("data", (chunk) => {
stderr = (stderr + chunk.toString()).slice(-65536);
});
let report;
try {
await exec(
ca65,
[join(root, "tests/conformance/ghidra/6502.s"), "-o", object],
{ timeout: 10000 },
);
await exec(
ld65,
["-C", join(root, "tests/conformance/ghidra/6502.cfg"), "-o", path, object],
{ timeout: 10000 },
);
const bytes = await readFile(path);
// Independently known opcodes and link addresses from the source fixture.
assert.equal(bytes.toString("hex"), "a907201080c90ad003a92a60a9006018690360");
const sha256 = createHash("sha256").update(bytes).digest("hex");
await client.connect(transport);
const opened = await call("open_binary", {
path,
format: "raw-binary",
architecture: "6502",
entry_point: "0x008001",
provider_id: "ghidra",
});
assert.equal(opened.architecture, "6502");
assert.equal(opened.entry_point, "0x8001");
const status = await call("binary_session");
const image = evidence(await call("inspect_native_load_image"), sha256);
assert.equal(
image.normalized_result.status,
"verified",
JSON.stringify(image.normalized_result),
);
assert.equal(image.normalized_result.architecture, "6502");
assert.equal(image.normalized_result.base_address, "0x8001");
assert.equal(image.normalized_result.language_id, "6502:LE:16:default");
assert.equal(image.normalized_result.compiler_spec_id, "default");
assert.equal(image.normalized_result.file_bytes, bytes.length);
const memory = evidence(
await call("read_bytes", { address: "0x8001", length: bytes.length }),
sha256,
);
assert.equal(memory.normalized_result.bytes_hex, bytes.toString("hex"));
const procedures = evidence(await call("list_procedures"), sha256);
for (const address of ["0x8001", "0x8010"])
assert.ok(
procedures.normalized_result.some(
(procedure) => procedure.address === address,
),
);
const instruction = evidence(
await call("inspect_native_instruction", { address: "0x8003" }),
sha256,
);
assert.equal(instruction.normalized_result.mnemonic.toUpperCase(), "JSR");
assert.ok(
instruction.normalized_result.references.some(
(reference) => reference.call && reference.target_address === "0x8010",
),
);
const pseudo = evidence(
await call("procedure_pseudo_code", { procedure: "0x8010" }),
sha256,
);
assert.match(pseudo.normalized_result, /\+\s*(?:3|0x3|'\\x03')/u);
const filePseudo = evidence(
await call("procedure_pseudo_code", { procedure: "file-offset:0xf" }),
sha256,
);
assert.equal(filePseudo.normalized_result, pseudo.normalized_result);
assert.ok(
filePseudo.locations.some(
(location) => location.kind === "file-offset" && location.offset === 15,
),
);
const rejected = await client.callTool({
name: "open_binary",
arguments: {
path,
format: "raw-binary",
architecture: "6502",
entry_point: "0xffff",
},
});
assert.equal(rejected.isError, true);
assert.match(JSON.stringify(rejected.content), /6502 address width/u);
const preserved = await call("binary_session");
assert.equal(preserved.analysis_run.run_id, status.analysis_run.run_id);
assert.equal(preserved.entry_point, "0x8001");
await call("close_binary");
await client.ping();
const { stdout } = await exec(
process.execPath,
[
entrypoint,
"inspect-native-load-image",
path,
"--target-format",
"raw-binary",
"--architecture",
"6502",
"--entry-point",
"0x8001",
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
const cli = parseEvidence(JSON.parse(stdout));
assert.deepEqual(cli.normalized_result, image.normalized_result);
assert.equal(cli.analysis_profile.digest, image.analysis_profile.digest);
assert.equal(cli.subject.digest.sha256, sha256);
const fileCli = await exec(
process.execPath,
[
entrypoint,
"decompile",
path,
"--file-offset=0xf",
"--target-format",
"raw-binary",
"--architecture",
"6502",
"--entry-point",
"0x8001",
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
const fileCliEvidence = parseEvidence(JSON.parse(fileCli.stdout));
assert.equal(fileCliEvidence.normalized_result, filePseudo.normalized_result);
assert.deepEqual(fileCliEvidence.locations, filePseudo.locations);
assert.equal(fileCliEvidence.subject.digest.sha256, sha256);
assert.deepEqual(await readFile(path), bytes);
assert.deepEqual(await readdir(runtime), []);
const lowMemory = [];
// BinaryLoader maps file bytes before trying processor-defined blocks. Prove
// that overlapping zero-page/stack definitions never overlay the selected image.
for (const [entry, fullAddressSpace] of [
[0xff, false],
[0, true],
]) {
const lowPath = join(workspace, `low-${entry}.bin`);
await exec(
ld65,
[
"-C",
join(root, "tests/conformance/ghidra/6502.cfg"),
"--start-addr",
String(entry),
"-o",
lowPath,
object,
],
{ timeout: 10000 },
);
const linked = await readFile(lowPath);
const expectedCode = Buffer.from(
"a907200000c90ad003a92a60a9006018690360",
"hex",
);
expectedCode.writeUInt16LE(entry + 15, 3);
assert.deepEqual(linked, expectedCode);
const lowBytes = fullAddressSpace
? Buffer.concat([linked, Buffer.alloc(65536 - linked.length)])
: linked;
await writeFile(lowPath, lowBytes);
const lowSha256 = createHash("sha256").update(lowBytes).digest("hex");
const lowEntry = `0x${entry.toString(16)}`;
const lowOpened = await call("open_binary", {
path: lowPath,
format: "raw-binary",
architecture: "6502",
entry_point: lowEntry,
provider_id: "ghidra",
});
assert.equal(lowOpened.entry_point, lowEntry);
const lowImage = evidence(
await call("inspect_native_load_image"),
lowSha256,
);
assert.equal(
lowImage.normalized_result.status,
"verified",
JSON.stringify(lowImage.normalized_result),
);
assert.equal(lowImage.normalized_result.base_address, lowEntry);
assert.equal(lowImage.normalized_result.file_bytes, lowBytes.length);
const fileMappings =
lowImage.normalized_result.observations.mappings.filter(
(mapping) => mapping.source_file_index !== null,
);
assert.equal(fileMappings.length, 1);
assert.equal(
fileMappings[0].address_space,
lowImage.normalized_result.observations.default_address_space,
);
assert.equal(fileMappings[0].overlay, false);
assert.equal(fileMappings[0].start, lowEntry);
assert.equal(fileMappings[0].length, lowBytes.length);
const lowRead = evidence(
await call("read_bytes", { address: lowEntry, length: linked.length }),
lowSha256,
);
assert.equal(lowRead.normalized_result.bytes_hex, linked.toString("hex"));
const lowCall = evidence(
await call("inspect_native_instruction", {
address: `0x${(entry + 2).toString(16)}`,
}),
lowSha256,
);
assert.equal(lowCall.normalized_result.mnemonic.toUpperCase(), "JSR");
assert.ok(
lowCall.normalized_result.references.some(
(reference) =>
reference.call &&
reference.target_address === `0x${(entry + 15).toString(16)}`,
),
);
await call("close_binary");
const lowCli = await exec(
process.execPath,
[
entrypoint,
"inspect-native-load-image",
lowPath,
"--target-format",
"raw-binary",
"--architecture",
"6502",
"--entry-point",
lowEntry,
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
const lowCliEvidence = evidence(JSON.parse(lowCli.stdout), lowSha256);
assert.deepEqual(
lowCliEvidence.normalized_result,
lowImage.normalized_result,
);
assert.equal(
lowCliEvidence.analysis_profile.digest,
lowImage.analysis_profile.digest,
);
assert.deepEqual(await readFile(lowPath), lowBytes);
assert.deepEqual(await readdir(runtime), []);
lowMemory.push({
entry: lowEntry,
bytes: lowBytes.length,
sha256: lowSha256,
});
}
let windowsReparseAdmission = "not-applicable";
if (process.platform === "win32") {
const junction = join(workspace, "linked");
await symlink(workspace, junction, "junction");
try {
const linkedPath = join(junction, "image.bin");
// Reopening the same bytes through a junction must replace the direct session.
await call("open_binary", {
path,
format: "raw-binary",
architecture: "6502",
entry_point: "0x8001",
provider_id: "ghidra",
});
const directImage = evidence(
await call("inspect_native_load_image"),
sha256,
);
assert.equal(directImage.normalized_result.status, "verified");
// Canonical file identity must not bypass admission of the new path.
await call("open_binary", {
path: linkedPath,
format: "raw-binary",
architecture: "6502",
entry_point: "0x8001",
provider_id: "ghidra",
});
// open_binary selects a lazy session; the first analysis admits the snapshot.
const linkedImage = await client.callTool(
{ name: "inspect_native_load_image", arguments: {} },
{ timeout: 240000 },
);
assert.equal(linkedImage.isError, true);
assert.match(
JSON.stringify(linkedImage),
/Reparse path component is unsupported/u,
);
await call("close_binary");
const snapshotPath = join(workspace, "direct-snapshot.json");
const cliArgs = (selectedPath) => [
entrypoint,
"analyze",
selectedPath,
"--target-format",
"raw-binary",
"--architecture",
"6502",
"--entry-point",
"0x8001",
"--provider",
"ghidra",
"--snapshot",
snapshotPath,
"--json",
];
const saved = await exec(process.execPath, cliArgs(path), {
env,
timeout: 240000,
maxBuffer: 16 * 1024 * 1024,
});
assert.equal(
evidence(JSON.parse(saved.stdout), sha256).operation,
"binary_overview",
);
const savedSnapshot = JSON.parse(await readFile(snapshotPath, "utf8"));
assert.ok(
savedSnapshot.workflow_entries.some(
({ operation }) => operation === "binary_overview",
),
);
assert.deepEqual(await readdir(runtime), []);
// Replaying a matching snapshot must still reach native path admission.
await assert.rejects(
exec(process.execPath, cliArgs(linkedPath), {
env,
timeout: 240000,
maxBuffer: 16 * 1024 * 1024,
}),
(cause) => {
assert.match(
cause.stdout + cause.stderr,
/Reparse path component is unsupported/u,
);
return true;
},
);
assert.deepEqual(await readFile(path), bytes);
assert.deepEqual(await readdir(runtime), []);
windowsReparseAdmission = "cli-snapshot-mcp-rejected";
} finally {
await unlink(junction);
}
}
report = {
ok: true,
mocked: false,
lane: "ghidra-6502",
host: process.platform,
toolchain,
provider: image.provider,
target: {
sha256,
bytes: bytes.length,
architecture: "6502",
entry: "0x8001",
},
cli_mcp_load_image_parity: true,
instructions_and_call_reference: true,
decompilation: true,
file_offset_decompilation_cli_mcp: true,
invalid_mapping_preserves_session: true,
low_memory_cli_mcp_parity: lowMemory,
windows_reparse_admission: windowsReparseAdmission,
source_unchanged: true,
runtime_cleanup: "complete",
};
} catch (cause) {
if (stderr) process.stderr.write(stderr);
throw cause;
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
const verifierRun = await completeVerifierRun(run);
if (process.platform !== "win32") {
assert.equal(verifierRun.process_lineage.status, "verified");
assert.deepEqual(verifierRun.process_lineage.descendants, []);
}
// Keep the workspace for diagnosis unless verification and cleanup both pass.
await rm(workspace, { recursive: true, force: true });
process.stdout.write(
JSON.stringify({ verifier_run: verifierRun, ...report }) + "\n",
);
async function call(name, args = {}) {
const result = await client.callTool(
{ name, arguments: args },
{ timeout: 240000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
if (["open_binary", "binary_session", "close_binary"].includes(name))
return requireMcpOperationResult(result, name);
return result.structuredContent;
}
function evidence(value, sha256) {
const result = parseEvidence(value);
assert.equal(result.subject.digest.sha256, sha256);
assert.equal(result.subject.architecture, "6502");
assert.equal(result.subject.format, "raw-binary");
return result;
}
-68
View File
@@ -45,13 +45,8 @@ const transport = new StdioClientTransport({
const source = fileURLToPath(
new URL("../tests/conformance/swift/type-inventory.swift", import.meta.url),
);
const literalSource = fileURLToPath(
new URL("../tests/conformance/swift/string-literals.swift", import.meta.url),
);
const binary = join(root, "swift-inventory");
const literalBinary = join(root, "swift-literals");
const reports = [];
let literalReport;
try {
await execute("swiftc", ["-O", "-g", source, "-o", binary], {
timeout: 60_000,
@@ -59,19 +54,6 @@ try {
await mkdir(join(root, "copied"));
const copied = join(root, "copied", "swift-inventory");
await copyFile(binary, copied);
// Decoding covers AArch64 register pairs, so build that slice on any Mac host.
await execute(
"swiftc",
[
"-O",
"-target",
"arm64-apple-macosx13.0",
literalSource,
"-o",
literalBinary,
],
{ timeout: 60_000 },
);
await client.connect(transport);
for (const [variant, path] of [
["debug", binary],
@@ -150,7 +132,6 @@ try {
await call("close_binary", {});
}
}
literalReport = await verifyStringLiterals();
} finally {
await client.close();
await transport.close();
@@ -162,7 +143,6 @@ console.log(
ok: true,
provider: "ghidra",
fixtures: reports,
swift_string_literals: literalReport,
verifier_run: await completeVerifierRun(run),
},
null,
@@ -179,51 +159,3 @@ async function call(name, arguments_) {
toolContract(name).outputSchema.parse(response.structuredContent);
return requireMcpOperationResult(response, name);
}
// Literal text lives only in register constants, so it must come from the decoded pairs.
async function verifyStringLiterals() {
await call("open_binary", { path: literalBinary, provider_id: "ghidra" });
try {
const procedures = await call("list_procedures", {});
const expected = {
smallLiteral: [["SleepDisabled", "small"]],
largeLiteral: [["The power setting could not be changed.", "large"]],
unicodeLiterals: [
["café", "small"],
["Résumé of the naïve décor, written twice", "large"],
],
emptyAndControlLiterals: [
["", "small"],
["tab\there", "small"],
],
returnedLiteral: [["Holding awake", "small"]],
};
const decoded = {};
for (const [name, literals] of Object.entries(expected)) {
const procedure = procedures.find(({ value }) => value.includes(name));
assert(procedure, `${name} is missing from the procedure inventory`);
const dossier = await call("analyze_function", {
procedure: procedure.address,
});
const observed = dossier.swift_string_literals;
assert(Array.isArray(observed), `${name} recorded no Swift literals`);
assert.deepEqual(dossier.swift_string_literal_omissions, []);
assert.deepEqual(
observed.map(({ value, representation }) => [value, representation]),
literals,
);
for (const literal of observed) {
assert.deepEqual(literal.registers, ["x0", "x1"]);
assert.equal(
literal.address === null,
literal.representation === "small",
`${literal.value} storage does not match its representation`,
);
}
decoded[name] = observed.length;
}
return decoded;
} finally {
await call("close_binary", {});
}
}
+1 -60
View File
@@ -1,8 +1,4 @@
#!/usr/bin/env node
import {
assertFeatureTrace,
findMappedNonFunction,
} from "./lib/ida-feature-trace.mjs";
import { requireMcpToolError } from "./lib/mcp-verifier-results.mjs";
import assert from "node:assert/strict";
@@ -10,7 +6,7 @@ import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { createReadStream } from "node:fs";
import { access, writeFile } from "node:fs/promises";
import { basename, dirname, join, resolve } from "node:path";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { parseArgs, promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
@@ -265,61 +261,6 @@ const mcp = async (cliDossier) => {
break;
}
}
if (config.mode === "headless") {
const documents = observe(await call("list_documents", {}));
assert.equal(documents.length, 1);
assert.equal(basename(documents[0]), basename(target));
assert.ok(
!workspaces.has(dirname(documents[0])),
"list_documents reported the private workspace copy.",
);
const resolveAt = async (address) =>
observe(await call("resolve_containing_procedure", { address }));
const instructionAddresses = dossier.assembly
.map((line) => /^(0x[0-9a-f]+)/iu.exec(line)?.[1])
.filter((address) => address !== undefined);
const queries = [
dossier.procedure.address,
...instructionAddresses.slice(1, 2),
];
for (const queried of queries) {
const containing = await resolveAt(queried);
assert.equal(containing.found, true);
assert.equal(containing.procedure.address, dossier.procedure.address);
assert.equal(containing.procedure.name, dossier.procedure.name);
}
const unmapped = await resolveAt("0xffffffffffffff00");
assert.equal(unmapped.found, false);
assert.equal(unmapped.reason, "outside_segments");
const strings = observe(await call("list_strings", {}));
const mappedString = await findMappedNonFunction(strings, resolveAt);
if (mappedString === undefined)
report.unverified.push(
"Mapped non-function containment: no string of the target is mapped outside every function.",
);
const context = await client.callTool({
name: "inspect_address_context",
arguments: { address: dossier.procedure.address },
});
assert.notEqual(context.isError, true);
const contextResult = context.structuredContent.result;
assert.equal(contextResult.document, documents[0]);
assert.equal(contextResult.procedure.state, "available");
assert.equal(
contextResult.procedure.value.procedure.address,
dossier.procedure.address,
);
const traced = await client.callTool({
name: "trace_feature",
arguments: { query: dossier.procedure.name },
});
assert.notEqual(traced.isError, true);
const trace = traced.structuredContent.normalized_result;
assertFeatureTrace(trace, dossier.procedure);
report.checks.push(
"headless containing procedures, the admitted program as the document, address context, and the procedure match and references of feature tracing",
);
}
if (values["expected-string"] === undefined)
report.unverified.push(
"String inventory/search correctness: no independent expected literal supplied.",
+2 -2
View File
@@ -27,9 +27,9 @@ if (
);
const offset = bytes.length >= 64 ? bytes.readUInt32LE(0x3c) : bytes.length;
if (
bytes.toString("latin1", 0, 2) !== "MZ" ||
bytes.toString("ascii", 0, 2) !== "MZ" ||
offset > bytes.length - 26 ||
bytes.toString("latin1", offset, offset + 4) !== "PE\0\0" ||
bytes.toString("ascii", offset, offset + 4) !== "PE\0\0" ||
bytes.readUInt16LE(offset + 4) !== 0x8664 ||
bytes.readUInt16LE(offset + 24) !== 0x20b
)
+203 -297
View File
@@ -1,8 +1,15 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import * as fs from "node:fs/promises";
import {
access,
mkdir,
mkdtemp,
readFile,
rm,
symlink,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import * as nodePath from "node:path";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
@@ -11,348 +18,247 @@ import { firmwareResultSchemas } from "../../../dist/domain/firmware/firmwareAna
import { parseEvidence } from "../../../dist/domain/evidence.js";
import { hashFirmwareFile } from "../../../dist/firmware/FirmwareFiles.js";
const execFileAsync = promisify(execFile);
const { join, resolve, dirname } = nodePath;
const CALL_TIMEOUT_MS = 150_000;
const CONNECT_TIMEOUT_MS = 30_000;
const SEVENZIP_TIMEOUT_MS = 10_000;
const MAX_STDOUT_BYTES = 16 * 1024 * 1024;
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
const ENTRYPOINT = resolve(
const exec = promisify(execFile);
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
const entrypoint = resolve(
process.env.REA_FIRMWARE_TEST_ENTRYPOINT ??
join(REPO_ROOT, "scripts/rea.mjs"),
join(repository, "scripts/rea.mjs"),
);
const FIXTURE_ROOT = resolve(
process.env.REA_FIRMWARE_FIXTURE_ROOT ??
join(REPO_ROOT, "_reference/firmware-integration/generated"),
);
// ---------------------------------------------------------------------------
// Prerequisite checks
// ---------------------------------------------------------------------------
async function checkPrerequisites() {
if (process.platform !== "linux") {
if (process.platform !== "linux")
throw new Error(
"verify:firmware requires Linux; other hosts are not verified",
);
for (const variable of ["REA_BINWALK_COMMAND", "REA_UNBLOB_COMMAND"]) {
const path = process.env[variable];
if (path === undefined)
throw new Error(
"verify:firmware requires Linux; other hosts are not verified",
`verify:firmware requires ${variable}; see docs/firmware-analysis.md`,
);
}
for (const name of ["REA_BINWALK_COMMAND", "REA_UNBLOB_COMMAND"]) {
const value = process.env[name];
if (value === undefined) {
throw new Error(
`verify:firmware requires ${name}; see docs/firmware-analysis.md`,
);
}
await fs.access(value);
}
try {
await execFileAsync("7z", ["i"], { timeout: SEVENZIP_TIMEOUT_MS });
} catch (cause) {
throw new Error(
"verify:firmware prerequisite unavailable: Unblob's gzip/USTAR fixture extraction requires 7z on PATH",
{ cause },
);
}
try {
await fs.access(join(FIXTURE_ROOT, "oracle.json"));
} catch (cause) {
throw new Error(
"verify:firmware requires generated fixtures; run npm run fixtures:firmware",
{ cause },
);
}
await access(path);
}
await checkPrerequisites();
// ---------------------------------------------------------------------------
// Shared state
// ---------------------------------------------------------------------------
const oracle = JSON.parse(
await fs.readFile(join(FIXTURE_ROOT, "oracle.json"), "utf8"),
try {
await exec("7z", ["i"], { timeout: 10_000 });
} catch (cause) {
throw new Error(
"verify:firmware prerequisite unavailable: Unblob's gzip/USTAR fixture extraction requires 7z on PATH",
{ cause },
);
}
const fixtureRoot = resolve(
process.env.REA_FIRMWARE_FIXTURE_ROOT ??
join(repository, "_reference/firmware-integration/generated"),
);
const firmwarePath = join(FIXTURE_ROOT, "firmware.bin");
assert.equal(await hashFirmwareFile(firmwarePath), oracle.sha256);
const workDir = await fs.mkdtemp(join(tmpdir(), "rea-real-firmware-"));
const baseEnv = Object.fromEntries(
Object.entries(process.env).filter(([, v]) => v !== undefined),
);
const mcpClient = new Client({
name: "rea-real-firmware-verifier",
version: "1",
await access(join(fixtureRoot, "oracle.json")).catch((cause) => {
throw new Error(
"verify:firmware requires generated fixtures; run npm run fixtures:firmware",
{ cause },
);
});
// ---------------------------------------------------------------------------
// Invocation helpers
// ---------------------------------------------------------------------------
async function runCli(args, env = baseEnv) {
const { stdout } = await execFileAsync(
const oracle = JSON.parse(
await readFile(join(fixtureRoot, "oracle.json"), "utf8"),
);
const path = join(fixtureRoot, "firmware.bin");
assert.equal(await hashFirmwareFile(path), oracle.sha256);
const root = await mkdtemp(join(tmpdir(), "rea-real-firmware-"));
const environment = Object.fromEntries(
Object.entries(process.env).filter(([, value]) => value !== undefined),
);
const client = new Client({ name: "rea-real-firmware-verifier", version: "1" });
const cli = async (args, env = environment) => {
const response = await exec(
process.execPath,
[ENTRYPOINT, ...args, "--format", "json"],
[entrypoint, ...args, "--format", "json"],
{
env,
cwd: REPO_ROOT,
timeout: CALL_TIMEOUT_MS,
maxBuffer: MAX_STDOUT_BYTES,
cwd: repository,
timeout: 150_000,
maxBuffer: 16 * 1024 * 1024,
},
);
return parseEvidence(JSON.parse(stdout));
}
async function callTool(name, args) {
const result = await mcpClient.callTool(
return parseEvidence(JSON.parse(response.stdout));
};
const call = async (name, args) => {
const response = await client.callTool(
{ name, arguments: args },
{ timeout: CALL_TIMEOUT_MS },
{ timeout: 150_000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
assert.ok(result.structuredContent !== undefined);
return result.structuredContent;
}
// ---------------------------------------------------------------------------
// Result verifiers
// ---------------------------------------------------------------------------
function verifyRegions(evidence) {
assert.notEqual(response.isError, true, JSON.stringify(response));
assert.ok(response.structuredContent !== undefined);
return response.structuredContent;
};
const verifyRegions = (evidence) => {
assert.equal(evidence.subject.digest.sha256, oracle.sha256);
const parsed = firmwareResultSchemas.inspect_firmware_regions.parse(
const result = firmwareResultSchemas.inspect_firmware_regions.parse(
evidence.normalized_result,
);
const gzipRegion = parsed.regions.find(
(r) => r.signature === "gzip" && r.offset === oracle.offset,
const gzip = result.regions.find(
(region) => region.signature === "gzip" && region.offset === oracle.offset,
);
assert.ok(
gzipRegion !== undefined,
gzip !== undefined,
"Binwalk must find the independently generated gzip offset",
);
assert.equal(gzipRegion.reported_size, oracle.length);
assert.equal(gzipRegion.size_basis, "provider_reported_validation_unknown");
// Strip provider-specific ids so CLI and MCP results compare equal.
return parsed.regions.map((region) => {
const { provider_id: _ignored, ...rest } = region;
return rest;
});
}
async function verifyFiles(evidence) {
const parsed = firmwareResultSchemas.extract_firmware.parse(
assert.equal(gzip.reported_size, oracle.length);
assert.equal(gzip.size_basis, "provider_reported_validation_unknown");
return result.regions.map(
({ provider_id: _provider_id, ...region }) => region,
);
};
const verifyFiles = async (evidence) => {
const result = firmwareResultSchemas.extract_firmware.parse(
evidence.normalized_result,
);
assert.ok(parsed.chunks.some((c) => c.handler === "gzip"));
assert.ok(result.chunks.some((chunk) => chunk.handler === "gzip"));
for (const [suffix, expected] of Object.entries(oracle.files)) {
const entry = parsed.files.find((f) =>
f.relative_path.endsWith(`/${suffix}`),
const file = result.files.find((candidate) =>
candidate.relative_path.endsWith(`/${suffix}`),
);
assert.ok(entry !== undefined, `Missing extracted ${suffix}`);
assert.equal(entry.sha256, expected.sha256);
assert.equal(entry.size, expected.size);
assert.equal(await hashFirmwareFile(entry.path), expected.sha256);
assert.equal(entry.original_file_range, null);
assert.equal(entry.runtime_address, null);
assert.ok(file !== undefined, `Missing extracted ${suffix}`);
assert.equal(file.sha256, expected.sha256);
assert.equal(file.size, expected.size);
assert.equal(await hashFirmwareFile(file.path), expected.sha256);
assert.equal(file.original_file_range, null);
assert.equal(file.runtime_address, null);
}
return parsed;
}
const toContentIdentity = (parsed) =>
parsed.files.map(({ relative_path, sha256, size }) => ({
relative_path,
sha256,
size,
}));
// ---------------------------------------------------------------------------
// Verification stages
// ---------------------------------------------------------------------------
async function stageCli() {
const regionsEvidence = await runCli([
"inspect-firmware-regions",
firmwarePath,
]);
const regions = verifyRegions(regionsEvidence);
const extractEvidence = await runCli([
return result;
};
try {
const inspected = await cli(["inspect-firmware-regions", path]);
const cliRegions = verifyRegions(inspected);
const extracted = await cli([
"extract-firmware",
firmwarePath,
join(workDir, "cli-output"),
path,
join(root, "cli-output"),
]);
const files = await verifyFiles(extractEvidence);
assert.equal(files.coverage, "partial");
const cliFiles = await verifyFiles(extracted);
assert.equal(cliFiles.coverage, "partial");
assert.ok(
files.chunks.some(
(c) =>
c.handler === null && c.range.offset === 0 && c.range.length === 64,
cliFiles.chunks.some(
(chunk) =>
chunk.handler === null &&
chunk.range.offset === 0 &&
chunk.range.length === 64,
),
);
return { regions, files };
}
async function connectMcp() {
const transport = new StdioClientTransport({
command: process.execPath,
args: [ENTRYPOINT, "mcp"],
cwd: REPO_ROOT,
env: baseEnv,
stderr: "pipe",
await client.connect(
new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
cwd: repository,
env: environment,
stderr: "pipe",
}),
{ timeout: 30_000 },
);
const inspectedMcp = await call("inspect_firmware_regions", { path });
const mcpEvidence = parseEvidence(inspectedMcp);
assert.deepEqual(verifyRegions(mcpEvidence), cliRegions);
const extractedMcp = await call("extract_firmware", {
path,
output_directory: join(root, "mcp-output"),
});
await mcpClient.connect(transport, { timeout: CONNECT_TIMEOUT_MS });
}
async function stageMcp(cli) {
const regionsRaw = await callTool("inspect_firmware_regions", {
path: firmwarePath,
});
assert.deepEqual(verifyRegions(parseEvidence(regionsRaw)), cli.regions);
const extractRaw = await callTool("extract_firmware", {
path: firmwarePath,
output_directory: join(workDir, "mcp-output"),
});
const mcpFiles = await verifyFiles(parseEvidence(extractRaw));
assert.deepEqual(toContentIdentity(mcpFiles), toContentIdentity(cli.files));
const rangedRaw = await callTool("extract_firmware", {
path: firmwarePath,
output_directory: join(workDir, "selected-output"),
const mcpFiles = await verifyFiles(parseEvidence(extractedMcp));
const contentIdentity = (result) =>
result.files.map(({ relative_path, sha256, size }) => ({
relative_path,
sha256,
size,
}));
assert.deepEqual(contentIdentity(mcpFiles), contentIdentity(cliFiles));
const selected = await call("extract_firmware", {
path,
output_directory: join(root, "selected-output"),
range: { offset: oracle.offset, length: oracle.length },
});
const ranged = await verifyFiles(parseEvidence(rangedRaw));
assert.equal(ranged.selection.offset, oracle.offset);
assert.equal(ranged.selection.length, oracle.length);
const depthRaw = await callTool("extract_firmware", {
path: firmwarePath,
output_directory: join(workDir, "depth-output"),
const selectedFiles = await verifyFiles(parseEvidence(selected));
assert.equal(selectedFiles.selection.offset, oracle.offset);
assert.equal(selectedFiles.selection.length, oracle.length);
const depth = await call("extract_firmware", {
path,
output_directory: join(root, "depth-output"),
max_depth: 1,
});
const depthParsed = firmwareResultSchemas.extract_firmware.parse(
depthRaw.normalized_result,
const depthResult = firmwareResultSchemas.extract_firmware.parse(
depth.normalized_result,
);
assert.equal(depthParsed.coverage, "partial");
assert.ok(depthParsed.depth_limited_paths.length > 0);
assert.equal(depthResult.coverage, "partial");
assert.ok(depthResult.depth_limited_paths.length > 0);
console.log(
"PASS real Binwalk/Unblob: CLI, MCP, independent offsets/digests, range extraction, unknown chunks and depth limits",
);
return mcpFiles;
}
async function stageExt4() {
const ext4Path = join(FIXTURE_ROOT, "ext4.bin");
await fs.access(ext4Path);
const raw = await callTool("extract_firmware", {
path: ext4Path,
output_directory: join(workDir, "ext4-output"),
});
const parsed = firmwareResultSchemas.extract_firmware.parse(
raw.normalized_result,
);
assert.ok(parsed.chunks.some((c) => c.handler === "extfs"));
assert.equal(parsed.coverage, "complete");
for (const [suffix, expected] of Object.entries(oracle.files)) {
const match = parsed.files.find((f) =>
f.relative_path.endsWith(`/${suffix}`),
if (process.env.REA_FIRMWARE_VERIFY_EXT4 === "1") {
const ext4 = join(fixtureRoot, "ext4.bin");
await access(ext4);
const response = await call("extract_firmware", {
path: ext4,
output_directory: join(root, "ext4-output"),
});
const result = firmwareResultSchemas.extract_firmware.parse(
response.normalized_result,
);
assert.equal(match?.sha256, expected.sha256);
assert.ok(result.chunks.some((chunk) => chunk.handler === "extfs"));
assert.equal(result.coverage, "complete");
for (const [suffix, expected] of Object.entries(oracle.files))
assert.equal(
result.files.find((file) => file.relative_path.endsWith(`/${suffix}`))
?.sha256,
expected.sha256,
);
console.log("PASS real ext4 extraction with debugfs; no mounts");
// Preserve the process-ownership utility while withholding all extractors.
const withoutExtractors = join(root, "without-extractors");
await access("/bin/ps");
await mkdir(withoutExtractors);
await symlink("/bin/ps", join(withoutExtractors, "ps"));
const missing = await cli(
["extract-firmware", ext4, join(root, "missing-extractor-output")],
{ ...environment, PATH: withoutExtractors },
);
const partial = firmwareResultSchemas.extract_firmware.parse(
missing.normalized_result,
);
assert.equal(partial.coverage, "partial");
assert.ok(JSON.stringify(partial.diagnostics).includes("debugfs"));
console.log(
"PASS real missing-extractor diagnostics retained despite Unblob exit 1",
);
}
console.log("PASS real ext4 extraction with debugfs; no mounts");
// Keep the process-ownership utility available but hide every extractor.
const emptyBin = join(workDir, "without-extractors");
await fs.access("/bin/ps");
await fs.mkdir(emptyBin);
await fs.symlink("/bin/ps", join(emptyBin, "ps"));
const degraded = await runCli(
["extract-firmware", ext4Path, join(workDir, "missing-extractor-output")],
{ ...baseEnv, PATH: emptyBin },
);
const degradedParsed = firmwareResultSchemas.extract_firmware.parse(
degraded.normalized_result,
);
assert.equal(degradedParsed.coverage, "partial");
assert.ok(JSON.stringify(degradedParsed.diagnostics).includes("debugfs"));
console.log(
"PASS real missing-extractor diagnostics retained despite Unblob exit 1",
);
}
async function stageGhidra(mcpFiles) {
if (process.env.GHIDRA_INSTALL_DIR === undefined) {
throw new Error(
"Firmware Ghidra handoff requires GHIDRA_INSTALL_DIR and Java; see docs/ghidra.md",
if (process.env.REA_FIRMWARE_VERIFY_GHIDRA === "1") {
if (process.env.GHIDRA_INSTALL_DIR === undefined)
throw new Error(
"Firmware Ghidra handoff requires GHIDRA_INSTALL_DIR and Java; see docs/ghidra.md",
);
const child = mcpFiles.files.find((file) =>
file.relative_path.endsWith("/usr/bin/probe"),
);
assert.ok(child !== undefined);
await call("open_binary", { path: child.path, provider_id: "ghidra" });
const dossier = await call("analyze_function", {
procedure: "firmware_probe",
});
const evidence = parseEvidence(dossier);
assert.equal(
evidence.subject.digest.sha256,
oracle.files["usr/bin/probe"].sha256,
);
const pseudocode = await call("procedure_pseudo_code", {
procedure: "firmware_probe",
});
assert.equal(parseEvidence(pseudocode).provider.id, "ghidra");
assert.ok(
JSON.stringify(evidence.normalized_result).includes("firmware_probe"),
);
await call("close_binary", {});
console.log(
"PASS real Ghidra handoff of the selected extracted host ELF, bound to its digest",
);
}
const probe = mcpFiles.files.find((f) =>
f.relative_path.endsWith("/usr/bin/probe"),
);
assert.ok(probe !== undefined);
await callTool("open_binary", { path: probe.path, provider_id: "ghidra" });
const dossierRaw = await callTool("analyze_function", {
procedure: "firmware_probe",
});
const dossier = parseEvidence(dossierRaw);
assert.equal(
dossier.subject.digest.sha256,
oracle.files["usr/bin/probe"].sha256,
);
const pseudoRaw = await callTool("procedure_pseudo_code", {
procedure: "firmware_probe",
});
assert.equal(parseEvidence(pseudoRaw).provider.id, "ghidra");
assert.ok(
JSON.stringify(dossier.normalized_result).includes("firmware_probe"),
);
await callTool("close_binary", {});
console.log(
"PASS real Ghidra handoff of the selected extracted host ELF, bound to its digest",
);
}
// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------
try {
const cli = await stageCli();
await connectMcp();
const mcpFiles = await stageMcp(cli);
if (process.env.REA_FIRMWARE_VERIFY_EXT4 === "1") await stageExt4();
if (process.env.REA_FIRMWARE_VERIFY_GHIDRA === "1")
await stageGhidra(mcpFiles);
assert.equal(
await hashFirmwareFile(firmwarePath),
await hashFirmwareFile(path),
oracle.sha256,
"Original firmware bytes must remain unchanged",
);
} finally {
await mcpClient.close();
await fs.rm(workDir, { recursive: true, force: true });
await client.close();
await rm(root, { recursive: true, force: true });
}
-30
View File
@@ -810,36 +810,6 @@ exit 1`,
expect(result.error.message).toContain("overwrite");
});
it.each([
["offline", false, "error: device offline"],
["unauthorized", false, "error: device unauthorized"],
["offline", true, "error: device offline"],
] as const)(
"reports the %s device state when the push existence probe cannot run (overwrite %s)",
async (state, overwrite, stderr) => {
const { binary, root } = await writeAdbStub(
(stubRoot) => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
if [ "$1" = "-s" ]; then printf '%s\\n' "$3" >> "${stubRoot}/calls"; printf '%s\\n' "${stderr}" >&2; exit 1; fi`,
);
const localFile = join(root, "payload.bin");
await writeFile(localFile, "x");
const projected = await refusedBy(providerOver(binary), {
operation: "push_adb_file",
input: {
serial: "emulator-5554",
local_path: localFile,
device_path: "/data/local/tmp/payload.bin",
overwrite,
},
});
// Existence is unknown, so nothing is claimed about the path and no push runs.
expect(projected).toMatchObject({ code: "unsupported_target" });
expect(projected.message).toContain(state);
expect(projected.message).not.toContain("already exists");
expect(await readFile(join(root, "calls"), "utf8")).toBe("shell\n");
},
);
it("reports a local push source that does not exist", async () => {
const { binary } = await writeAdbStub(
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
+10 -8
View File
@@ -1623,15 +1623,17 @@ export class AdbProvider {
{ timeoutMs: SETTINGS_TIMEOUT_MS, maxOutputBytes: 1024 },
signal,
);
// `test -e` exits nonzero with no adb diagnosis when the path is absent.
// Any other probe failure leaves existence unknown, so it is reported as
// is rather than read as an existing path or followed by a push.
if (
!existsProbe.ok &&
!(existsProbe.error instanceof AnalysisProtocolError)
)
let deviceExists: boolean;
if (existsProbe.ok) deviceExists = true;
else if (existsProbe.error instanceof AnalysisProtocolError)
deviceExists = false;
else if (existsProbe.error instanceof AnalysisTimeoutError)
return existsProbe;
const deviceExists = existsProbe.ok;
else if (existsProbe.error instanceof AnalysisCancelledError)
return existsProbe;
else if (existsProbe.error instanceof AnalysisResourceConstraintError)
return existsProbe;
else deviceExists = true; // authorization/refusal states surface on push instead
if (deviceExists && !input.overwrite)
return err(
new AnalysisOutputError(
-11
View File
@@ -50,13 +50,6 @@ export interface AnalysisExecution {
readonly limitations: readonly string[];
/** Provider-owned completeness assessment for caller/callee observations. */
readonly relationshipCoverage?: RelationshipCoverage;
/**
* Whether the provider capped an `xrefs` read: true when it reported a
* truncation, false when it explicitly reported none. Absent when it reports
* nothing, which is not a completeness claim. Never part of Evidence or tool
* output.
*/
readonly referencesTruncated?: boolean;
readonly locations: readonly EvidenceLocation[];
readonly subject: EvidenceSubjectTarget | null;
}
@@ -66,7 +59,6 @@ interface CreateAnalysisExecutionOptions {
readonly analysisProfile?: AnalysisProfileCommitment;
readonly limitations?: readonly string[];
readonly relationshipCoverage?: RelationshipCoverage;
readonly referencesTruncated?: boolean;
readonly locations?: readonly EvidenceLocation[];
readonly subject?: EvidenceSubjectTarget;
}
@@ -111,9 +103,6 @@ export function createAnalysisExecution(
options.relationshipCoverage,
),
}),
...(options.referencesTruncated === undefined
? {}
: { referencesTruncated: options.referencesTruncated }),
locations: [...(options.locations ?? [])],
subject: options.subject ?? null,
};
+5 -7
View File
@@ -76,8 +76,6 @@ export const parseBinaryTarget = async (
await access(candidate, constants.R_OK);
throwIfTargetResolutionCancelled(signal);
const canonical = await realpath(candidate);
// Windows native admission must inspect the caller-selected reparse components.
const sourcePath = process.platform === "win32" ? candidate : canonical;
throwIfTargetResolutionCancelled(signal);
const resolved = await resolveAppBundle(canonical);
throwIfTargetResolutionCancelled(signal);
@@ -115,7 +113,7 @@ export const parseBinaryTarget = async (
);
return ok({
path,
sourcePath,
sourcePath: canonical,
sha256: await sha256Handle(handle, path, metadata, signal),
kind: "executable",
format: "raw-binary",
@@ -154,7 +152,7 @@ export const parseBinaryTarget = async (
);
return ok({
path,
sourcePath,
sourcePath: canonical,
sha256: await sha256Handle(handle, path, metadata, signal),
kind: "executable",
format: "dos-com",
@@ -168,7 +166,7 @@ export const parseBinaryTarget = async (
)
return ok({
path,
sourcePath,
sourcePath: process.platform === "win32" ? candidate : canonical,
sha256: await sha256Handle(handle, path, metadata, signal),
kind: "database",
format: "analysis-database",
@@ -177,7 +175,7 @@ export const parseBinaryTarget = async (
if (artifactFormat !== undefined) {
const identity = {
path,
sourcePath,
sourcePath: process.platform === "win32" ? candidate : canonical,
sha256: await sha256Handle(handle, path, metadata, signal),
};
return isArchiveFormat(artifactFormat)
@@ -197,7 +195,7 @@ export const parseBinaryTarget = async (
);
return ok({
path,
sourcePath,
sourcePath: process.platform === "win32" ? candidate : canonical,
...bundle,
sha256: await sha256Handle(handle, path, metadata, signal),
kind: "executable",
@@ -497,71 +497,3 @@ it("reports host denial before target mismatch but keeps composed workflow prere
).reason,
).toBe("target_required");
});
describe("trace_feature requirements", () => {
const capabilities = (operations: readonly string[]) =>
operations.map((operation) => ({
operation,
available: true,
reason: null,
}));
it("needs the search operations the workflow actually calls", () => {
expect(
entry(
"trace_feature",
status({
open: true,
kind: "executable",
capabilities: capabilities([
"search_strings",
"search_procedures",
"xrefs",
"resolve_containing_procedure",
]),
}),
),
).toMatchObject({ available: true, reason: "available" });
});
it.each(["search_strings", "search_procedures"])(
"is unavailable without %s",
(missing) => {
const operations = [
"search_strings",
"search_procedures",
"xrefs",
"resolve_containing_procedure",
].filter((operation) => operation !== missing);
expect(
entry(
"trace_feature",
status({
open: true,
kind: "executable",
capabilities: capabilities(operations),
}),
),
).toMatchObject({ available: false, reason: "provider_missing" });
},
);
it("is unavailable when only the list operations exist", () => {
expect(
entry(
"trace_feature",
status({
open: true,
kind: "executable",
capabilities: capabilities([
"list_strings",
"list_procedures",
"xrefs",
"resolve_containing_procedure",
]),
}),
),
).toMatchObject({ available: false, reason: "provider_missing" });
});
});
+2 -3
View File
@@ -121,8 +121,8 @@ const ENHANCED_REQUIREMENTS: Readonly<Record<string, readonly string[]>> = {
analyze_function: ["analyze_function"],
inspect_native_api: ["analyze_function"],
trace_feature: [
"search_strings",
"search_procedures",
"list_strings",
"list_procedures",
"xrefs",
"resolve_containing_procedure",
],
@@ -141,7 +141,6 @@ const FRIDA_OPERATION_NAMES: ReadonlySet<string> = new Set(
/** Native contracts defined only for an active Mach-O executable. */
const MACHO_TARGET_OPERATIONS: ReadonlySet<string> = new Set([
"inspect_signature",
"inspect_macho",
"list_architectures",
"observe_native_calls",
+12 -14
View File
@@ -1,5 +1,9 @@
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
import { stringify as stringifyYaml } from "yaml";
import {
parse as parseYaml,
parseDocument as parseYamlDocument,
stringify as stringifyYaml,
} from "yaml";
import { isDeepStrictEqual } from "node:util";
import {
@@ -8,10 +12,6 @@ import {
editDshDocument,
dshClientRegistrationEntry,
} from "./DshCordisPatch.js";
import {
editHermesConfigurationYaml,
parseHermesConfigurationYaml,
} from "./HermesConfigurationYaml.js";
import {
applyEdits,
findNodeAtLocation,
@@ -118,10 +118,7 @@ const parseDocument = (
if (isEmptyClientConfigurationText(text)) return {};
// Hermes stores MCP servers in `mcp_servers` inside a YAML document.
if (format === "hermes")
return requireObject(
parseHermesConfigurationYaml(text),
"The configuration root",
);
return requireObject(parseYaml(text), "The configuration root");
// ZCode loads nonempty native files with strict JSON.parse.
if (format === "zcode")
return requireObject(JSON.parse(text), "The configuration root");
@@ -1422,11 +1419,12 @@ const serializeHermesConfiguration = (
typeof servers === "object" && servers !== null
? (servers as Record<string, unknown>)[PRODUCT_IDENTITY.mcpServerKey]
: undefined;
return editHermesConfigurationYaml(
originalText,
[serversKey, PRODUCT_IDENTITY.mcpServerKey],
entry,
);
const yamlDocument = parseYamlDocument(originalText);
if (entry === undefined)
yamlDocument.deleteIn([serversKey, PRODUCT_IDENTITY.mcpServerKey]);
else yamlDocument.setIn([serversKey, PRODUCT_IDENTITY.mcpServerKey], entry);
// Editing REA must not reflow unrelated user prompts or other YAML scalars.
return yamlDocument.toString({ lineWidth: 0 });
};
/** Build the stdio entry shape expected by one client's configuration dialect. */
-6
View File
@@ -23,7 +23,6 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { access } from "node:fs/promises";
import { readAnalysisSnapshot } from "./binary/AnalysisSnapshotFiles.js";
import { requiresNativeSourceAdmissionBeforeReplay } from "./binary/AnalysisSnapshotCache.js";
import { parseBinaryTarget } from "./BinaryTargetResolver.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import {
@@ -255,14 +254,9 @@ const runAnalysis = async (
const route = preview.value.route;
const bindingProfile = route.profile ?? undefined;
const evidenceProfile = analysisProfileForRoute(route, tool);
// A parsed Windows path does not prove Ghidra's native snapshot owner
// admitted the selected source; CLI replay must wait for that client.
if (
bindingProfile !== undefined &&
evidenceProfile !== undefined &&
!requiresNativeSourceAdmissionBeforeReplay(
route.binding?.identity.id,
) &&
allowsSnapshotReplay(route, tool)
) {
const cached = snapshotEvidenceForQuery(snapshot, {
+17 -47
View File
@@ -1,8 +1,5 @@
import { z } from "zod";
import type {
AnalysisExecution,
AnalysisOperation,
} from "./AnalysisProvider.js";
import type { AnalysisOperation } from "./AnalysisProvider.js";
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { JsonValue } from "../domain/jsonValue.js";
@@ -19,12 +16,6 @@ type AnalysisCall = (
signal?: AbortSignal,
) => Promise<Result<JsonValue, AnalysisError>>;
type ObserveCall = (
name: AnalysisOperation,
arguments_: Readonly<Record<string, JsonValue>>,
signal?: AbortSignal,
) => Promise<Result<AnalysisExecution, AnalysisError>>;
type LiteralSearchSource = readonly [
"search_strings" | "search_procedures",
"string" | "procedure",
@@ -48,7 +39,6 @@ export interface LiteralTraceInput {
/** Trace matching strings and procedures through provider xrefs. */
export const traceLiteralFeature = async (
call: AnalysisCall,
observe: ObserveCall,
input: LiteralTraceInput,
signal?: AbortSignal,
): EnhancedResult => {
@@ -59,7 +49,7 @@ export const traceLiteralFeature = async (
signal,
);
if (!searched.ok) return searched;
const traced = await traceReferences(call, observe, {
const traced = await traceReferences(call, {
matches: searched.value.matches,
...(signal === undefined ? {} : { signal }),
});
@@ -69,8 +59,8 @@ export const traceLiteralFeature = async (
search_mode: "literal",
matches: searched.value.matches,
references: traced.value.references,
truncated: traced.value.residual.length > 0,
residual_unknowns: traced.value.residual,
truncated: false,
residual_unknowns: [],
});
};
@@ -136,39 +126,24 @@ const addPageMatches = (input: {
const traceReferences = async (
call: AnalysisCall,
observe: ObserveCall,
request: ReferenceTraceRequest,
): Promise<
Result<{ references: TraceReference[]; residual: string[] }, AnalysisError>
> => {
): Promise<Result<{ references: TraceReference[] }, AnalysisError>> => {
const references: TraceReference[] = [];
const residual: string[] = [];
// A source address is resolved once however many matches reference it.
const containing = new Map<string, JsonValue>();
for (const match of request.matches) {
const observed = await observe(
const xrefs = await call(
"xrefs",
{ address: match.address },
request.signal,
);
if (!observed.ok) return observed;
// Only an explicit producer report of truncation is a coverage gap; a
// provider that reports nothing keeps its previous, unqualified result.
const unknown = `References to ${match.address} were truncated by the provider; additional references may exist.`;
if (
observed.value.referencesTruncated === true &&
!residual.includes(unknown)
)
residual.push(unknown);
const sources = observed.value.result;
if (!Array.isArray(sources))
if (!xrefs.ok) return xrefs;
if (!Array.isArray(xrefs.value))
return err(
new AnalysisOutputError(
"xrefs",
"provider returned a non-array result",
),
);
for (const source of sources) {
for (const source of xrefs.value) {
if (typeof source !== "string")
return err(
new AnalysisOutputError(
@@ -176,23 +151,18 @@ const traceReferences = async (
"provider returned a non-address value",
),
);
let containingProcedure = containing.get(source);
if (containingProcedure === undefined) {
const resolved = await call(
"resolve_containing_procedure",
{ address: source },
request.signal,
);
if (!resolved.ok) return resolved;
containingProcedure = resolved.value;
containing.set(source, containingProcedure);
}
const resolved = await call(
"resolve_containing_procedure",
{ address: source },
request.signal,
);
if (!resolved.ok) return resolved;
references.push({
target_address: match.address,
source_address: source,
containing_procedure: containingProcedure,
containing_procedure: resolved.value,
});
}
}
return ok({ references, residual });
return ok({ references });
};
-2
View File
@@ -163,8 +163,6 @@ export class EnhancedTools {
return traceLiteralFeature(
(name, arguments_, operationSignal) =>
this.#call(name, arguments_, operationSignal),
(name, arguments_, operationSignal) =>
this.#observe(name, arguments_, operationSignal),
call.input,
signal,
);
@@ -1,21 +0,0 @@
import { describe, expect, it } from "vitest";
import {
editHermesConfigurationYaml,
parseHermesConfigurationYaml,
} from "./HermesConfigurationYaml.js";
describe("Hermes configuration YAML", () => {
it("writes an edited number from its new value", () => {
expect(
editHermesConfigurationYaml("mode: 0600\nmask: 0x1F\n", ["mode"], 384),
).toBe("mode: 384\nmask: 0x1F\n");
});
it("accepts keys Hermes reads as distinct and rejects a repeated key", () => {
expect(() =>
parseHermesConfigurationYaml("0600: octal\n600: decimal\n"),
).not.toThrow();
expect(() => parseHermesConfigurationYaml('a: 1\n"a": 2\n')).toThrow();
});
});
@@ -1,74 +0,0 @@
import {
isScalar,
parse,
parseDocument,
type ParsedNode,
type ScalarTag,
type Tags,
} from "yaml";
const NUMBER_TAGS = new Set([
"tag:yaml.org,2002:int",
"tag:yaml.org,2002:float",
]);
/**
* Write a parsed number with its source spelling while that spelling still
* reads as the node's value. Hermes reads config.yaml as YAML 1.1 with exact
* integers, so re-printing a JavaScript number changes what it reads: a
* Discord channel ID loses its low digits, `0600` (octal) becomes `600`, and
* `0.0000001` becomes `1e-7`, which YAML 1.1 does not define as a float.
*/
const keepNumberSource = (tag: Tags[number]): Tags[number] => {
if (typeof tag === "string" || tag.collection !== undefined) return tag;
const { stringify } = tag;
if (!NUMBER_TAGS.has(tag.tag) || stringify === undefined) return tag;
return {
...tag,
stringify: (item, context, ...rest) => {
const { source } = item;
if (source === undefined) return stringify(item, context, ...rest);
const reader = context.doc.schema.tags.find(
(candidate): candidate is ScalarTag =>
candidate.default === true && candidate.test?.test(source) === true,
);
const read = reader?.resolve(
source,
() => undefined,
context.doc.options,
);
return Object.is(isScalar(read) ? read.value : read, item.value)
? source
: stringify(item, context, ...rest);
},
};
};
const HERMES_YAML_OPTIONS = {
customTags: (tags: Tags) => tags.map(keepNumberSource),
// Hermes keeps integers exact, so IDs that round to one double, or `0600`
// and `600`, are distinct keys.
uniqueKeys: (a: ParsedNode, b: ParsedNode) =>
a === b ||
(isScalar(a) &&
isScalar(b) &&
a.value === b.value &&
a.source === b.source),
};
/** Parse a Hermes `config.yaml`. */
export const parseHermesConfigurationYaml = (text: string): unknown =>
parse(text, HERMES_YAML_OPTIONS);
/** Set or delete one value in a Hermes `config.yaml`, keeping the rest as written. */
export const editHermesConfigurationYaml = (
text: string,
path: readonly string[],
value: unknown,
): string => {
const document = parseDocument(text, HERMES_YAML_OPTIONS);
if (value === undefined) document.deleteIn(path);
else document.setIn(path, value);
// Editing REA must not reflow unrelated user prompts or other YAML scalars.
return document.toString({ lineWidth: 0 });
};
+1 -3
View File
@@ -20,9 +20,7 @@ export const workflowAnalysisProfile = (
upstream_analysis_profile: upstream,
...(operation === "binary_overview"
? { workflow_revision: 3 }
: operation === "batch_decompile" ||
operation === "get_call_graph" ||
operation === "trace_feature"
: operation === "batch_decompile" || operation === "get_call_graph"
? { workflow_revision: 2 }
: {}),
});
@@ -97,22 +97,18 @@ const resolveInternalSpecifier = (
);
if (logicalPathEscapesRoot(normalized))
return { to: normalized, resolution: "unresolved" };
// Preserve directory intent before normalization removes terminal dot segments.
const directoryReference =
specifier.endsWith("/") || [".", ".."].includes(posix.basename(specifier));
const suffixes = ["", ".ts", ".js", ".mjs", ".cjs", ".tsx", ".jsx"];
const candidates = suffixes.flatMap((suffix) => [
...(directoryReference ? [] : [`${normalized}${suffix}`]),
posix.join(normalized, `index${suffix}`),
`${normalized}${suffix}`,
`${normalized}/index${suffix}`,
]);
// Retain present runtime files first; substitute only a missing counterpart.
// Counterparts cover explicit filenames, not directory/index resolution.
if (!directoryReference)
candidates.push(
...typescriptEmittedSourceCandidates(fromPath, normalized).map(
({ path }) => path,
),
);
candidates.push(
...typescriptEmittedSourceCandidates(fromPath, normalized).map(
({ path }) => path,
),
);
const match = lookupPath.resolveCandidates(candidates);
return match === undefined
? { to: normalized, resolution: "unresolved" }
+2 -34
View File
@@ -135,9 +135,6 @@ const codexDirectory = ({ home, env }: ClientPathContext): string =>
const qoderDirectory = ({ home, env }: ClientPathContext): string =>
env.QODER_CONFIG_DIR || join(home, ".qoder");
const factoryDirectory = ({ home }: ClientPathContext): string =>
join(home, ".factory");
/** Match Qwen Code's home override, including tilde and cwd-relative paths. */
const qwenCodeDirectory = ({ home, env }: ClientPathContext): string => {
const override = env.QWEN_HOME;
@@ -160,27 +157,8 @@ const qwenCodeDirectory = ({ home, env }: ClientPathContext): string => {
const grokDirectory = ({ home, env }: ClientPathContext): string =>
env.GROK_HOME || join(home, ".grok");
/** Match Harness home-paths for both independent roots; trim only to detect blanks. */
const dshDirectory = (
home: string,
override: string | undefined,
fallback: string,
): string => {
const selected =
override !== undefined && override.trim().length > 0
? override
: join(home, fallback);
const expanded =
selected === "~"
? home
: selected.startsWith("~/") || selected.startsWith("~\\")
? join(home, selected.slice(2))
: selected;
return resolve(expanded);
};
const dshHomeDirectory = ({ home, env }: ClientPathContext): string =>
dshDirectory(home, env.DSH_HOME, ".dsh");
env.DSH_HOME ?? join(home, ".dsh");
const dshPatchDirectory = (context: ClientPathContext): string =>
join(dshHomeDirectory(context), "cordis.patch.yml");
@@ -561,16 +539,6 @@ export const SUPPORTED_CLIENT_DEFINITIONS = [
markerPath: qoderDirectory,
format: "json",
},
{
name: "droid",
displayName: "Factory Droid",
skillPath: (context: ClientPathContext) =>
join(factoryDirectory(context), "skills"),
configPath: (context: ClientPathContext) =>
join(factoryDirectory(context), "mcp.json"),
markerPath: factoryDirectory,
format: "json",
},
{
name: "zcode",
zcodeFallbackPath: ({ home }: ClientPathContext) =>
@@ -587,7 +555,7 @@ export const SUPPORTED_CLIENT_DEFINITIONS = [
// The home Cordis patch applies to every profile; the skill directory is
// shared agent storage Harness discovers.
skillPath: ({ home, env }: ClientPathContext) =>
join(dshDirectory(home, env.DSH_AGENTS_HOME, ".agents"), "skills"),
join(env.DSH_AGENTS_HOME ?? join(home, ".agents"), "skills"),
configPath: dshPatchDirectory,
markerPath: dshHomeDirectory,
format: "dsh",
@@ -661,52 +661,3 @@ it("does not replay pre-envelope call graphs under the revised workflow profile"
}).success,
).toBe(false);
});
describe("executions that report truncated references", () => {
const record = (referencesTruncated: boolean | undefined) => {
const cache = new AnalysisSnapshotCache();
const parameters = { address: "0x1000", document: "fixture" };
cache.record({
target: ANALYSIS_SNAPSHOT_TARGET,
profile: ANALYSIS_SNAPSHOT_PROFILE,
operation: "xrefs",
parameters,
execution: createAnalysisExecution(
["0x2000"],
ANALYSIS_SNAPSHOT_PROVIDER,
{
subject: ANALYSIS_SNAPSHOT_TARGET,
analysisProfile: ANALYSIS_SNAPSHOT_PROFILE,
...(referencesTruncated === undefined ? {} : { referencesTruncated }),
},
),
});
return cache.lookup(
ANALYSIS_SNAPSHOT_TARGET,
ANALYSIS_SNAPSHOT_PROFILE,
"xrefs",
parameters,
);
};
it("are not replayed, because a replay would lose the truncation", () => {
expect(record(true)).toBeUndefined();
});
it("do not stop an execution that is complete or silent from being replayed", () => {
expect(record(false)?.result).toEqual(["0x2000"]);
expect(record(undefined)?.result).toEqual(["0x2000"]);
});
});
it("revises the trace_feature workflow profile so earlier results are not replayed", () => {
const profile = ANALYSIS_SNAPSHOT_PROFILE;
expect(workflowAnalysisProfile(profile, "trace_feature")).not.toEqual(
workflowAnalysisProfile(profile),
);
expect(
workflowAnalysisProfile(profile, "trace_call_path"),
"other workflows keep their profile",
).toEqual(workflowAnalysisProfile(profile));
});
@@ -84,11 +84,6 @@ export const isSnapshotCacheable = (
descriptor.effects.mayWriteFilesystem === false) &&
descriptor.effects.changesPermissions === false;
/** Windows Ghidra must admit the selected source through its native snapshot owner. */
export const requiresNativeSourceAdmissionBeforeReplay = (
providerId: string | undefined,
): boolean => process.platform === "win32" && providerId === "ghidra";
/** Bounded in-memory cache for one immutable binary identity. */
export class AnalysisSnapshotCache {
readonly #entries = new Map<string, AnalysisSnapshotEntry>();
@@ -342,9 +337,6 @@ export class AnalysisSnapshotCache {
}): void {
const { target, profile, operation, parameters, execution } = input;
this.select(target, profile);
// The snapshot format does not carry reference truncation; a replay would
// present a capped read as complete, so it is not recorded.
if (execution.referencesTruncated === true) return;
const queryId = analysisQueryId(
snapshotTarget(target),
snapshotBinding(profile),
-12
View File
@@ -58,7 +58,6 @@ interface SessionBinding {
readonly profile: AnalysisProfileCommitment | null;
readonly route: SessionProviderRoute;
readonly runId: string;
nativeSourceAdmitted: boolean;
}
/**
@@ -324,7 +323,6 @@ export class BinarySession
profile,
route,
runId,
nativeSourceAdmitted: false,
};
this.#clearRuntimeAvailability();
this.resetSnapshotInvalidation();
@@ -611,7 +609,6 @@ export class BinarySession
active: this.#active,
operation: name,
parameters: arguments_,
nativeSourceAdmitted: this.#active?.nativeSourceAdmitted === true,
unboundOperationError: (operation, route) =>
this.#providerRouter.unboundOperationError(operation, route),
lookupSnapshot: (target, profile, operation, parameters) =>
@@ -648,14 +645,6 @@ export class BinarySession
},
})
: profiled;
// A successful Ghidra operation proves this client's native source snapshot
// was admitted; a composite health response and parsed path do not.
if (
execution.ok &&
capability.provider.id === "ghidra" &&
this.#active === active
)
this.#active.nativeSourceAdmitted = true;
this.#observeRuntimeAvailability(name, execution);
if (
execution.ok &&
@@ -849,7 +838,6 @@ export class BinarySession
profile: previous.profile,
route: previous.route,
runId,
nativeSourceAdmitted: false,
};
return ok(null);
}
@@ -13,10 +13,7 @@ import type {
AnalysisOperation,
CapabilityDescriptor,
} from "../AnalysisProvider.js";
import {
isSnapshotCacheable,
requiresNativeSourceAdmissionBeforeReplay,
} from "./AnalysisSnapshotCache.js";
import { isSnapshotCacheable } from "./AnalysisSnapshotCache.js";
import type { SessionProviderRoute } from "./SessionProviderRouter.js";
interface ActiveExecutionBinding {
@@ -38,7 +35,6 @@ interface PrepareSessionExecutionInput {
readonly active: ActiveExecutionBinding | undefined;
readonly operation: AnalysisOperation;
readonly parameters: Readonly<Record<string, JsonValue>>;
readonly nativeSourceAdmitted: boolean;
readonly unboundOperationError: (
operation: AnalysisOperation,
route: SessionProviderRoute,
@@ -59,7 +55,6 @@ export const prepareSessionExecution = (
active,
operation,
parameters,
nativeSourceAdmitted,
unboundOperationError,
lookupSnapshot,
} = input;
@@ -84,12 +79,9 @@ export const prepareSessionExecution = (
const cacheable =
profile !== undefined &&
isSnapshotCacheable(operation, capability, parameters);
const cached =
cacheable &&
(!requiresNativeSourceAdmissionBeforeReplay(capability.provider.id) ||
nativeSourceAdmitted)
? lookupSnapshot(active.target, profile, operation, parameters)
: undefined;
const cached = cacheable
? lookupSnapshot(active.target, profile, operation, parameters)
: undefined;
return ok({ active, capability, profile, cacheable, cached });
};
+6 -12
View File
@@ -92,7 +92,9 @@ export const resolveSessionTarget = async (
input: ResolveSessionTargetInput,
): Promise<Result<ResolvedSessionOpen, AnalysisError>> => {
const { router, current, target, options } = input;
const sameTarget = sameSessionTarget(current?.target, target);
const sameTarget =
current?.target.path === target.path &&
snapshotMatchesTarget(snapshotTarget(current.target), target);
const resolvedRoute =
sameTarget && options.providerId === undefined && current !== undefined
? ({ ok: true, value: current.route } as const)
@@ -111,7 +113,9 @@ export const validateResolvedSessionTarget = (
},
): Result<ResolvedSessionOpen, AnalysisError> => {
const { current, target, route, options, stagedSnapshotMatches } = input;
const sameTarget = sameSessionTarget(current?.target, target);
const sameTarget =
current?.target.path === target.path &&
snapshotMatchesTarget(snapshotTarget(current.target), target);
const snapshotError = validateSnapshot({
snapshot: options.snapshot,
target,
@@ -123,16 +127,6 @@ export const validateResolvedSessionTarget = (
return { ok: true, value: { target, route, sameTarget } };
};
const sameSessionTarget = (
current: BinaryTarget | undefined,
target: BinaryTarget,
): boolean =>
current !== undefined &&
current.path === target.path &&
// Native admission is bound to the requested path, not just its realpath.
(current.sourcePath ?? current.path) === (target.sourcePath ?? target.path) &&
snapshotMatchesTarget(snapshotTarget(current), target);
interface ValidateSnapshotInput {
readonly snapshot: AnalysisSnapshot | undefined;
readonly target: BinaryTarget;
+2 -10
View File
@@ -40,7 +40,6 @@ import { ArtifactResourceScope } from "./ArtifactResourceScope.js";
import { artifactCapabilities } from "./ArtifactProviderMetadata.js";
import { ARTIFACT_GRAPH_PROVIDER } from "../application/InvestigationProviders.js";
import { createEvidence } from "../domain/evidence.js";
import { freezeJsonSnapshot } from "../domain/immutableJson.js";
import { createArtifactInspection } from "../domain/artifactInspection.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
@@ -342,11 +341,7 @@ class ArtifactClient implements AnalysisClient {
total: 1,
message: "inventory substep started",
});
// The freshly parsed inventory is exclusively owned here. Sealing it lets
// the nested Evidence share it instead of cloning and re-digesting it.
const inventory = freezeJsonSnapshot(
await this.inventory(inventoryParameters, options),
);
const inventory = await this.inventory(inventoryParameters, options);
const subject = subjectFor(
this.target.sourcePath ?? this.target.path,
inventory.manifest,
@@ -360,10 +355,7 @@ class ArtifactClient implements AnalysisClient {
// The nested inventory already names every occurrence's logical path.
locations: [],
});
// The returned execution and its Evidence then reuse this owned report.
const result = freezeJsonSnapshot(
createArtifactInspection(inventoryEvidence),
);
const result = createArtifactInspection(inventoryEvidence);
await options?.progress?.report({
phase: "inspect_artifact.inventory",
completed: 1,
+2 -2
View File
@@ -8,7 +8,7 @@ import {
type NativeCommandRunner,
} from "../native/CommandRunner.js";
import {
parseLipoOutput,
parseLipoArchitectures,
type LipoArchitecture,
} from "../native/parsers/lipo.js";
import type { MachoSlice } from "../domain/apple/dylibResolution.js";
@@ -114,7 +114,7 @@ export class MachOSliceArtifactReader implements ArtifactReader {
};
await this.#verifySource(source);
const fileSize = source.initial.size;
const { architectures } = parseLipoOutput(captured.value.stdout, this.path);
const architectures = parseLipoArchitectures(captured.value.stdout);
const structural = await readStructuralSlices(source.handle, fileSize);
await this.#verifySource(source);
if (structural.status === "malformed")
+6 -79
View File
@@ -2,29 +2,7 @@ import type { FileHandle } from "node:fs/promises";
import { PassThrough, type Readable } from "node:stream";
import { once } from "node:events";
import {
ERR_AMBIGUOUS_ARCHIVE,
ERR_BAD_FORMAT,
ERR_CENTRAL_DIRECTORY_NOT_FOUND,
ERR_ENCRYPTED_CENTRAL_DIRECTORY,
ERR_ENTRY_DATA_OUT_OF_BOUNDS,
ERR_EOCDR_LOCATOR_ZIP64_NOT_FOUND,
ERR_EOCDR_NOT_FOUND,
ERR_EXTRAFIELD_ZIP64_NOT_FOUND,
ERR_INVALID_COMPRESSED_DATA,
ERR_INVALID_CRC32,
ERR_INVALID_UNCOMPRESSED_SIZE,
ERR_LOCAL_FILE_HEADER_NOT_FOUND,
ERR_OVERLAPPING_ENTRY,
ERR_SPLIT_ZIP_FILE,
ERR_UNSUPPORTED_COMPRESSION,
ERR_UNSUPPORTED_ENCRYPTION,
ERR_UNSUPPORTED_UINT64,
Reader,
ZipReader,
type Entry,
type FileEntry,
} from "@zip.js/zip.js";
import { Reader, ZipReader, type Entry, type FileEntry } from "@zip.js/zip.js";
import {
ArtifactReaderFailure,
@@ -181,9 +159,9 @@ export class ZipArtifactReader implements ArtifactReader {
let index = 0;
// A traversal owns its accounting, including overlapping or repeated passes.
let declaredFileBytes = 0;
for await (const entry of typedZipFailures(
this.#reader.getEntriesGenerator({ decodeText: decodeZipEntryText }),
)) {
for await (const entry of this.#reader.getEntriesGenerator({
decodeText: decodeZipEntryText,
})) {
abortIfNeeded(signal);
// ZIP member identity is its directory occurrence, not its filename.
// Logical path uniqueness belongs to the artifact inventory boundary.
@@ -349,7 +327,7 @@ const extractStream = (entry: FileEntry, signal?: AbortSignal): Readable => {
output.end();
},
abort: (reason) => {
output.destroy(toError(typedZipFailure(reason)));
output.destroy(toError(reason));
},
});
void entry
@@ -359,61 +337,10 @@ const extractStream = (entry: FileEntry, signal?: AbortSignal): Readable => {
checkOverlappingEntry: true,
onprogress: () => abortIfNeeded(signal),
})
.catch((cause: unknown) => output.destroy(toError(typedZipFailure(cause))));
.catch((cause: unknown) => output.destroy(toError(cause)));
return output;
};
/** zip.js reports archive faults as plain errors identified by message. */
const ZIP_FAILURE_REASONS: ReadonlyMap<string, "format" | "unavailable"> =
new Map([
...[
ERR_AMBIGUOUS_ARCHIVE,
ERR_BAD_FORMAT,
ERR_CENTRAL_DIRECTORY_NOT_FOUND,
ERR_ENTRY_DATA_OUT_OF_BOUNDS,
ERR_EOCDR_LOCATOR_ZIP64_NOT_FOUND,
ERR_EOCDR_NOT_FOUND,
ERR_EXTRAFIELD_ZIP64_NOT_FOUND,
ERR_INVALID_COMPRESSED_DATA,
ERR_INVALID_CRC32,
ERR_INVALID_UNCOMPRESSED_SIZE,
ERR_LOCAL_FILE_HEADER_NOT_FOUND,
ERR_OVERLAPPING_ENTRY,
ERR_UNSUPPORTED_UINT64,
].map((message) => [message, "format"] as const),
...[
ERR_ENCRYPTED_CENTRAL_DIRECTORY,
ERR_SPLIT_ZIP_FILE,
ERR_UNSUPPORTED_COMPRESSION,
ERR_UNSUPPORTED_ENCRYPTION,
].map((message) => [message, "unavailable"] as const),
]);
/** Type a zip.js archive fault so consumers do not report it as file I/O. */
const typedZipFailure = (cause: unknown): unknown => {
if (!(cause instanceof Error) || cause instanceof ArtifactReaderFailure)
return cause;
const reason = ZIP_FAILURE_REASONS.get(cause.message);
if (reason === undefined) return cause;
return new ArtifactReaderFailure(
reason,
reason === "format"
? `ZIP archive is malformed: ${cause.message}`
: `ZIP archive uses an unsupported feature: ${cause.message}`,
{ cause },
);
};
async function* typedZipFailures<T>(
source: AsyncIterable<T>,
): AsyncIterable<T> {
try {
yield* source;
} catch (cause: unknown) {
throw typedZipFailure(cause);
}
}
const isSymlink = (entry: Entry): boolean =>
entry.unixMode !== undefined && (entry.unixMode & 0o170000) === 0o120000;
@@ -56,7 +56,6 @@ export const analyzeInterfaceBuilderBundle = async (input: {
const omittedArchives: Array<{ path: string; reason: string }> = [];
let unreportedOmittedArchives = 0;
const incompleteHierarchies = new Map<string, number>();
const unresolvedConnections = new Map<string, number>();
const prototypeKeyOmissions = new Map<string, number | null>();
let omitted = 0;
let attempted = 0;
@@ -137,8 +136,6 @@ export const analyzeInterfaceBuilderBundle = async (input: {
: { value: nib.raw, omittedPrototypeKeys: 0 };
if (nib !== null && nib.omitted > 0)
incompleteHierarchies.set(entry.path, nib.omitted);
if (nib !== null && nib.unresolvedConnections > 0)
unresolvedConnections.set(entry.path, nib.unresolvedConnections);
if (omittedPrototypeKeys !== 0)
prototypeKeyOmissions.set(entry.path, omittedPrototypeKeys);
const documentHash = createHash("sha256").update(bytes).digest("hex");
@@ -173,7 +170,6 @@ export const analyzeInterfaceBuilderBundle = async (input: {
});
return finalizeHierarchyCoverage(result, {
incompleteHierarchies,
unresolvedConnections,
prototypeKeyOmissions,
invalid,
omittedArchives,
@@ -241,7 +237,6 @@ const finalizeHierarchyCoverage = (
result: ReturnType<typeof buildInterfaceBuilderAnalysis>,
counts: {
incompleteHierarchies: ReadonlyMap<string, number>;
unresolvedConnections: ReadonlyMap<string, number>;
prototypeKeyOmissions: ReadonlyMap<string, number | null>;
invalid: readonly string[];
omittedArchives: readonly { path: string; reason: string }[];
@@ -252,7 +247,6 @@ const finalizeHierarchyCoverage = (
) => {
const {
incompleteHierarchies,
unresolvedConnections,
prototypeKeyOmissions,
invalid,
omittedArchives,
@@ -266,13 +260,6 @@ const finalizeHierarchyCoverage = (
...result,
documents: result.documents.map((document) => ({
...document,
// The dictionary parser only sees connectors that were projected, so the
// omission recorded where an ownerless connector was dropped has to be
// added here for this producer to report the same document count as the
// keyed-archive producer for the same recognized connector.
connection_count:
document.connection_count +
(unresolvedConnections.get(document.relative_path) ?? 0),
hierarchy_complete:
document.hierarchy_complete &&
!incompleteHierarchies.has(document.relative_path),
@@ -281,32 +268,15 @@ const finalizeHierarchyCoverage = (
...result.graph,
coverage: [
...result.graph.coverage.map((facet) => {
const omittedHierarchy = incompleteHierarchies.get(
const omitted = incompleteHierarchies.get(
facet.facet.replace(/^hierarchy:/u, ""),
);
if (
facet.facet.startsWith("hierarchy:") &&
omittedHierarchy !== undefined
)
return {
...facet,
status: "partial" as const,
reason: "serialized_view_hierarchy_incomplete",
omitted: facet.omitted + omittedHierarchy,
};
const omittedConnection = unresolvedConnections.get(
facet.facet.replace(/^connections:/u, ""),
);
return facet.facet.startsWith("connections:") &&
omittedConnection !== undefined
return facet.facet.startsWith("hierarchy:") && omitted !== undefined
? {
...facet,
status: "partial" as const,
// A connection dropped by the limit is recoverable by raising
// it, so that cause keeps the reason the graph already named.
// Only a facet with no other cause reports the omission.
reason: facet.reason ?? "connection_unresolved",
omitted: facet.omitted + omittedConnection,
reason: "serialized_view_hierarchy_incomplete",
omitted: facet.omitted + omitted,
}
: facet;
}),
@@ -336,8 +306,7 @@ const finalizeHierarchyCoverage = (
result.graph.truncated ||
counts.omitted > 0 ||
archiveDecodePartial ||
incompleteHierarchies.size > 0 ||
unresolvedConnections.size > 0,
incompleteHierarchies.size > 0,
},
limitations: [
...result.limitations,
@@ -347,11 +316,6 @@ const finalizeHierarchyCoverage = (
: [
"Some serialized hierarchy links could not be projected within the bounded hierarchy; their parentage remains unknown.",
]),
...(unresolvedConnections.size === 0
? []
: [
"Some recognized NIBArchive connectors declare no resolvable owner; they carry no connection evidence and are reported as omissions.",
]),
...(invalid.length === 0
? []
: [
@@ -532,10 +496,6 @@ const projectNibArchive = (
);
const connections: Record<string, JsonValue[]> = {};
const hierarchy: NibHierarchyNode[] = [];
// A recognized connector whose owner reference cannot be resolved produces no
// connection entry, so the omission has to be counted here: the projected
// dictionary carries no trace of it for a downstream consumer to find.
let unresolvedConnections = 0;
const reference = (value: JsonValue | undefined): number | null => {
if (typeof value !== "object" || value === null || Array.isArray(value))
return null;
@@ -552,10 +512,7 @@ const projectNibArchive = (
const values = object.values;
const connectionFields = interfaceBuilderConnectionFieldNames(className);
const source = reference(values[connectionFields.source]);
if (source === null) {
unresolvedConnections += 1;
continue;
}
if (source === null) continue;
const destination = reference(values[connectionFields.destination]);
const label = referencedString(values[connectionFields.label]);
const type = className.includes("Outlet")
@@ -651,7 +608,6 @@ const projectNibArchive = (
);
return {
omitted: viewHierarchy.omitted + hierarchyOmitted + merged.omitted,
unresolvedConnections,
raw: jsonValueSchema.parse({
"com.apple.ibtool.document.objects": objects,
"com.apple.ibtool.document.connections": connections,
@@ -1,103 +0,0 @@
import { describe, expect, it } from "vitest";
import { CPU, fatImage, machoImage } from "../apple/MachoImage.fixture.js";
import { classifyArtifactContent } from "./ArtifactGraphConstruction.js";
const fat = (wide = false): Buffer =>
Buffer.from(fatImage([{ cpu: CPU.arm64, bytes: machoImage({}) }], wide));
describe("bounded FAT inventory classification", () => {
it("requires the complete table but not slice bytes in the retained prefix", () => {
const bytes = fat();
for (const length of [4, 7, 8, 27]) {
expect(
classifyArtifactContent(
"native",
bytes.subarray(0, length),
bytes.length,
),
).toEqual({ kind: "unknown", format: "unknown" });
}
expect(
classifyArtifactContent(
"native.class",
bytes.subarray(0, 28),
bytes.length,
),
).toEqual({ kind: "executable", format: "mach-o-universal" });
});
it.each([
["empty table", (bytes: Buffer) => bytes.writeUInt32BE(0, 4)],
["slice overlapping table", (bytes: Buffer) => bytes.writeUInt32BE(0, 16)],
["empty slice", (bytes: Buffer) => bytes.writeUInt32BE(0, 20)],
[
"slice beyond file",
(bytes: Buffer) => bytes.writeUInt32BE(bytes.length, 20),
],
["unaligned slice", (bytes: Buffer) => bytes.writeUInt32BE(4097, 16)],
["unsupported alignment", (bytes: Buffer) => bytes.writeUInt32BE(53, 24)],
] as const)("keeps %s unknown", (_label, mutate) => {
const bytes = fat();
mutate(bytes);
expect(classifyArtifactContent("native", bytes)).toEqual({
kind: "unknown",
format: "unknown",
});
});
it("does not infer Mach-O from an out-of-bounds architecture count", () => {
const bytes = fat();
bytes.writeUInt32BE(0xffffffff, 4);
expect(classifyArtifactContent("native", bytes)).toEqual({
kind: "resource",
format: "file",
});
});
it("does not lose unsafe FAT64 range values to number conversion", () => {
const bytes = fat(true);
bytes.writeBigUInt64BE(BigInt(Number.MAX_SAFE_INTEGER) + 1n, 16);
expect(classifyArtifactContent("native", bytes)).toEqual({
kind: "unknown",
format: "unknown",
});
});
it("requires valid ranges for every declared slice", () => {
const bytes = Buffer.from(
fatImage([
{ cpu: CPU.arm64, bytes: machoImage({}) },
{ cpu: CPU.x86_64, bytes: machoImage({ cpu: CPU.x86_64 }) },
]),
);
bytes.writeUInt32BE(bytes.length, 40);
expect(classifyArtifactContent("native", bytes)).toEqual({
kind: "unknown",
format: "unknown",
});
});
it("requires observed slice magic for an unknown CPU type", () => {
const cpu = { type: 0x12345678, subtype: 0 };
const bytes = Buffer.from(fatImage([{ cpu, bytes: machoImage({ cpu }) }]));
expect(
classifyArtifactContent("native", bytes.subarray(0, 28), bytes.length),
).toEqual({ kind: "unknown", format: "unknown" });
expect(classifyArtifactContent("native", bytes)).toEqual({
kind: "executable",
format: "mach-o-universal",
});
});
it.each([NaN, Infinity, -1, 8.5, Number.MAX_SAFE_INTEGER + 1, 27])(
"keeps invalid or contradictory file size %s unknown",
(size) => {
const bytes = fat();
expect(classifyArtifactContent("native", bytes, size)).toEqual({
kind: "unknown",
format: "unknown",
});
},
);
});
@@ -37,7 +37,6 @@ interface ExpectedFile {
interface ExpectedContainer {
readonly hash_status: ArtifactOccurrence["hash_status"];
readonly format: ArtifactOccurrence["artifact_format"];
readonly inventory: JavaScriptArtifactContainer | null;
}
@@ -220,9 +219,6 @@ const visitReader = async (
"integrity",
`Nested ASAR was not integrity-verified: ${path}`,
);
// Inventory expands an ASAR-named file only when its bytes are an
// ASAR. One it recorded as ordinary bytes has no members to read.
if (container.format !== "asar") continue;
const inventory = container.inventory;
if (inventory === null)
throw new ArtifactReaderFailure(
@@ -399,7 +395,6 @@ const expectedInventory = (
if (isNestedAsar)
containers.set(occurrence.logical_path, {
hash_status: occurrence.hash_status,
format: occurrence.artifact_format,
inventory: null,
});
continue;
@@ -417,7 +412,6 @@ const expectedInventory = (
if (isNestedAsar)
containers.set(occurrence.logical_path, {
hash_status: occurrence.hash_status,
format: occurrence.artifact_format,
inventory: {
path: occurrence.logical_path,
sha256: node.sha256,
+37 -247
View File
@@ -2,8 +2,6 @@ import { describe, expect, it } from "vitest";
import { inspectWebPageInputSchema } from "../domain/browserObservation.js";
import { captureDom } from "./CdpCaptureDocuments.js";
import { capturedSvgNodeKind } from "./CdpCaptureDomSvg.js";
import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
const origin = "https://app.example.test";
const documentUrl = `${origin}/screens/app`;
@@ -52,11 +50,8 @@ const snapshot = (
],
});
const capture = (value: ReturnType<typeof snapshot>) => {
const step = captureDom(value, new Set([origin]), input).next();
if (!step.done) throw new Error("Unexpected SVG identity query");
return step.value;
};
const capture = (value: ReturnType<typeof snapshot>) =>
captureDom(value, new Set([origin]), input);
describe("DOM metadata document base URLs", () => {
it("resolves links, actions and agent hints against the captured document base", () => {
@@ -287,7 +282,6 @@ interface SvgSnapshotNode {
readonly name: string;
readonly parent: number;
readonly attributes?: readonly (readonly [string, string])[];
readonly svg?: boolean;
}
const svgDocument = (
@@ -306,13 +300,7 @@ const svgDocument = (
const nodeValue = [intern("")];
const parentIndex = [-1];
const attributes: number[][] = [[]];
const backendNodeId = [1];
const svgNodeKinds = new Map<string, boolean>();
for (const element of elements) {
const id = backendNodeId.length + 1;
backendNodeId.push(id);
if (element.svg !== undefined)
svgNodeKinds.set(`${String(id)}:${element.name}`, element.svg);
nodeType.push(1);
nodeName.push(intern(element.name));
nodeValue.push(intern(""));
@@ -328,35 +316,14 @@ const svgDocument = (
{
documentURL: 0,
baseURL: 1,
nodes: {
nodeType,
nodeName,
nodeValue,
parentIndex,
attributes,
backendNodeId,
},
svgNodeKinds,
nodes: { nodeType, nodeName, nodeValue, parentIndex, attributes },
},
],
};
};
const captureSvg = (value: ReturnType<typeof svgDocument>) => {
const kinds = new Map(
value.documents.flatMap((document) => [...document.svgNodeKinds]),
);
const capture = captureDom(value, new Set([origin]), input);
let step = capture.next();
while (!step.done) {
step = capture.next(
step.value.map((query) =>
kinds.get(`${String(query.backendNodeId)}:${query.nodeName}`),
),
);
}
return step.value;
};
const captureSvg = (value: ReturnType<typeof svgDocument>) =>
captureDom(value, new Set([origin]), input);
const urlFor = (
result: ReturnType<typeof captureSvg>,
@@ -364,136 +331,6 @@ const urlFor = (
): (typeof result.urls)[number] | undefined =>
result.urls.find((url) => url.node_index === nodeIndex);
describe("browser SVG identity", () => {
const query = { backendNodeId: 42, nodeName: "a" };
const node = { ...query, nodeType: 1 };
it("uses only a matching described element's SVG flag", () => {
expect(capturedSvgNodeKind(query, { node: { ...node, isSVG: true } })).toBe(
true,
);
expect(
capturedSvgNodeKind(query, { node: { ...node, isSVG: false } }),
).toBe(false);
expect(capturedSvgNodeKind(query, { node })).toBe(false);
for (const invalid of [
{},
{ node: { ...node, backendNodeId: 43, isSVG: true } },
{ node: { ...node, nodeName: "A", isSVG: true } },
{ node: { ...node, nodeType: 3, isSVG: true } },
{ node: { ...node, isSVG: "true" } },
])
expect(capturedSvgNodeKind(query, invalid)).toBeUndefined();
});
it("distinguishes identically named SVG and MathML elements regardless of parent", () => {
const result = captureSvg(
svgDocument([
{ name: "svg", parent: 0 },
{ name: "desc", parent: 1 },
{
name: "a",
svg: true,
parent: 2,
attributes: [["xlink:href", "/svg"]],
},
{
name: "a",
svg: false,
parent: 2,
attributes: [["xlink:href", "/math"]],
},
{
name: "A",
svg: false,
parent: 1,
attributes: [["xlink:href", "/html"]],
},
{
name: "a",
svg: true,
parent: 0,
attributes: [["xlink:href", "/orphan"]],
},
]),
);
expect(result.urls.map(({ url }) => url)).toEqual([
`${origin}/svg`,
`${origin}/orphan`,
]);
});
it("records unresolved identity without inventing a destination or hiding attributes", () => {
const value = svgDocument([
{ name: "a", parent: 0, attributes: [["xlink:href", "/unknown"]] },
]);
const completeness = new CdpCaptureCompleteness();
const capture = captureDom(value, new Set([origin]), input, completeness);
expect(capture.next()).toEqual({
done: false,
value: [{ backendNodeId: 2, nodeName: "a" }],
});
const step = capture.next([undefined]);
expect(step.done).toBe(true);
if (!step.done) throw new Error("Unexpected second query");
expect(step.value.unresolvedSvgNodes).toBe(1);
expect(step.value.urls).toEqual([]);
expect(step.value.nodes[1]?.attribute_names).toEqual(["xlink:href"]);
expect(completeness.snapshot().excluded).toEqual(
expect.arrayContaining([
expect.objectContaining({
section: "metadata",
reason: "provider_unavailable",
count: 1,
}),
]),
);
});
});
it("keeps batched SVG identities aligned around missing IDs and ordinary links", () => {
const value = svgDocument([
{ name: "a", parent: 0, attributes: [["xlink:href", "/svg"]] },
{ name: "A", parent: 0, attributes: [["href", "/ordinary"]] },
{ name: "a", parent: 0, attributes: [["xlink:href", "/missing-id"]] },
{ name: "a", parent: 0, attributes: [["xlink:href", "/non-svg"]] },
]);
const completeness = new CdpCaptureCompleteness();
const capture = captureDom(
{
...value,
documents: value.documents.map((document) => ({
...document,
nodes: { ...document.nodes, backendNodeId: [1, 2, 3, null, 5] },
})),
},
new Set([origin]),
input,
completeness,
);
expect(capture.next()).toEqual({
done: false,
value: [
{ backendNodeId: 2, nodeName: "a" },
{ backendNodeId: 5, nodeName: "a" },
],
});
const step = capture.next([true, false]);
if (!step.done) throw new Error("Unexpected second batch");
expect(
step.value.urls.map(({ node_index, url }) => [node_index, url]),
).toEqual([
[1, `${origin}/svg`],
[2, `${origin}/ordinary`],
]);
expect(step.value.unresolvedSvgNodes).toBe(1);
expect(completeness.snapshot().excluded).toContainEqual({
section: "metadata",
reason: "provider_unavailable",
count: 1,
});
});
describe("SVG xlink:href destinations", () => {
it("reports an SVG anchor that only has xlink:href, keeping the first value", () => {
const result = captureSvg(
@@ -502,7 +339,6 @@ describe("SVG xlink:href destinations", () => {
{ name: "g", parent: 1 },
{
name: "a",
svg: true,
parent: 2,
attributes: [
["xlink:href", "/xl-s2h"],
@@ -535,7 +371,6 @@ describe("SVG xlink:href destinations", () => {
{ name: "svg", parent: 0 },
{
name: "a",
svg: true,
parent: 1,
attributes: [
[first, valueFor(first)],
@@ -560,21 +395,18 @@ describe("SVG xlink:href destinations", () => {
svgDocument([
{
name: "A",
svg: false,
parent: 0,
attributes: [["xlink:href", "/xl-html"]],
},
{ name: "math", parent: 0 },
{
name: "mrow",
svg: false,
parent: 2,
attributes: [["xlink:href", "/xl-math"]],
},
{ name: "svg", parent: 0 },
{
name: "a",
svg: true,
parent: 4,
attributes: [["xlink:href", "/xl-s2h"]],
},
@@ -593,7 +425,6 @@ describe("SVG xlink:href destinations", () => {
{ name: "svg", parent: 0 },
{
name: "a",
svg: true,
parent: 1,
attributes: [["xlink:href", "https://cdn.example.test/out"]],
},
@@ -613,18 +444,8 @@ describe("SVG xlink:href destinations", () => {
const result = captureSvg(
svgDocument([
{ name: "svg", parent: 0 },
{
name: "image",
svg: true,
parent: 1,
attributes: [["xlink:href", "guide"]],
},
{
name: "use",
svg: true,
parent: 1,
attributes: [["xlink:href", "icon.svg"]],
},
{ name: "image", parent: 1, attributes: [["xlink:href", "guide"]] },
{ name: "use", parent: 1, attributes: [["xlink:href", "icon.svg"]] },
]),
);
expect(result.urls).toEqual([
@@ -645,24 +466,14 @@ describe("SVG xlink:href destinations", () => {
});
describe("SVG xlink:href context", () => {
it("binds SVG identity to the selected element across documents", () => {
it("keeps forward ancestor context local to each document", () => {
const svg = svgDocument([
{
name: "a",
svg: true,
parent: 2,
attributes: [["xlink:href", "/svg-link"]],
},
{ name: "a", parent: 2, attributes: [["xlink:href", "/svg-link"]] },
{ name: "g", parent: 3 },
{ name: "svg", parent: 0 },
]);
const html = svgDocument([
{
name: "A",
svg: false,
parent: 2,
attributes: [["xlink:href", "/html-link"]],
},
{ name: "A", parent: 2, attributes: [["xlink:href", "/html-link"]] },
{ name: "DIV", parent: 3 },
{ name: "BODY", parent: 0 },
]);
@@ -696,39 +507,32 @@ describe("SVG xlink:href context", () => {
]);
});
it.each(["foreignObject", "desc", "title"])(
"preserves browser SVG identity across %s HTML integration points",
(integrationPoint) => {
const result = captureSvg(
svgDocument([
{ name: "svg", parent: 0 },
{ name: integrationPoint, parent: 1 },
{
name: "A",
svg: false,
parent: 2,
attributes: [["xlink:href", "/xl-html-fo"]],
},
{ name: "svg", parent: 2 },
{
name: "a",
svg: true,
parent: 4,
attributes: [["xlink:href", "/xl-nested"]],
},
]),
);
expect(urlFor(result, 3)).toBeUndefined();
expect(urlFor(result, 5)).toMatchObject({
attribute: "href",
url: `${origin}/xl-nested`,
destination_scope: "approved",
});
},
);
});
it("stops SVG context at foreignObject and resumes it for a nested svg", () => {
const result = captureSvg(
svgDocument([
{ name: "svg", parent: 0 },
{ name: "foreignObject", parent: 1 },
{
name: "A",
parent: 2,
attributes: [["xlink:href", "/xl-html-fo"]],
},
{ name: "svg", parent: 2 },
{
name: "a",
parent: 4,
attributes: [["xlink:href", "/xl-nested"]],
},
]),
);
expect(urlFor(result, 3)).toBeUndefined();
expect(urlFor(result, 5)).toMatchObject({
attribute: "href",
url: `${origin}/xl-nested`,
destination_scope: "approved",
});
});
describe("SVG XLink precedence and unknown identity", () => {
it("keeps an empty href ahead of xlink:href", () => {
const baseUrl = `${origin}/screens/assets/`;
const result = captureSvg(
@@ -737,7 +541,6 @@ describe("SVG XLink precedence and unknown identity", () => {
{ name: "svg", parent: 0 },
{
name: "a",
svg: true,
parent: 1,
attributes: [
["href", ""],
@@ -764,7 +567,6 @@ describe("SVG XLink precedence and unknown identity", () => {
{ name: "svg", parent: 0 },
{
name: "a",
svg: true,
parent: 1,
attributes: [["xlink:href", "javascript:void(0)"]],
},
@@ -780,7 +582,7 @@ describe("SVG XLink precedence and unknown identity", () => {
]);
});
it("keeps an unavailable SVG identity unknown despite ancestor names", () => {
it("does not treat a cyclic or dangling parent chain as SVG", () => {
const cyclic = captureSvg(
svgDocument([
{ name: "g", parent: 2 },
@@ -850,12 +652,7 @@ describe("XML SVG roots and namespace scope restoration", () => {
["xmlns:p", XLINK_NAMESPACE],
],
},
{
name: "a",
svg: true,
parent: 1,
attributes: [["p:href", "/linked"]],
},
{ name: "a", parent: 1, attributes: [["p:href", "/linked"]] },
]),
);
expect(result.urls).toEqual([approvedHref(2, `${origin}/linked`)]);
@@ -967,7 +764,6 @@ describe("XHTML and XML SVG namespace prefixes", () => {
{ name: "svg:foreignObject", parent: 2 },
{
name: "a",
svg: true,
parent: 3,
attributes: [["xlink:href", "/xl-html-fo"]],
},
@@ -1003,7 +799,6 @@ describe("XHTML and XML SVG namespace prefixes", () => {
},
{
name: "a",
svg: true,
parent: 2,
attributes: [
["xlink:href", "/not-xlink"],
@@ -1123,13 +918,11 @@ describe("XHTML SVG exclusions, HTML documents, and prefix scope", () => {
},
{
name: "a",
svg: true,
parent: 2,
attributes: [["xlink:href", "/xl-literal"]],
},
{
name: "a",
svg: true,
parent: 2,
attributes: [["x:href", "/xl-custom-html"]],
},
@@ -1144,7 +937,6 @@ describe("XHTML SVG exclusions, HTML documents, and prefix scope", () => {
},
{
name: "A",
svg: false,
parent: 1,
attributes: [["xlink:href", "/xl-html-anchor"]],
},
@@ -1152,9 +944,7 @@ describe("XHTML SVG exclusions, HTML documents, and prefix scope", () => {
);
expect(result.urls).toEqual([approvedHref(3, `${origin}/xl-literal`)]);
});
});
describe("XML namespace resolution ordering and capacity", () => {
it("applies an element's own xmlns declarations to its name and href", () => {
const result = captureSvg(
svgDocument([
+32 -128
View File
@@ -11,11 +11,7 @@ import {
} from "./CdpCaptureValues.js";
import { exclusionReasonForUrl } from "./CdpCaptureEventHelpers.js";
import type { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
import {
createDocumentSvgAncestry,
domSvgLink,
type DomSvgNodeQuery,
} from "./CdpCaptureDomSvg.js";
import { createDocumentSvgAncestry, domSvgLink } from "./CdpCaptureDomSvg.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
export type CapturedResource = Omit<
@@ -139,39 +135,18 @@ export const walkFrameTrees = function* (
}
};
interface DomMetadataCapture {
readonly urls: WebPageInspection["metadata"]["dom_urls"];
readonly agentHints: WebPageInspection["metadata"]["agent_hints"];
readonly excludedUrls: number;
readonly unresolvedSvgNodes: number;
}
export interface DomCapture extends DomMetadataCapture {
readonly total: number;
readonly nodes: WebPageInspection["dom"]["nodes"];
}
// Bound retained projections and outstanding CDP reads independently of page size.
// Eight leaves room for the session's other cancellation listeners.
const DOM_METADATA_BATCH_SIZE = 8;
interface PendingDomMetadata {
readonly options: DomMetadataOptions;
// Undefined means no query is needed; null means the candidate has no valid ID.
readonly svgQuery: DomSvgNodeQuery | null | undefined;
}
/** Project one snapshot, requesting bounded batches of SVG identity facts. */
export function* captureDom(
export const captureDom = (
result: unknown,
allowedOrigins: ReadonlySet<string>,
input: InspectWebPageInput,
completeness?: CdpCaptureCompleteness,
): Generator<
readonly DomSvgNodeQuery[],
DomCapture,
readonly (boolean | undefined)[]
> {
): {
readonly total: number;
readonly nodes: WebPageInspection["dom"]["nodes"];
readonly urls: WebPageInspection["metadata"]["dom_urls"];
readonly agentHints: WebPageInspection["metadata"]["agent_hints"];
readonly excludedUrls: number;
} => {
const root = requiredRecord(result);
const strings = Array.isArray(root.strings)
? root.strings.map((value) => cdpStringValue(value) ?? "")
@@ -181,7 +156,6 @@ export function* captureDom(
const agentHints: WebPageInspection["metadata"]["agent_hints"] = [];
let excludedUrls = 0;
let total = 0;
let unresolvedSvgNodes = 0;
for (const document of recordsValue(root.documents)) {
const documentUrl = indexedString(strings, document.documentURL);
if (allowedSanitizedUrl(documentUrl, allowedOrigins) === undefined) {
@@ -198,9 +172,6 @@ export function* captureDom(
const attributes = Array.isArray(documentNodes.attributes)
? documentNodes.attributes
: [];
const backendNodeIds = Array.isArray(documentNodes.backendNodeId)
? documentNodes.backendNodeId
: [];
const baseIndex = nodes.length;
const ancestry = createDocumentSvgAncestry({
nodeTypes,
@@ -210,29 +181,12 @@ export function* captureDom(
attributes,
});
total += nodeTypes.length;
const pendingMetadata: PendingDomMetadata[] = [];
for (let index = 0; index < nodeTypes.length; index += 1) {
const attributeIndexes = numberArray(attributes[index]);
const parent = Math.trunc(parents[index] ?? -1);
const nodeIndex = nodes.length;
const nodeName = indexedString(strings, nodeNames[index]);
const attributeNames = uniqueAttributeNames(attributeIndexes, strings);
const svgLink = domSvgLink(ancestry, index);
let svgQuery: DomSvgNodeQuery | null | undefined;
if (
!svgLink.xmlDocument &&
nodeTypes[index] === 1 &&
attributeNames.includes("xlink:href") &&
!attributeNames.includes("href")
) {
const backendNodeId = numberValue(backendNodeIds[index]);
svgQuery =
backendNodeId !== undefined &&
Number.isSafeInteger(backendNodeId) &&
backendNodeId > 0
? { backendNodeId, nodeName }
: null;
}
nodes.push({
index: nodeIndex,
parent_index: parent < 0 ? -1 : baseIndex + parent,
@@ -241,34 +195,30 @@ export function* captureDom(
node_value_length: indexedString(strings, nodeValues[index]).length,
attribute_names: attributeNames,
});
pendingMetadata.push({
svgQuery,
options: {
strings,
attributes: attributeIndexes,
// The BASE element itself resolves against the document fallback URL.
baseUrl: nodeName.toLowerCase() === "base" ? documentUrl : baseUrl,
documentUrl,
nodeIndex,
nodeName,
allowedOrigins,
...svgLink,
},
const metadata = domMetadata({
strings,
attributes: attributeIndexes,
// The BASE element itself resolves against the document fallback URL.
baseUrl: nodeName.toLowerCase() === "base" ? documentUrl : baseUrl,
documentUrl,
nodeIndex,
nodeName,
allowedOrigins,
...domSvgLink(ancestry, index),
});
if (
pendingMetadata.length === DOM_METADATA_BATCH_SIZE ||
index === nodeTypes.length - 1
) {
const metadata = yield* captureDomMetadata(
pendingMetadata,
completeness,
);
urls.push(...metadata.urls);
agentHints.push(...metadata.agentHints);
excludedUrls += metadata.excludedUrls;
unresolvedSvgNodes += metadata.unresolvedSvgNodes;
pendingMetadata.length = 0;
for (const url of metadata.urls) {
urls.push(url);
if (url.destination_scope !== "approved") {
excludedUrls += 1;
completeness?.exclude(
"metadata",
url.destination_scope === "outside_policy"
? "disallowed_origin"
: "unsupported_url",
);
}
}
agentHints.push(...metadata.agentHints);
}
}
return {
@@ -277,54 +227,8 @@ export function* captureDom(
urls,
agentHints,
excludedUrls,
unresolvedSvgNodes,
};
}
function* captureDomMetadata(
pending: readonly PendingDomMetadata[],
completeness: CdpCaptureCompleteness | undefined,
): Generator<
readonly DomSvgNodeQuery[],
DomMetadataCapture,
readonly (boolean | undefined)[]
> {
const queries = pending.flatMap(({ svgQuery }) =>
svgQuery == null ? [] : [svgQuery],
);
const kinds = queries.length > 0 ? yield queries : [];
const urls: DomMetadataCapture["urls"] = [];
const agentHints: DomMetadataCapture["agentHints"] = [];
let queryIndex = 0;
let unresolvedSvgNodes = 0;
let excludedUrls = 0;
for (const { options, svgQuery } of pending) {
let svgElement = options.svgElement;
if (svgQuery !== undefined) {
const kind = svgQuery === null ? undefined : kinds[queryIndex++];
svgElement = kind === true;
if (kind === undefined) {
unresolvedSvgNodes += 1;
completeness?.exclude("metadata", "provider_unavailable");
}
}
const metadata = domMetadata({ ...options, svgElement });
for (const url of metadata.urls) {
urls.push(url);
if (url.destination_scope !== "approved") {
excludedUrls += 1;
completeness?.exclude(
"metadata",
url.destination_scope === "outside_policy"
? "disallowed_origin"
: "unsupported_url",
);
}
}
agentHints.push(...metadata.agentHints);
}
return { urls, agentHints, unresolvedSvgNodes, excludedUrls };
}
};
export const captureAccessibility = (
results: readonly unknown[],
+47 -27
View File
@@ -1,4 +1,4 @@
import { numberValue, recordValue } from "./CdpCaptureValues.js";
import { numberValue } from "./CdpCaptureValues.js";
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
const XLINK_NAMESPACE = "http://www.w3.org/1999/xlink";
@@ -18,34 +18,11 @@ interface XmlSvgLink {
interface DocumentSvgAncestry {
readonly xml: boolean;
readonly nodes: DomSnapshotNodes;
readonly svgContexts: Map<number, boolean>;
readonly xmlLinks: Map<number, XmlSvgLink>;
}
export interface DomSvgNodeQuery {
readonly backendNodeId: number;
readonly nodeName: string;
}
/** Bind the browser's SVG flag to the exact snapshotted element. */
export const capturedSvgNodeKind = (
query: DomSvgNodeQuery,
result: unknown,
): boolean | undefined => {
const node = recordValue(recordValue(result)?.node);
if (
node?.backendNodeId !== query.backendNodeId ||
node.nodeName !== query.nodeName ||
node.nodeType !== 1
)
return undefined;
// CDP omits isSVG for non-SVG nodes. Other values are malformed, not false.
return node.isSVG === undefined
? false
: typeof node.isSVG === "boolean"
? node.isSVG
: undefined;
};
interface DomSvgLink {
readonly svgElement: boolean;
readonly xmlDocument: boolean;
@@ -102,6 +79,8 @@ export const createDocumentSvgAncestry = (
const xml = xmlDocument(nodes);
return {
xml,
nodes,
svgContexts: new Map(),
xmlLinks: xml ? resolveXmlElements(nodes) : new Map(),
};
};
@@ -112,7 +91,11 @@ export const domSvgLink = (
): DomSvgLink => {
if (!ancestry.xml) {
return {
svgElement: false,
svgElement: elementInSvgContext(
index,
ancestry.nodes,
ancestry.svgContexts,
),
xmlDocument: false,
xlinkHref: undefined,
};
@@ -125,6 +108,43 @@ export const domSvgLink = (
};
};
// Chrome's HTML parser records SVG local names (`svg`, `foreignObject`).
// Descendants of foreignObject are HTML until a nested `svg`.
const elementInSvgContext = (
index: number,
nodes: DomSnapshotNodes,
contexts: Map<number, boolean>,
): boolean => {
let current = index;
const seen = new Set<number>();
let inSvg = false;
while (
current >= 0 &&
current < nodes.nodeNames.length &&
!seen.has(current)
) {
const cached = contexts.get(current);
if (cached !== undefined) {
inSvg = cached;
break;
}
seen.add(current);
if (stringAt(nodes.strings, nodes.nodeNames[current]) === "svg") {
inSvg = true;
break;
}
const parent = Math.trunc(nodes.parents[current] ?? -1);
if (parent < 0 || parent === current) break;
if (stringAt(nodes.strings, nodes.nodeNames[parent]) === "foreignObject")
break;
current = parent;
}
// Cache the entire examined chain, including forward references and cycles,
// so shared ancestors are examined once per document.
for (const node of seen) contexts.set(node, inSvg);
return inSvg;
};
interface NamespaceRestore {
readonly prefix: string;
readonly previous: string | undefined;
+5 -49
View File
@@ -17,10 +17,8 @@ import {
captureFrames,
captureResources,
type CapturedResource,
type DomCapture,
mainFrameUrl,
} from "./CdpCaptureDocuments.js";
import { capturedSvgNodeKind } from "./CdpCaptureDomSvg.js";
import {
allowedSanitizedUrl,
delayWithCancellation,
@@ -54,7 +52,7 @@ interface CaptureState {
interface CapturedSections {
readonly attachedUrl: string;
readonly frames: WebPageInspection["frames"];
readonly dom: DomCapture;
readonly dom: ReturnType<typeof captureDom>;
readonly accessibility: ReturnType<typeof captureAccessibility>;
readonly scripts: WebPageInspection["scripts"];
readonly resources: WebPageInspection["resources"];
@@ -262,7 +260,9 @@ const authorizeObservationWindow = async (
throw new BrowserObservationError("inspect_web_page", "target_not_allowed");
};
const capturePageDom = async (state: CaptureState): Promise<DomCapture> => {
const capturePageDom = async (
state: CaptureState,
): Promise<ReturnType<typeof captureDom>> => {
const { context, allowedOrigins } = state;
const result = await context.connection.send(
"DOMSnapshot.captureSnapshot",
@@ -274,56 +274,12 @@ const capturePageDom = async (state: CaptureState): Promise<DomCapture> => {
context.sessionId,
context.signal,
);
const capture = captureDom(
return captureDom(
result,
allowedOrigins,
context.input,
state.events.completeness,
);
let step = capture.next();
let descriptionUnavailable = false;
const pendingController = new AbortController();
const signal =
context.signal === undefined
? pendingController.signal
: AbortSignal.any([context.signal, pendingController.signal]);
while (!step.done) {
const pending = step.value.map(async (query) => {
if (descriptionUnavailable) return undefined;
try {
return capturedSvgNodeKind(
query,
await context.connection.send(
"DOM.describeNode",
{ backendNodeId: query.backendNodeId, depth: 0 },
context.sessionId,
signal,
),
);
} catch (cause: unknown) {
// A detached node can disappear between snapshot and description.
// Preserve partial metadata, but propagate transport/cancellation errors.
if (!(cause instanceof CdpCommandRejection)) throw cause;
if (cause.code === -32_601) descriptionUnavailable = true;
else if (cause.code !== -32_000) throw cause;
return undefined;
}
});
try {
// Promise.all preserves snapshot order even when CDP replies out of order.
step = capture.next(await Promise.all(pending));
} catch (cause: unknown) {
// Stop and settle this bounded batch before the session can be detached.
pendingController.abort();
await Promise.allSettled(pending);
throw cause;
}
}
if (step.value.unresolvedSvgNodes > 0)
state.limitations.push(
`SVG identity was unavailable for ${String(step.value.unresolvedSvgNodes)} snapshotted XLink candidate nodes; their URL destinations remain unknown.`,
);
return step.value;
};
const normalizedInspection = (
+2 -3
View File
@@ -216,9 +216,8 @@ export const createCliResultOutput = (
: filtered === undefined
? { ok: true, meta }
: { ok: true, data: filtered, meta }
: // A selected null is a value; only a selection with no JSON value
// falls back to the empty document.
filtered === undefined ||
: filtered === undefined ||
filtered === null ||
typeof filtered === "function" ||
typeof filtered === "symbol"
? {}
+3 -29
View File
@@ -9,7 +9,7 @@ export type CliOutputArgumentValidation =
| {
readonly ok: false;
readonly format: StructuredOutputFormat;
readonly code: "UNSUPPORTED_OUTPUT_COMBINATION" | "INVALID_OUTPUT_FILTER";
readonly code: "UNSUPPORTED_OUTPUT_COMBINATION";
readonly message: string;
};
@@ -89,39 +89,13 @@ export const parseCliOutputArguments = (
return parsed(false);
};
/** Reject slices that cannot advance within Incur's comma-delimited paths. */
const hasUnclosedFilterSlice = (expression: string): boolean => {
let depth = 0;
let unclosed = false;
for (const character of expression) {
if (character === "[") {
depth += 1;
unclosed = true;
} else if (character === "]") {
depth -= 1;
// Incur consumes everything through the next closing bracket, including
// nested opening brackets. Do not impose a different filter grammar.
unclosed = false;
} else if (character === "," && depth === 0 && unclosed) return true;
}
return unclosed;
};
/** Reject output controls that cannot safely produce a complete result. */
/** Reject text-window controls that would corrupt a structured document. */
export const validateCliOutputArguments = (
arguments_: readonly string[],
): CliOutputArgumentValidation => {
const { filterOutput, format, parseError, tokenWindow } =
const { format, parseError, tokenWindow } =
parseCliOutputArguments(arguments_);
if (parseError) return { ok: true };
if (filterOutput !== undefined && hasUnclosedFilterSlice(filterOutput))
return {
ok: false,
format: format === "yaml" || format === "jsonl" ? format : "json",
code: "INVALID_OUTPUT_FILTER",
message:
"The --filter-output expression has a slice without a closing ]. Close the slice or remove --filter-output.",
};
if (
tokenWindow &&
(format === "json" || format === "jsonl" || format === "yaml")
+1 -1
View File
@@ -107,7 +107,7 @@ export const NATIVE_TOOL_CONTRACTS = [
),
native(
"inspect_signature",
"Inspect the active Mach-O executable's code-signing identity, hashes, authorities, requirements, entitlements, hardened-runtime state, and exact command provenance.",
"Inspect the active artifact's code-signing identity, hashes, authorities, requirements, entitlements, hardened-runtime state, and exact command provenance.",
z.object({}),
),
native(
+2 -2
View File
@@ -149,12 +149,12 @@ export const OFFICIAL_TOOL_CONTRACTS = [
),
official(
"procedure_callees",
"Return the provider's resolved direct callees for one procedure identified by symbol or address. Unresolved indirect calls may be absent; use analyze_function and typed references to preserve available edge uncertainty. Ghidra terminal-call assumptions that may cut the analyzed body are reported in Evidence limitations.",
"Return the provider's resolved direct callees for one procedure identified by symbol or address. Unresolved indirect calls may be absent; use analyze_function and typed references to preserve available edge uncertainty.",
z.object({ procedure, document }),
),
official(
"procedure_callers",
"Return the provider's resolved direct callers for one procedure identified by symbol or address. Results reflect completed static analysis and may omit unresolved indirect references. Ghidra terminal-call assumptions that may cut the analyzed body are reported in Evidence limitations.",
"Return the provider's resolved direct callers for one procedure identified by symbol or address. Results reflect completed static analysis and may omit unresolved indirect references.",
z.object({ procedure, document }),
),
official(
@@ -6,34 +6,6 @@ import { z } from "zod";
import { toolContract } from "./toolContracts.js";
import { toolOutputSchemaWithMetadata } from "./toolSchemaMetadata.js";
import { createEvidence } from "../domain/evidence.js";
it.each(["draft-2020-12", "draft-07"] as const)(
"advertises the raw-only 6502 Evidence subject constraint in %s",
(target) => {
const valid = createEvidence(
{
path: "/fixture/image.bin",
sha256: "a".repeat(64),
format: "raw-binary",
architecture: "6502",
},
{ id: "fixture", name: "Fixture", version: "1" },
{ operation: "address_name", parameters: {}, result: "entry" },
);
const advertised = toolOutputSchemaWithMetadata(
toolContract("address_name"),
)["~standard"].jsonSchema.output({ target });
const validate = new AjvJsonSchemaValidator().getValidator(advertised);
expect(validate(valid).valid).toBe(true);
expect(
validate({
...valid,
subject: { ...valid.subject, format: "pe" },
}).valid,
).toBe(false);
},
);
it("keeps dialect-specific validators distinct after JSON schema serialization", () => {
const contract = {
+1 -1
View File
@@ -444,11 +444,11 @@ const lifecycleTargetFields = {
path: z.string(),
kind: targetKindSchema,
sha256: z.string().regex(/^[a-f0-9]{64}$/u),
architecture: z.enum(BINARY_ARCHITECTURES).nullable(),
};
const detectedLifecycleTargetSchema = z.object({
...lifecycleTargetFields,
format: targetFormatSchema.exclude(["raw-binary"]),
architecture: z.enum(BINARY_ARCHITECTURES).exclude(["6502"]).nullable(),
});
const rawLifecycleTargetSchema = z.object({
...lifecycleTargetFields,
+2 -65
View File
@@ -52,7 +52,7 @@ const snapshotWithHistoricalEvidence = () => {
};
return {
current,
snapshot: analysisSnapshotSchema.parse({
snapshot: {
target,
binding,
entries: [entry],
@@ -66,32 +66,11 @@ const snapshotWithHistoricalEvidence = () => {
result: { summary: "unprofiled" },
}),
]),
}),
} satisfies AnalysisSnapshot,
};
};
describe("analysis snapshot target identity", () => {
it("keeps 6502 confined to executable raw snapshots", () => {
const target = {
sha256: ANALYSIS_SNAPSHOT_TARGET.sha256,
kind: "executable",
architecture: "6502",
};
expect(
analysisSnapshotSchema.shape.target.safeParse({
...target,
format: "pe",
}).success,
).toBe(false);
expect(
analysisSnapshotSchema.shape.target.safeParse({
...target,
format: "raw-binary",
entry_point: "0x8001",
}).success,
).toBe(true);
});
it("partitions raw snapshots and query identities by the canonical entry", () => {
const target: BinaryTarget = {
path: "/fixture/image.bin",
@@ -141,48 +120,6 @@ describe("analysis snapshot target identity", () => {
});
});
describe("analysis snapshot nested subject contract", () => {
it("rejects raw-only 6502 in nested detected execution subjects", () => {
const { snapshot } = snapshotWithHistoricalEvidence();
const entry = snapshot.entries[0];
if (entry === undefined) throw new Error("Fixture has no snapshot entry");
const malformedSubject = {
...entry.execution.subject,
format: "pe",
architecture: "6502",
};
expect(
analysisSnapshotSchema.safeParse({
...snapshot,
entries: [
{
...entry,
execution: {
...entry.execution,
subject: malformedSubject,
},
},
],
}).success,
).toBe(false);
expect(
analysisSnapshotSchema.safeParse({
...snapshot,
workflow_entries: [
{
...entry,
execution: {
...entry.execution,
analysis_profile: snapshot.binding.analysis_profile,
subject: malformedSubject,
},
},
],
}).success,
).toBe(false);
});
});
describe("analysis snapshot contract", () => {
it.each([
["dos-mz", "x86"],
+28 -47
View File
@@ -41,10 +41,7 @@ import { digestSchema } from "./../domain/digests.js";
import { prefixedDigestSchema } from "./../domain/digests.js";
import { canonicalJsonDigestSteps } from "./canonicalJsonDigestSteps.js";
import { freezeJsonSnapshot } from "./immutableJson.js";
import {
binaryArchitectureSchema,
rawBinaryEntryPointSchema,
} from "./rawBinary.js";
import { rawBinaryEntryPointSchema } from "./rawBinary.js";
import {
relationshipCoverageSchema,
type RelationshipCoverage,
@@ -76,30 +73,20 @@ const formatSchema = z.enum([
"source-map",
]);
const kindSchema = z.enum(["executable", "database", "archive", "artifact"]);
const subjectBaseSchema = z.object({
const subjectSchema = z.object({
path: localPathStringSchema,
sha256: digestSchema,
format: z.enum([
...formatSchema.options,
"hopper",
"directory",
"file",
"unknown",
"javascript-bundle",
"entitlements",
]),
architecture: architectureSchema.nullable(),
});
const subjectFormatSchema = z.enum([
...formatSchema.options,
"hopper",
"directory",
"file",
"unknown",
"javascript-bundle",
"entitlements",
]);
// Snapshot entries retain the same raw-only architecture boundary as targets.
const subjectSchema = z.discriminatedUnion("format", [
subjectBaseSchema.extend({
format: subjectFormatSchema.exclude(["raw-binary"]),
architecture: architectureSchema.exclude(["6502"]).nullable(),
}),
subjectBaseSchema.extend({
format: z.literal("raw-binary"),
architecture: architectureSchema.nullable(),
}),
]);
const targetIdentitySchema = z.object({
sha256: digestSchema,
kind: kindSchema,
@@ -110,12 +97,9 @@ const targetSchema = z.discriminatedUnion("format", [
format: z.enum(
formatSchema.options.filter((format) => format !== "raw-binary"),
),
architecture: architectureSchema.exclude(["6502"]).nullable(),
}),
targetIdentitySchema.extend({
format: z.literal("raw-binary"),
kind: z.literal("executable"),
architecture: binaryArchitectureSchema,
entry_point: rawBinaryEntryPointSchema,
}),
]);
@@ -348,21 +332,18 @@ const canonicalJson = (value: unknown): string => {
};
/** Project a binary into the path-free identity used for cache invalidation. */
export const snapshotTarget = (target: BinaryTarget): AnalysisSnapshotTarget =>
target.format === "raw-binary"
? {
sha256: target.sha256,
kind: target.kind,
architecture: target.architecture,
format: target.format,
entry_point: target.entryPoint,
}
: {
sha256: target.sha256,
kind: target.kind,
architecture: target.architecture ?? null,
format: target.format,
};
export const snapshotTarget = (
target: BinaryTarget,
): AnalysisSnapshotTarget => {
const identity = {
sha256: target.sha256,
kind: target.kind,
architecture: target.architecture ?? null,
};
return target.format === "raw-binary"
? { ...identity, format: target.format, entry_point: target.entryPoint }
: { ...identity, format: target.format };
};
/** Build the immutable provider/profile binding used by a cached snapshot. */
export const snapshotBinding = (
@@ -494,10 +475,10 @@ export const createAnalysisSnapshotWorkflowEntry = (input: {
subject:
execution.subject === null
? null
: subjectSchema.parse({
: {
...execution.subject,
architecture: execution.subject.architecture ?? null,
}),
},
},
};
};
@@ -557,10 +538,10 @@ export const createAnalysisSnapshotEntry = (input: {
subject:
execution.subject === null
? null
: subjectSchema.parse({
: {
...execution.subject,
architecture: execution.subject.architecture ?? null,
}),
},
},
};
};
+1 -19
View File
@@ -169,8 +169,7 @@ it("pages every retained dossier collection and the native API record", () => {
| "comments"
| "unresolved_calls"
| "referenced_strings"
| "referenced_names"
| "swift_string_literals",
| "referenced_names",
result: unknown = original,
) => {
const view = projectAnalysisView(nativeParent(result), {
@@ -196,23 +195,6 @@ it("pages every retained dossier collection and the native API record", () => {
}),
).toMatchObject({ item: [{ text: "entry" }] });
expect(page("referenced_names")).toMatchObject({ coverage: { total: 0 } });
const literal = {
source_address: "0x401004",
value: "SleepDisabled",
representation: "small",
address: null,
registers: ["x0", "x1"],
words: ["0x7369447065656c53", "0xed000064656c6261"],
};
expect(
page("swift_string_literals", {
...original,
swift_string_literals: [literal],
}),
).toMatchObject({ item: [literal], coverage: { total: 1 } });
expect(page("swift_string_literals")).toMatchObject({
item: { available: false, reason: "not recorded" },
});
const { unresolved_calls: _omitted, ...withoutUnresolved } = original;
expect(
page("unresolved_calls", {
-2
View File
@@ -160,8 +160,6 @@ const nativeViewSchema = z.strictObject({
"unresolved_calls",
"referenced_strings",
"referenced_names",
"swift_string_literals",
"swift_string_literal_omissions",
"native_api",
"value_flow_summary",
"value_flow_operations",
@@ -79,8 +79,6 @@ const nativeRows = (
unresolved_calls: dossier.unresolved_calls,
referenced_strings: dossier.referenced_strings,
referenced_names: dossier.referenced_names,
swift_string_literals: dossier.swift_string_literals,
swift_string_literal_omissions: dossier.swift_string_literal_omissions,
value_flow_operations: flow?.operations,
value_flow_def_use: flow?.def_use,
value_flow_effects: flow?.effects,
@@ -165,8 +163,7 @@ export const projectNativeFunctionView = (
singleton({
available: false,
// Only value-flow facets inherit the value-flow failure reason; an
// older dossier simply has no unresolved_calls record, and a target
// whose provider does not decode Swift literals has no such record.
// older dossier simply has no unresolved_calls record.
reason:
view.facet.startsWith("value_flow_") && flow?.available === false
? flow.reason
@@ -510,106 +510,6 @@ describe("AppKit keyed archive connectors", () => {
});
});
describe("keyed archive unresolved connectors", () => {
// A rooted UIKit archive whose only connector is UID 3, with UID 1 as the
// owner, UID 2 as the destination and UID 7 as the "view" label.
const rootedOutletArchive = (connection: Record<string, unknown>) => ({
$archiver: "NSKeyedArchiver",
$version: 100000,
$objects: [
"$null",
{ $class: { CF$UID: 4 } },
{ $class: { CF$UID: 5 } },
{ $class: { CF$UID: 6 }, ...connection },
{
$classname: "UIViewController",
$classes: ["UIViewController", "UIResponder", "NSObject"],
},
{ $classname: "UIView", $classes: ["UIView", "UIResponder", "NSObject"] },
{
$classname: "UIRuntimeOutletConnection",
$classes: [
"UIRuntimeOutletConnection",
"UIRuntimeConnection",
"NSObject",
],
},
"view",
{
$class: { CF$UID: 11 },
"NS.objects": [{ CF$UID: 1 }, { CF$UID: 2 }, { CF$UID: 3 }],
},
{ $class: { CF$UID: 11 }, "NS.objects": [{ CF$UID: 3 }] },
{
$class: { CF$UID: 11 },
"NS.objects": [{ CF$UID: 1 }, { CF$UID: 2 }],
},
{ $classname: "NSArray", $classes: ["NSArray", "NSObject"] },
],
$top: {
root: { CF$UID: 8 },
UINibConnectionsKey: { CF$UID: 9 },
UINibObjectsKey: { CF$UID: 10 },
UINibTopLevelObjectsKey: { CF$UID: 10 },
},
});
const project = (raw: unknown) =>
buildInterfaceBuilderAnalysis({
targetSha256: hash,
toolVersion: "test",
documents: [
{
relativePath: "Main.nib",
archiveSha256: hash,
documentKind: "nib",
raw,
},
],
limits: interfaceBuilderLimitsSchema.parse({}),
});
it("covers a resolvable connector as a complete examined connection", () => {
const raw = rootedOutletArchive({
UISource: { CF$UID: 1 },
UIDestination: { CF$UID: 2 },
UILabel: { CF$UID: 7 },
});
expect(parseInterfaceBuilderRecords(raw).unresolvedConnections).toBe(0);
expect(project(raw).graph.coverage).toContainEqual(
expect.objectContaining({
facet: "connections:Main.nib",
status: "complete",
examined: 1,
omitted: 0,
}),
);
});
it("reports a recognized connector with no resolvable owner as unresolved", () => {
const raw = rootedOutletArchive({
UIDestination: { CF$UID: 2 },
UILabel: { CF$UID: 7 },
});
expect(parseInterfaceBuilderRecords(raw).unresolvedConnections).toBe(1);
const analysis = project(raw);
expect(analysis.graph.nodes.some(({ kind }) => kind === "outlet")).toBe(
false,
);
expect(analysis.graph.coverage).toContainEqual(
expect.objectContaining({
facet: "connections:Main.nib",
status: "partial",
reason: "connection_unresolved",
examined: 0,
omitted: 1,
}),
);
expect(analysis.graph.truncated).toBe(true);
});
});
it("names the archive once per evidence record", () => {
const relativePath = "Views/Main.storyboardc/scene.nib/objects.nib";
const result = buildInterfaceBuilderAnalysis({
+5 -18
View File
@@ -410,26 +410,18 @@ export const buildInterfaceBuilderAnalysis = (input: {
const connectionsTruncated =
parserOmittedConnections > 0 ||
parsed.connections.length > input.limits.max_connections;
// A recognized connector the parser could not attach to an owner produces
// no connection record, so its coverage is partial with its own reason
// rather than absent or truncated.
const connectionsUnresolved = parsed.unresolvedConnections > 0;
omittedObjects +=
parsed.omittedObjects +
Math.max(0, parsed.objects.length - documentObjectCount);
omittedConnections +=
Math.max(0, parsed.connections.length - input.limits.max_connections) +
parserOmittedConnections +
parsed.unresolvedConnections;
parserOmittedConnections;
summaries.push({
relative_path: document.relativePath,
archive_sha256: document.archiveSha256,
document_kind: document.documentKind,
object_count: parsed.objectCount,
connection_count:
parsed.connections.length +
parserOmittedConnections +
parsed.unresolvedConnections,
connection_count: parsed.connections.length + parserOmittedConnections,
hierarchy_complete:
parsed.hierarchy.length > 0 &&
!truncated &&
@@ -448,17 +440,13 @@ export const buildInterfaceBuilderAnalysis = (input: {
{
facet: `connections:${document.relativePath}`,
status:
connectionsTruncated ||
documentOmittedConnections > 0 ||
connectionsUnresolved
connectionsTruncated || documentOmittedConnections > 0
? "partial"
: "complete",
reason:
connectionsTruncated || documentOmittedConnections > 0
? "connection_limit_reached"
: connectionsUnresolved
? "connection_unresolved"
: null,
: null,
examined: Math.min(
parsed.connections.length,
input.limits.max_connections,
@@ -469,8 +457,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
parsed.connections.length - input.limits.max_connections,
) +
documentOmittedConnections +
parserOmittedConnections +
parsed.unresolvedConnections,
parserOmittedConnections,
},
{
facet: `hierarchy:${document.relativePath}`,
@@ -55,7 +55,6 @@ export const parseInterfaceBuilderRecords = (
readonly objectCount: number;
readonly omittedObjects: number;
readonly omittedConnections: number;
readonly unresolvedConnections: number;
readonly omittedHierarchyReferences: number;
} => {
const root = record(value);
@@ -136,7 +135,6 @@ export const parseInterfaceBuilderRecords = (
objectCount: objectEntries.length,
omittedObjects: Math.max(0, objectEntries.length - 20_000),
omittedConnections,
unresolvedConnections: 0,
omittedHierarchyReferences: 0,
};
};
@@ -322,7 +320,6 @@ const parseKeyedArchive = (
const connections: InterfaceBuilderConnection[] = [];
let objectCount = 0;
let omittedConnections = 0;
let unresolvedConnections = 0;
for (const [index, raw] of objectTable.entries()) {
if (index === 0) continue;
const resolved = record(resolve(raw));
@@ -374,13 +371,7 @@ const parseKeyedArchive = (
controlAction ? fields.to : fields.from,
controlAction ? fields.target : fields.owner,
);
if (source === null) {
// A recognized connector that cannot name its owner is an unresolved
// connection, not an absent one, and cannot be represented as a
// connection because connection identity requires a source.
unresolvedConnections += 1;
continue;
}
if (source === null) continue;
const kind = controlAction ? "action" : classifyConnection(className);
const parsedConnection = connection.parse({
id: `archive:${objectId}`,
@@ -435,7 +426,6 @@ const parseKeyedArchive = (
objectCount,
omittedObjects: Math.max(0, objectCount - 20_000),
omittedConnections,
unresolvedConnections,
omittedHierarchyReferences: hierarchy.omittedReferences,
};
};
-5
View File
@@ -63,11 +63,6 @@ export const readFatContainerHeader = (
const magic = view.getUint32(0, false);
if (![FAT_MAGIC, FAT_MAGIC_64, FAT_CIGAM, FAT_CIGAM_64].includes(magic))
return { state: "not-fat" };
if (!Number.isSafeInteger(fileSize) || fileSize < bytes.byteLength)
return {
state: "malformed",
reason: "FAT file size is invalid or contradicts retained bytes",
};
if (bytes.byteLength < 8) {
return fileSize < 8
? { state: "malformed", reason: "FAT header is truncated" }
+3 -9
View File
@@ -278,12 +278,8 @@ export const parsePeRecord = (
record.toString("binary", 0, 4) !== "PE\u0000\u0000"
)
return err("invalid or truncated PE header");
const machineType = record.readUInt16LE(4);
const machine = decodePeMachine(machineType);
if (machine === undefined)
return err(
`unsupported PE architecture (COFF machine 0x${machineType.toString(16).padStart(4, "0")})`,
);
const machine = decodePeMachine(record.readUInt16LE(4));
if (machine === undefined) return err("unsupported PE architecture");
const { architecture } = machine;
const optionalHeaderSize = record.readUInt16LE(20);
if (optionalHeaderSize > 4096 || record.length < 24 + optionalHeaderSize)
@@ -351,9 +347,7 @@ const machArchitecture = (cpu: number): HeaderArchitecture | undefined => {
return undefined;
};
const elfArchitecture = (
machine: number,
): Exclude<BinaryArchitecture, "6502"> | undefined => {
const elfArchitecture = (machine: number): BinaryArchitecture | undefined => {
switch (machine) {
case 3:
return "x86";
+3 -13
View File
@@ -1,13 +1,12 @@
import type { ZipPackageFormat } from "./zipPackageFormat.js";
/** Provider-neutral CPU families detected from headers or explicitly selected raw loads. */
/** Provider-neutral CPU families detected from supported executable headers. */
export const BINARY_ARCHITECTURES = [
"x86",
"x86_64",
"arm",
"arm64",
"mips",
"6502",
"riscv",
] as const;
export type BinaryArchitecture = (typeof BINARY_ARCHITECTURES)[number];
@@ -41,7 +40,7 @@ export const isElfMetadataArchitecture = (
/** Recognized family with no ELF metadata member; the header alone is the fact. */
export type HeaderArchitecture = Exclude<
BinaryArchitecture,
ElfMetadataArchitecture | "6502"
ElfMetadataArchitecture
>;
/** Raw ELF facts; family recognition does not establish ISA or ABI support. */
@@ -145,17 +144,8 @@ export type BinaryTarget =
readonly executableRole?: never;
readonly managed?: never;
})
| (BinaryTargetIdentity & {
readonly kind: "executable";
| (ExecutableTarget & {
readonly format: "raw-binary";
readonly architecture: Exclude<
BinaryArchitecture,
ElfMetadataArchitecture
>;
readonly availableArchitectures: readonly Exclude<
BinaryArchitecture,
ElfMetadataArchitecture
>[];
readonly entryPoint: string;
readonly entryPointSource: string;
readonly executableRole?: never;
-83
View File
@@ -1,7 +1,6 @@
import { describe, expect, it } from "vitest";
import { buildCallPath } from "./callPath.js";
import { combineFunctionRelationshipCoverage } from "./functionCallCoverage.js";
import { callPathResultSchema, type CallPathInput } from "./callPathSchemas.js";
import { createEvidence, type Evidence } from "./evidence.js";
import type { JsonValue } from "./jsonValue.js";
@@ -397,60 +396,6 @@ describe("explicit call relationship coverage", () => {
expect(result.limitations).toEqual([reason]);
});
it("establishes not_found when only caller coverage is unknown", () => {
const reason =
"Callers: Upstream xrefs do not establish direct caller relationships.";
const result = run([
observe("0x1000", ["0x2000"], "rea-workflow", {
relationshipCoverage: { status: "unknown", reason },
}),
observe("0x2000", [], "rea-workflow", {
relationshipCoverage: { status: "unknown", reason },
}),
]);
expect(result).toMatchObject({
status: "not_found",
search_scope: { exhaustive: true },
});
expect(result.limitations).toContain(reason);
});
it("stays unknown when callee coverage is unknown", () => {
const reason =
"Callees: Upstream callees capped results at 500; additional callees remain unknown.";
const result = run([
observe("0x1000", ["0x2000"], "rea-workflow", {
relationshipCoverage: { status: "unknown", reason },
}),
observe("0x2000", [], "rea-workflow", {
relationshipCoverage: { status: "unknown", reason },
}),
]);
expect(result).toMatchObject({
status: "unknown",
search_scope: { exhaustive: false },
});
expect(result.limitations).toContain(reason);
});
it("stays unknown when both caller and callee coverage are unknown", () => {
const reason =
"Callers: Upstream xrefs do not establish direct caller relationships. Callees: Upstream callees are not observed.";
const result = run([
observe("0x1000", ["0x2000"], "rea-workflow", {
relationshipCoverage: { status: "unknown", reason },
}),
observe("0x2000", [], "rea-workflow", {
relationshipCoverage: { status: "unknown", reason },
}),
]);
expect(result).toMatchObject({
status: "unknown",
search_scope: { exhaustive: false },
});
expect(result.limitations).toContain(reason);
});
it("ignores incomplete relationship coverage outside the reachable search closure", () => {
const reason = "An unrelated function's callees were not observed.";
const result = run([
@@ -470,34 +415,6 @@ describe("explicit call relationship coverage", () => {
});
});
describe("IDA-capped callee coverage beside unknown callers", () => {
it("keeps the search non-exhaustive for the combined coverage value", () => {
const combined = combineFunctionRelationshipCoverage(
{
status: "unknown",
reason: "Upstream xrefs do not establish direct caller relationships.",
},
{
status: "partial",
limitations: [
"Upstream callees capped results at 500; additional callees remain unknown.",
],
},
);
expect(combined.status).toBe("unknown");
const result = run([
observe("0x1000", [], "rea-workflow", { relationshipCoverage: combined }),
observe("0x4000", []),
]);
expect(result).toMatchObject({
status: "unknown",
search_scope: { exhaustive: false },
shortest_hops: null,
paths: [],
});
});
});
describe("call path evidence admission", () => {
it("normalizes hex addresses and rejects duplicates and mixed providers", () => {
expect(
+1 -6
View File
@@ -10,7 +10,6 @@ import {
parseFunctionEvidence,
type FunctionSnapshot,
} from "./functionDossierEvidence.js";
import { isCallerOnlyUnknownCoverage } from "./functionCallCoverage.js";
import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js";
/** Minimal directed caller-to-callee adjacency for call-path search. */
@@ -256,14 +255,10 @@ const inspectSearch = ({
// Explicit, Evidence-bound relationship coverage owns completeness;
// generic provider/dossier limitations remain contextual. Legacy Evidence
// without that metadata retains its conservative limitation-based rule.
// The search only expands callee edges, so coverage that is unknown
// solely because callers are unobservable does not make the searched
// callee closure incomplete; the caller caveat stays in limitations.
if (
coverage === null
? snapshot.limitations.length > 0
: coverage.status !== "complete" &&
!isCallerOnlyUnknownCoverage(coverage)
: coverage.status !== "complete"
)
incompleteExpandedCoverage = true;
} else if (node === goal) {
-27
View File
@@ -19,33 +19,6 @@ const TARGET: BinaryTarget = {
const PROVIDER = { id: "fixture", name: "Fixture provider", version: "1" };
const PROFILE = createAnalysisProfile(PROVIDER, { loader: "default" });
it("rejects a detected Evidence subject with the raw-only 6502 architecture", () => {
const selected = {
path: TARGET.path,
sha256: TARGET.sha256,
format: "raw-binary" as const,
architecture: "6502" as const,
};
expect(
createEvidence(selected, PROVIDER, {
operation: "inspect",
parameters: {},
result: null,
}).subject,
).toMatchObject({ format: "raw-binary", architecture: "6502" });
const malformed = {
...selected,
format: "pe" as const,
};
expect(() =>
createEvidence(malformed, PROVIDER, {
operation: "inspect",
parameters: {},
result: null,
}),
).toThrow();
});
it("preserves omitted relationship diagnostics and authenticates reported diagnostics", () => {
const observation = {
operation: "procedure_callees",
+33 -43
View File
@@ -38,52 +38,42 @@ export const providerSchema = z.strictObject({
name: z.string().min(1),
version: z.string().nullable(),
});
const subjectFormatSchema = z.enum([
"hopper",
"analysis-database",
"mach-o",
"elf",
"pe",
"dos-mz",
"dos-com",
"raw-binary",
"zip",
"ipa",
"apk",
"msix",
"appx",
"hap",
"hsp",
"app-pack",
"asar",
"dmg",
"pkg",
"plist",
"javascript",
"source-map",
"directory",
"file",
"unknown",
"mach-o-universal",
"javascript-bundle",
"entitlements",
]);
const subjectBaseSchema = z.strictObject({
const subjectSchema = z.strictObject({
name: z.string().min(1),
digest: z.strictObject({ sha256: digestSchema }),
format: z.enum([
"hopper",
"analysis-database",
"mach-o",
"elf",
"pe",
"dos-mz",
"dos-com",
"raw-binary",
"zip",
"ipa",
"apk",
"msix",
"appx",
"hap",
"hsp",
"app-pack",
"asar",
"dmg",
"pkg",
"plist",
"javascript",
"source-map",
"directory",
"file",
"unknown",
"mach-o-universal",
"javascript-bundle",
"entitlements",
]),
architecture: z.enum(BINARY_ARCHITECTURES).nullable(),
local_path: nulFreeStringSchema,
});
// The format owns the architecture claim, including nested Evidence subjects.
const subjectSchema = z.discriminatedUnion("format", [
subjectBaseSchema.extend({
format: subjectFormatSchema.exclude(["raw-binary"]),
architecture: z.enum(BINARY_ARCHITECTURES).exclude(["6502"]).nullable(),
}),
subjectBaseSchema.extend({
format: z.literal("raw-binary"),
architecture: z.enum(BINARY_ARCHITECTURES).nullable(),
}),
]);
/** Source location attached to an evidence observation. */
/** Source-location schema shared by evidence-bearing persistence formats. */
export const evidenceLocationSchema = z.discriminatedUnion("kind", [
@@ -196,7 +186,7 @@ const immutableResultSchema = z
});
export type EvidenceLocation = z.infer<typeof evidenceLocationSchema>;
/** Candidate local artifact identity validated by the Evidence subject schema. */
/** Minimal immutable local artifact identity accepted by Evidence. */
export interface EvidenceSubjectTarget {
readonly path: string;
readonly sha256: string;
+10 -13
View File
@@ -16,17 +16,14 @@ import {
} from "./residualUnknown.js";
import { freezeJsonSnapshot } from "./immutableJson.js";
const [detectedSubjectSchema, rawSubjectSchema] =
evidenceSchema.shape.subject.unwrap().options;
const manifestFields = {
digest: true,
format: true,
architecture: true,
} as const;
const artifactManifestSchema = z.discriminatedUnion("format", [
detectedSubjectSchema.pick(manifestFields),
rawSubjectSchema.pick(manifestFields),
]);
const artifactManifestSchema = evidenceSchema.shape.subject
.unwrap()
.pick({
digest: true,
format: true,
architecture: true,
})
.strict();
const providerManifestSchema = providerSchema;
const environmentManifestSchema = evidenceSchema.shape.environment.unwrap();
const scenarioManifestSchema = z.strictObject({
@@ -88,11 +85,11 @@ export const createEvidenceBundle = (
subject === null
? []
: [
artifactManifestSchema.parse({
{
digest: subject.digest,
format: subject.format,
architecture: subject.architecture,
}),
},
],
),
),
-64
View File
@@ -1,64 +0,0 @@
import { describe, expect, it } from "vitest";
import {
combineFunctionRelationshipCoverage,
isCallerOnlyUnknownCoverage,
} from "./functionCallCoverage.js";
import type { RelationshipCoverage } from "./relationshipCoverage.js";
const unknownCallers: RelationshipCoverage = {
status: "unknown",
reason: "Upstream xrefs do not establish direct caller relationships.",
};
const unknownCallees: RelationshipCoverage = {
status: "unknown",
reason: "Upstream callees are not observed in this profile.",
};
const cappedCallees: RelationshipCoverage = {
status: "partial",
limitations: [
"Upstream callees capped results at 500; additional callees remain unknown.",
],
};
const complete: RelationshipCoverage = { status: "complete" };
describe("isCallerOnlyUnknownCoverage", () => {
it("detects unknown callers beside complete callees", () => {
const combined = combineFunctionRelationshipCoverage(
unknownCallers,
complete,
);
expect(combined.status).toBe("unknown");
expect(isCallerOnlyUnknownCoverage(combined)).toBe(true);
});
it("rejects unknown callees beside complete callers", () => {
const combined = combineFunctionRelationshipCoverage(
complete,
unknownCallees,
);
expect(isCallerOnlyUnknownCoverage(combined)).toBe(false);
});
it("rejects unknown coverage on both sides", () => {
const combined = combineFunctionRelationshipCoverage(
unknownCallers,
unknownCallees,
);
expect(isCallerOnlyUnknownCoverage(combined)).toBe(false);
});
it("rejects unknown callers beside IDA-capped callees", () => {
const combined = combineFunctionRelationshipCoverage(
unknownCallers,
cappedCallees,
);
expect(combined.status).toBe("unknown");
expect(isCallerOnlyUnknownCoverage(combined)).toBe(false);
});
it("rejects complete coverage", () => {
const combined = combineFunctionRelationshipCoverage(complete, complete);
expect(isCallerOnlyUnknownCoverage(combined)).toBe(false);
});
});
+2 -20
View File
@@ -2,9 +2,6 @@ import type { RelationshipCoverage } from "./relationshipCoverage.js";
import type { FunctionDossier } from "./hopperValues.js";
import { uniqueSorted } from "./canonicalOrdering.js";
const callerCoveragePrefix = "Callers: ";
const calleeCoveragePrefix = "Callees: ";
/** Combine caller and callee read outcomes without losing either explanation. */
export const combineFunctionRelationshipCoverage = (
callers: RelationshipCoverage | undefined,
@@ -23,8 +20,7 @@ export const combineFunctionRelationshipCoverage = (
);
const details = diagnostics.length > 0 ? { diagnostics } : {};
const reasons = reads.flatMap(({ direction, coverage }) => {
const label =
direction === "callers" ? callerCoveragePrefix : calleeCoveragePrefix;
const label = direction === "callers" ? "Callers" : "Callees";
const limitations =
coverage === undefined
? ["Provider did not report relationship coverage."]
@@ -33,7 +29,7 @@ export const combineFunctionRelationshipCoverage = (
: coverage.status === "partial"
? coverage.limitations
: [];
return limitations.map((reason) => `${label}${reason}`);
return limitations.map((reason) => `${label}: ${reason}`);
});
if (
reads.some(
@@ -54,20 +50,6 @@ export const combineFunctionRelationshipCoverage = (
return { status: "complete", ...details };
};
/**
* Whether combined relationship coverage is unknown only on the caller side.
* The combine helper above prefixes each direction's reasons, so a reason
* that carries the caller prefix and no callee prefix records observed
* callees with unobservable callers. Forward (callee-edge) searches stay
* exhaustive under that caveat; the caller reason remains a limitation.
*/
export const isCallerOnlyUnknownCoverage = (
coverage: RelationshipCoverage,
): boolean =>
coverage.status === "unknown" &&
coverage.reason.startsWith(callerCoveragePrefix) &&
!coverage.reason.includes(calleeCoveragePrefix);
/** Explain why two resolved call sets cannot establish call-set equality. */
export const functionCallCoverageLimitations = (
coverage: RelationshipCoverage | null,
-230
View File
@@ -716,236 +716,6 @@ describe("function comparison identity and malformed CFG", () => {
});
});
const withLiterals = (base: "0x1000" | "0x2000", values: readonly string[]) =>
functionDossierSchema.parse({
...dossier("return 0;", base),
swift_string_literals: values.map((value) => ({
source_address: base,
value,
representation: "small" as const,
address: null,
registers: ["x0", "x1"] as const,
words: ["0x0", "0xe"] as const,
})),
});
const stringsDimension = (
leftDigit: string,
left: ReturnType<typeof withLiterals> | ReturnType<typeof dossier>,
rightDigit: string,
right: ReturnType<typeof withLiterals> | ReturnType<typeof dossier>,
) =>
compareFunctions(
observe(leftDigit, left),
observe(rightDigit, right),
).dimensions.find(({ dimension }) => dimension === "strings_names");
describe("function comparison Swift string literals", () => {
it("treats differing decoded literals as a strings and names change", () => {
expect(
stringsDimension(
"1",
withLiterals("0x1000", ["SleepDisabled"]),
"2",
withLiterals("0x2000", ["Holding awake"]),
),
).toMatchObject({ status: "changed" });
});
it("matches the same decoded literal after the function is relocated", () => {
expect(
stringsDimension(
"3",
withLiterals("0x1000", ["SleepDisabled"]),
"4",
withLiterals("0x2000", ["SleepDisabled"]),
),
).toMatchObject({ status: "unchanged" });
});
it("keeps strings and names unknown when only one dossier recorded Swift literals", () => {
expect(
stringsDimension(
"5",
withLiterals("0x1000", []),
"6",
dossier("return 0;", "0x2000"),
),
).toMatchObject({
status: "unknown",
limitations: [
"Swift String literal coverage differs or is partial; referenced strings and names match, and complete coverage does not establish a differing literal.",
],
});
});
it("reports a referenced name change even when Swift literal coverage differs", () => {
const named = functionDossierSchema.parse({
...dossier("return 0;", "0x2000"),
referenced_names: [
{ address: "0x3000", value: "helper", source_address: "0x2000" },
],
});
expect(
stringsDimension("7", withLiterals("0x1000", []), "8", named),
).toMatchObject({ status: "changed" });
});
});
describe("function comparison partial Swift literal coverage", () => {
it("does not equate a partial literal record with a complete one", () => {
const partial = functionDossierSchema.parse({
...withLiterals("0x2000", ["SleepDisabled"]),
swift_string_literal_omissions: [
{
source_address: "0x2000",
representation: "large",
address: "0x3000",
byte_length: 64,
reason: "capacity",
registers: ["x0", "x1"],
words: ["0xd000000000000040", "0x8000000000002fe0"],
},
],
});
expect(
stringsDimension(
"9",
withLiterals("0x1000", ["SleepDisabled"]),
"a",
partial,
),
).toMatchObject({ status: "unknown" });
});
it("reports a literal that a complete dossier shows absent at a site the partial one decoded", () => {
const partial = functionDossierSchema.parse({
...withLiterals("0x2000", ["SleepDisabled"]),
swift_string_literal_omissions: [
{
source_address: "0x2004",
representation: "small",
address: null,
byte_length: 4,
reason: "capacity",
registers: ["x0", "x1"],
words: ["0x732f4220", "0xe400000000000000"],
},
],
});
expect(
stringsDimension("d", withLiterals("0x1000", []), "e", partial),
).toMatchObject({ status: "changed" });
});
it("does not interpret an unexamined path as a missing literal at a retained site", () => {
const partial = functionDossierSchema.parse({
...withLiterals("0x2000", ["A"]),
swift_string_literal_omissions: [
{
source_address: "0x2004",
representation: "small",
address: null,
byte_length: 0,
reason: "capacity",
registers: ["x0", "x1"],
words: ["0x0", "0xe000000000000000"],
},
],
});
const complete = withLiterals("0x1000", ["A", "B"]);
expect(stringsDimension("1", complete, "2", partial)).toMatchObject({
status: "unknown",
});
expect(stringsDimension("3", partial, "4", complete)).toMatchObject({
status: "unknown",
});
expect(
stringsDimension("5", withLiterals("0x1000", []), "6", partial),
).toMatchObject({ status: "changed" });
});
it("treats an omission record without decoded literals as partial coverage", () => {
const omissionOnly = functionDossierSchema.parse({
...dossier("return 0;", "0x2000"),
swift_string_literal_omissions: [
{
source_address: "0x2004",
representation: "small",
address: null,
byte_length: 4,
reason: "capacity",
registers: ["x0", "x1"],
words: ["0x732f4220", "0xe400000000000000"],
},
],
});
expect(
stringsDimension("f", dossier("return 0;", "0x1000"), "0", omissionOnly),
).toMatchObject({ status: "unknown" });
});
it("keeps different retained representations unknown when both dossiers are partial", () => {
const omission = {
source_address: "0x1004",
representation: "small",
address: null,
byte_length: 4,
reason: "capacity",
registers: ["x0", "x1"],
words: ["0x732f4220", "0xe400000000000000"],
} as const;
const small = functionDossierSchema.parse({
...withLiterals("0x1000", ["SleepDisabled"]),
swift_string_literal_omissions: [omission],
});
const large = functionDossierSchema.parse({
...dossier("return 0;", "0x2000"),
swift_string_literals: [
{
source_address: "0x2000",
value: "SleepDisabled",
representation: "large",
address: "0x3000",
registers: ["x0", "x1"],
words: ["0xd00000000000000d", "0x8000000000002fe0"],
},
],
swift_string_literal_omissions: [
{ ...omission, source_address: "0x2004" },
],
});
expect(stringsDimension("1", small, "2", large)).toMatchObject({
status: "unknown",
});
});
it("keeps different retained literals unknown when both dossiers are partial", () => {
const partial = (base: "0x1000" | "0x2000", value: string) =>
functionDossierSchema.parse({
...withLiterals(base, [value]),
swift_string_literal_omissions: [
{
source_address: base === "0x1000" ? "0x1004" : "0x2004",
representation: "large",
address: "0x3000",
byte_length: 64,
reason: "capacity",
registers: ["x0", "x1"],
words: ["0xd000000000000040", "0x8000000000002fe0"],
},
],
});
expect(
stringsDimension(
"b",
partial("0x1000", "SleepDisabled"),
"c",
partial("0x2000", "Holding awake"),
),
).toMatchObject({ status: "unknown" });
});
});
const expectInlineComparisonEvidence = (
result: ReturnType<typeof compareFunctions>,
): void => {
+3 -39
View File
@@ -11,7 +11,6 @@ import {
referenceProjection,
sorted,
stringAndNameProjection,
swiftLiteralSitesConflict,
} from "./functionComparisonNormalization.js";
import type {
DimensionName,
@@ -140,12 +139,8 @@ const compareStringsAndNames = (
right: FunctionSnapshot,
context: ComparisonContext,
): FunctionDimension => {
const complete =
swiftLiteralCoverage(left) === swiftLiteralCoverage(right) &&
swiftLiteralCoverage(left) !== "partial";
// Facts both dossiers record decide a change before Swift literal coverage can.
const leftValues = stringAndNameProjection(left, complete);
const rightValues = stringAndNameProjection(right, complete);
const leftValues = stringAndNameProjection(left);
const rightValues = stringAndNameProjection(right);
if (leftValues === null || rightValues === null)
return unresolvedDimension({
dimension: "strings_names",
@@ -156,40 +151,9 @@ const compareStringsAndNames = (
"Reference source locations could not be normalized to function offsets.",
],
});
const compared = compareValues(
"strings_names",
leftValues,
rightValues,
context,
);
if (complete || compared.status === "changed") return compared;
// A retained literal differs only when the opposite dossier establishes its absence.
if (swiftLiteralSitesConflict(left, right)) {
const leftAll = stringAndNameProjection(left);
const rightAll = stringAndNameProjection(right);
if (leftAll !== null && rightAll !== null)
return compareValues("strings_names", leftAll, rightAll, context);
}
return unresolvedDimension({
dimension: "strings_names",
links: context.links,
leftCount: null,
rightCount: null,
limitations: [
"Swift String literal coverage differs or is partial; referenced strings and names match, and complete coverage does not establish a differing literal.",
],
});
return compareValues("strings_names", leftValues, rightValues, context);
};
const swiftLiteralCoverage = (
snapshot: FunctionSnapshot,
): "absent" | "complete" | "partial" =>
(snapshot.dossier.swift_string_literal_omissions ?? []).length > 0
? "partial"
: snapshot.dossier.swift_string_literals === undefined
? "absent"
: "complete";
const compareReferences = (
left: FunctionSnapshot,
right: FunctionSnapshot,
@@ -142,7 +142,6 @@ export const commentProjection = (
export const stringAndNameProjection = (
snapshot: FunctionSnapshot,
includeSwiftLiterals = true,
): readonly unknown[] | null => {
const values = [
...snapshot.dossier.referenced_strings.map(
@@ -165,90 +164,12 @@ export const stringAndNameProjection = (
value,
}),
),
...(includeSwiftLiterals
? (snapshot.dossier.swift_string_literals ?? [])
: []
).map(
({
source_address: sourceAddress,
value,
representation,
registers,
}) => ({
kind: "swift_string",
source_offset: relativeAddress(
sourceAddress,
snapshot.dossier.procedure.address,
),
value,
representation,
registers,
}),
),
];
return values.some(({ source_offset: offset }) => offset === null)
? null
: sorted(values);
};
/**
* A retained literal proves presence, not that every path through its site was examined.
* A differing retained fact proves a change only when the opposite dossier is complete
* and therefore establishes its absence.
*/
export const swiftLiteralSitesConflict = (
left: FunctionSnapshot,
right: FunctionSnapshot,
): boolean => {
const leftSites = swiftLiteralSites(left);
const rightSites = swiftLiteralSites(right);
for (const site of new Set([
...leftSites.values.keys(),
...rightSites.values.keys(),
])) {
const leftValues =
leftSites.values.get(site) ?? (leftSites.complete ? [] : undefined);
const rightValues =
rightSites.values.get(site) ?? (rightSites.complete ? [] : undefined);
if (leftValues === undefined || rightValues === undefined) continue;
const leftFacts = new Set(leftValues);
const rightFacts = new Set(rightValues);
if (
(rightSites.complete &&
leftValues.some((value) => !rightFacts.has(value))) ||
(leftSites.complete && rightValues.some((value) => !leftFacts.has(value)))
)
return true;
}
return false;
};
const swiftLiteralSites = (snapshot: FunctionSnapshot) => {
const base = snapshot.dossier.procedure.address;
const omissions = snapshot.dossier.swift_string_literal_omissions ?? [];
const site = (sourceAddress: string, registers: readonly string[]) => {
const offset = relativeAddress(sourceAddress, base);
return offset === null ? null : canonicalJson([offset, registers]);
};
// Keep the same literal facts the full strings_names projection compares.
const values = new Map<string, string[]>();
for (const literal of snapshot.dossier.swift_string_literals ?? []) {
const key = site(literal.source_address, literal.registers);
if (key === null) continue;
values.set(
key,
[
...(values.get(key) ?? []),
canonicalJson([literal.value, literal.representation]),
].sort(compareUnicodeCodePoints),
);
}
const complete =
snapshot.dossier.swift_string_literals !== undefined &&
omissions.length === 0;
return { values, complete };
};
export const sorted = (values: readonly unknown[]): readonly unknown[] =>
values
.map((value) => ({
-76
View File
@@ -290,70 +290,6 @@ export const unresolvedCallSchema = z.strictObject({
reason: z.string(),
});
const swiftStringLiteralFields = {
source_address: z
.string()
.describe(
"Instruction that consumes both words: a call, tail call, return, or paired store.",
),
value: z
.string()
.describe("Decoded UTF-8 text, including the canonical empty string."),
registers: z.tuple([z.string(), z.string()]),
words: z
.tuple([z.string(), z.string()])
.describe("Observed constant register values, in register order."),
};
/** A Swift String literal decoded from the two words of its ABI value, not a data reference. */
export const swiftStringLiteralSchema = z.discriminatedUnion("representation", [
z.strictObject({
...swiftStringLiteralFields,
representation: z
.literal("small")
.describe("The UTF-8 bytes are inside the two words."),
address: z.null().describe("A small literal has no storage of its own."),
}),
z.strictObject({
...swiftStringLiteralFields,
representation: z
.literal("large")
.describe(
"An immortal literal whose words hold its count and a pointer to its bytes.",
),
address: z.string().describe("Address of a large literal's bytes."),
}),
]);
const swiftStringLiteralOmissionFields = {
source_address: swiftStringLiteralFields.source_address,
byte_length: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
reason: z
.literal("capacity")
.describe(
"The record did not fit the provider's memory budget for this dossier; decoding stopped here, so later use sites were not examined.",
),
registers: swiftStringLiteralFields.registers,
words: swiftStringLiteralFields.words,
};
/** A Swift literal whose layout matched but whose record did not fit; decoding stopped there. */
export const swiftStringLiteralOmissionSchema = z.discriminatedUnion(
"representation",
[
z.strictObject({
...swiftStringLiteralOmissionFields,
representation: z.literal("small"),
address: z.null(),
}),
z.strictObject({
...swiftStringLiteralOmissionFields,
representation: z.literal("large"),
address: z.string().describe("Address of the literal's bytes."),
}),
],
);
/** Observed endpoints with complete or partial reference classification evidence. */
export const referenceEdgeSchema = z
.object({
@@ -405,18 +341,6 @@ export const functionDossierSchema = z
})
.strict(),
),
swift_string_literals: z
.array(swiftStringLiteralSchema)
.exactOptional()
.describe(
"Swift String literals decoded from constant register values. Absent when the provider does not decode them for this target.",
),
swift_string_literal_omissions: z
.array(swiftStringLiteralOmissionSchema)
.exactOptional()
.describe(
"The literal at which decoding stopped for capacity. Present exactly when swift_string_literals is present; empty when decoding examined every use site.",
),
basic_blocks: z.array(
z
.object({
@@ -1,351 +0,0 @@
import { describe, expect, it } from "vitest";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
import { onlyCallable } from "./javascriptSemanticAnalysis.fixture.js";
const returnedValue = (getter: string, write = "box.v.x = 2;") =>
onlyCallable(
analyzeJavaScriptSemantics(`export function result() {
const shared = { x: 1, y: 1 };
const box = { s: shared, get v() { ${getter} } };
${write}
return shared.x;
}`),
"result",
).returnSites[0]?.value;
describe("projected private getter receivers", () => {
it.each([
[
"destructured self field",
"shared",
"peer = this;",
"const { peer: self } = this; return self.#s;",
true,
"unknown",
],
[
"unrelated private child",
"{ peer: shared }",
"peer = this;",
"const { peer: self } = this; return self.#s;",
true,
"literal",
],
[
"read without write",
"shared",
"peer = this;",
"const { peer: self } = this; return self.#s;",
false,
"literal",
],
[
"two self fields",
"shared",
"peer = this; next = this.peer;",
"const { next: self } = this; return self.#s;",
true,
"unknown",
],
[
"nested receiver projection",
"shared",
"peer = this;",
"const { peer: { peer: self } } = this; return self.#s;",
true,
"unknown",
],
[
"member receiver alias",
"shared",
"peer = this;",
"const self = this.peer.peer; return self.#s;",
true,
"unknown",
],
[
"projected result suffix",
"{ child: shared }",
"peer = this;",
"const { peer: self } = this; return self.#s.child;",
true,
"unknown",
],
[
"external receiver",
"shared",
"peer = external;",
"const { peer: self } = this; return self.#s;",
true,
"literal",
],
[
"field receiver cycle",
"shared",
"peer = this.other; other = this.peer;",
"const { peer: self } = this; return self.#s;",
true,
"literal",
],
] as const)(
"keeps receiver and result paths separate for %s",
(_name, value, fields, getter, write, status) => {
const ir = analyzeJavaScriptSemantics(`export function result() {
const shared = { x: 1 };
class Box {
#s = ${value}; ${fields}
get v() { ${getter} }
}
const box = new Box();
${write ? "box.v.x = 2;" : "const saved = box.v;"}
return shared.x;
}`);
expect(onlyCallable(ir, "result").returnSites[0]?.value).toMatchObject(
status === "literal" ? { status, value: 1 } : { status },
);
},
);
});
describe("private receiver field ownership", () => {
it("bounds growing receiver projections under cyclic class inheritance", () => {
const ir = analyzeJavaScriptSemantics(`export function result() {
const shared = { x: 1 };
class A extends B {
#s = shared;
peer = this.other.other;
get v() { const { peer: self } = this; return self.#s; }
}
class B extends A { other = this.peer.peer; }
new A().v.x = 2;
return shared.x;
}`);
expect(onlyCallable(ir, "result").returnSites[0]?.value).toMatchObject({
status: "literal",
value: 1,
});
}, 1000);
it.each([
[
"own static receiver",
"class Box { static #s=shared; static peer=this; static get v(){const {peer:self}=this;return self.#s;} } Box.v.x=2;",
"unknown",
],
[
"inherited instance receiver",
"class Base {peer=this;} class Box extends Base {#s=shared;get v(){const {peer:self}=this;return self.#s;}} new Box().v.x=2;",
"unknown",
],
[
"inherited static field retains its declaring class",
"class Base {static peer=this;} class Box extends Base {static #s=shared;static get v(){const {peer:self}=this;return self.#s;}} Box.v.x=2;",
"literal",
],
[
"inherited static getter's own private field",
"class Base {static #s=shared;static peer=this;static get v(){const {peer:self}=this;return self.#s;}} class Box extends Base {} Box.v.x=2;",
"unknown",
],
[
"a field that is not initialized yet",
"class Box {#s=shared;peer=this.later;later=this;get v(){const {peer:self}=this;return self.#s;}} new Box().v.x=2;",
"literal",
],
[
"remaining getter projection after a captured field",
"class Box {#s=shared;peer=this;next=this.peer;later=this;get v(){const {next:{later:self}}=this;return self.#s;}} new Box().v.x=2;",
"unknown",
],
[
"a replaced self field",
"class Box {#s=shared;peer=this;peer=external;get v(){const {peer:self}=this;return self.#s;}} new Box().v.x=2;",
"literal",
],
] as const)("respects %s", (_name, body, status) => {
const ir = analyzeJavaScriptSemantics(`export function result() {
const shared = { x: 1 };
${body}
return shared.x;
}`);
expect(onlyCallable(ir, "result").returnSites[0]?.value).toMatchObject(
status === "literal" ? { status, value: 1 } : { status },
);
});
});
describe("getter-local receiver aliases (#2653)", () => {
it.each([
["initialized alias", "const self = this; return self.s;"],
["assigned alias", "let self; self = this; return self.s;"],
[
"chained assignment",
"let self; let alias; self = alias = this; return self.s;",
],
["destructured receiver", "const { s: value } = this; return value;"],
["alias chain", "const self = this; const saved = self; return saved.s;"],
[
"capture before reassignment",
"let self = this; const saved = self; self = { s: { x: 1 } }; return saved.s;",
],
[
"conditional receiver alias",
"const self = flag ? this : { s: { x: 1 } }; return self.s;",
],
])("invalidates shared values through %s", (_name, getter) => {
expect(returnedValue(getter)?.status).toBe("unknown");
});
it.each([
["no write", "const self = this; return self.s;", "const saved = box.v;"],
[
"an unrelated property write",
"const self = this; return self.s;",
"box.v.y = 2;",
],
[
"a cycle that does not grow the selected path",
"let self = this; while (flag) self = self; return self.s;",
"box.v.y = 2;",
],
[
"an alias replaced before return",
"let self = this; self = { s: { x: 1 } }; return self.s;",
"box.v.x = 2;",
],
[
"a shadowing block binding",
"const self = this; { const self = { s: { x: 1 } }; return self.s; }",
"box.v.x = 2;",
],
[
"a nested function's receiver",
"function other() { const self = this; return self.s; } return other();",
"box.v.x = 2;",
],
[
"an alias assigned in a different receiver scope",
"let self; function assign() { self = this; } assign(); return self.s;",
"box.v.x = 2;",
],
[
"an excluded destructuring default",
"const { value = shared } = { value: { x: 1 } }; return value;",
"box.v.x = 2;",
],
])("preserves known values with %s", (_name, getter, write) => {
expect(returnedValue(getter, write)).toMatchObject({
status: "literal",
value: 1,
});
});
});
describe("getter receiver alias boundaries", () => {
it("does not reinterpret an outer this binding as the getter receiver", () => {
const ir = analyzeJavaScriptSemantics(`export function result() {
const shared = { x: 1 };
const outside = this;
const box = { s: shared, get v() { return outside.s; } };
box.v.x = 2;
return shared.x;
}`);
expect(onlyCallable(ir, "result").returnSites[0]?.value).toMatchObject({
status: "literal",
value: 1,
});
});
it("retains a private field's lexical owner through a receiver alias", () => {
const ir = analyzeJavaScriptSemantics(`export function result() {
const shared = { x: 1 };
class Box {
#s = shared;
get v() { const self = this; return self.#s; }
}
new Box().v.x = 2;
return shared.x;
}`);
expect(onlyCallable(ir, "result").returnSites[0]?.value.status).toBe(
"unknown",
);
});
it("does not append a receiver projection to a private field's result path", () => {
const ir = analyzeJavaScriptSemantics(`export function result() {
const shared = { x: 1 };
class Box {
#s = { peer: shared };
peer = this;
get v() { const { peer: self } = this; return self.#s; }
}
new Box().v.x = 2;
return shared.x;
}`);
expect(onlyCallable(ir, "result").returnSites[0]?.value).toMatchObject({
status: "literal",
value: 1,
});
});
it("resolves a long local alias chain without recursive expansion", () => {
const aliases = ["const a0 = this;"];
for (let index = 1; index <= 2000; index++)
aliases.push(`const a${String(index)} = a${String(index - 1)};`);
expect(returnedValue(`${aliases.join("\n")} return a2000.s;`)?.status).toBe(
"unknown",
);
});
it("bounds a growing alias cycle without losing the original receiver", () => {
expect(
returnedValue(
"let self = this; while (flag) self = self.s; return self.s;",
)?.status,
).toBe("unknown");
});
it.each([true, false])(
"retains deeper shared references through an alias cycle (write=%s)",
(write) => {
const ir = analyzeJavaScriptSemantics(`export function result() {
const shared = { x: 1 };
const box = {
start: { child: { child: shared } },
get v() {
let self = this.start;
while (flag) self = self.child;
return self;
}
};
${write ? "box.v.x = 2;" : "const saved = box.v;"}
return shared.x;
}`);
expect(onlyCallable(ir, "result").returnSites[0]?.value).toMatchObject(
write ? { status: "unknown" } : { status: "literal", value: 1 },
);
},
);
it("does not widen a growing cycle to unrelated receiver properties", () => {
const ir = analyzeJavaScriptSemantics(`export function result() {
const shared = { x: 1 };
const unrelated = { x: 1 };
const box = {
start: { child: { child: shared } },
unrelated,
get v() {
let self = this.start;
while (flag) self = self.child;
return self;
}
};
box.v.x = 2;
return unrelated.x;
}`);
expect(onlyCallable(ir, "result").returnSites[0]?.value).toMatchObject({
status: "literal",
value: 1,
});
});
});
@@ -33,38 +33,6 @@ const getterReceivers = [
"nested owner",
"const outer = { box: { x: 1, get self() { return this; } } }; outer.box.self.x = 2; return outer.box.x;",
],
[
"array element",
"const arr = [{ x: 1, get self() { return this; } }]; arr[0].self.x = 2; return arr[0].x;",
],
[
"array element after a hole",
"const arr = [, { x: 1, get self() { return this; } }]; arr[1].self.x = 2; return arr[1].x;",
],
[
"mixed object and array owners",
"const outer = { rows: [{ box: { x: 1, get self() { return this; } } }] }; outer.rows[0].box.self.x = 2; return outer.rows[0].box.x;",
],
[
"assigned owner",
"let box; box = { x: 1, get self() { return this; } }; box.self.x = 2; return box.x;",
],
[
"saved array receiver alias",
"const arr = [{ x: 1, get self() { return this; } }]; const saved = arr[0].self; saved.x = 2; return arr[0].x;",
],
[
"old receiver captured inside its replacement initializer",
"const old = { x: 1 }; let box = old; box = { old: box, get self() { return this; } }; box.self.old.x = 2; return old.x;",
],
[
"array receiver after an unknown spread",
"const arr = [...items, { x: 1, get self() { return this; } }]; arr[index].self.x = 2; return arr[index].x;",
],
[
"saved receiver before its array alias is reassigned",
"const original = [{ x: 1, get self() { return this; } }]; let arr = original; const saved = arr[0].self; arr = [{ x: 1 }]; saved.x = 2; return original[0].x;",
],
[
"inherited receiver field",
"const shared = { x: 1 }; class A { get v() { return this.s; } } class B extends A { s = shared; } new B().v.x = 2; return shared.x;",
@@ -107,7 +75,7 @@ const getterReceivers = [
],
] as const;
describe("getter receiver mutation aliases (#1665, #2654)", () => {
describe("getter receiver mutation aliases (#1665)", () => {
it.each(getterReceivers)(
"does not report a stale literal through %s",
(_shape, body) => {
@@ -135,42 +103,6 @@ describe("getter receiver mutation aliases (#1665, #2654)", () => {
"unrelated sibling",
"const box = { x: 1, y: 1, get self() { return this; } }; box.self.y = 2; return box.x;",
],
[
"array receiver read without write",
"const arr = [{ x: 1, get self() { return this; } }]; const saved = arr[0].self; return arr[0].x;",
],
[
"assigned receiver read without write",
"let box; box = { x: 1, get self() { return this; } }; const saved = box.self; return box.x;",
],
[
"unrelated array receiver property",
"const arr = [{ x: 1, y: 1, get self() { return this; } }]; arr[0].self.y = 2; return arr[0].x;",
],
[
"unrelated array element",
"const arr = [{ x: 1, get self() { return this; } }, { x: 1 }]; arr[0].self.x = 2; return arr[1].x;",
],
[
"array replacement after a receiver was captured",
"const original = [{ x: 1, get self() { return this; } }]; const replacement = [{ x: 1 }]; let arr = original; const saved = arr[0].self; arr = replacement; saved.x = 2; return replacement[0].x;",
],
[
"known array prefix before a spread receiver",
"const arr = [{ x: 1 }, ...items, { x: 1, get self() { return this; } }]; arr[1].self.x = 2; return arr[0].x;",
],
[
"outer binding shadowed by an assigned receiver",
"let box; box = { x: 1 }; { let box; box = { x: 1, get self() { return this; } }; box.self.x = 2; } return box.x;",
],
[
"old object before an unconditional receiver assignment",
"const old = { x: 1 }; let box = old; box = { x: 1, get self() { return this; } }; box.self.x = 2; return old.x;",
],
[
"unmodified old receiver captured inside its replacement initializer",
"const old = { x: 1 }; let box = old; box = { x: 1, old: box, get self() { return this; } }; box.self.x = 2; return old.x;",
],
[
"replace getter",
"const shared = { x: 1 }; const box = { s: shared, get v() { return this.s; } }; box.v = {}; return shared.x;",

Some files were not shown because too many files have changed in this diff Show More