Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f337cb398 |
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
@@ -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",
|
||||
|
||||
@@ -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).
|
||||
@@ -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",
|
||||
|
||||
@@ -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>;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,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.",
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
@@ -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" }
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user