mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 13:56:47 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
733c160d4f |
No files matched your search
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "rea",
|
||||
"interface": {
|
||||
"displayName": "REA"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "rea",
|
||||
"source": {
|
||||
"source": "npm",
|
||||
"package": "rea-agents",
|
||||
"version": "6.8.0"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Developer Tools"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
---
|
||||
name: rea-analysis-optimization
|
||||
description: Optimize REA providers and shared analysis workflows for agent context, latency, CPU, and memory while preserving correctness and Evidence. Use for analysis bottlenecks and cache or query design; CI/test-suite optimization belongs to rea-ci-tests-audit.
|
||||
---
|
||||
|
||||
# REA analysis optimization
|
||||
|
||||
Find a concrete source of unnecessary work, change it at its owner, and verify
|
||||
equivalent completed investigations. A useful outcome can also be a measured
|
||||
decision to leave an implementation alone.
|
||||
|
||||
Follow the checkout's [performance priorities](../../../AGENTS.md#performance),
|
||||
[tool design](../../../docs/tool-design.md), and
|
||||
[testing guide](../../../docs/testing.md). This skill supplies an optimization
|
||||
workflow, not additional permission to execute targets, publish, or merge.
|
||||
Use the current contracts, provider prerequisites, and user authorization.
|
||||
|
||||
Reserve performance measurements for changes that target performance or have a
|
||||
concrete regression concern. Routine cleanup uses correctness checks and does
|
||||
not require this optimization workflow or synthetic benchmarks merely because
|
||||
helper calls, imports, or representations change.
|
||||
|
||||
Read [the Kuna case study](references/kuna-case-study.md) when selecting a
|
||||
reuse strategy or interpreting a benchmark. Its pinned observations illustrate
|
||||
tradeoffs; they are not current-provider guarantees or universal speedups.
|
||||
|
||||
## Pin the question and the correctness oracle
|
||||
|
||||
Describe the analyst's completed task, including the facts and relationships
|
||||
needed to answer it, requested detail, source access, and meaningful unknowns.
|
||||
Choose a representative real artifact and a small or empty case. Record the
|
||||
baseline revision, target digest, engine identity, options, host, and entrypoint.
|
||||
Use a separate checkout for changes without disturbing other work.
|
||||
|
||||
Establish an oracle independent of the changed implementation: source-built
|
||||
fixture expectations, independently mapped file bytes, or a native producer
|
||||
paired with checks of its interpretation. Comparing two wrappers around the
|
||||
same faulty decoder does not establish correctness. Preserve address widths,
|
||||
same-address alternatives, unresolved endpoints, coverage, and source linkage.
|
||||
An empty result proves absence only within the actually examined scope.
|
||||
|
||||
## Locate the earliest unnecessary work
|
||||
|
||||
Trace one public CLI or MCP task through shared application code to its producer.
|
||||
Measure relevant phases: admission and identity checks, native analysis,
|
||||
decoding, lookup, projection, serialization, Evidence retention, and cleanup.
|
||||
Include catalog discovery and extra calls when they affect the completed task.
|
||||
|
||||
Use the evidence to choose the smallest structural correction:
|
||||
|
||||
| Observed cost | Candidate correction | Condition to establish |
|
||||
| --------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| Repeated scans of validated facts | Reuse existing order or lookup structure | The owner establishes ordering and identity; every matching alternative survives |
|
||||
| Repeated equivalent engine work | Share a validated result or in-flight producer | Inputs, freshness, lifetime, and per-caller outcomes remain explicit |
|
||||
| Full reports for selected facts | Push selection or projection to the earliest capable owner | Required facts and evidence remain reachable; examined coverage is reported |
|
||||
| Large temporary transport buffers | Decode incrementally or use bounded source reads | Validate the whole admitted document; retained facts and copies still consume memory |
|
||||
| Many repeated homogeneous reads | Share admission, decoding, or batch work | Preserve item identity, order where required, and individual failures |
|
||||
|
||||
Do not introduce an index, generic cache, batch endpoint, or orchestration layer
|
||||
without identifying the work it removes and its maintenance/resource cost.
|
||||
Projection after materializing a full report may reduce output while leaving
|
||||
analysis time and peak memory unchanged. Fix missing or incorrect facts before
|
||||
benchmarking an alternative composition against a better-covered workflow.
|
||||
|
||||
## Keep one interpretation and a defined reuse lifetime
|
||||
|
||||
Let the producer boundary validate and normalize facts once. Consumers query
|
||||
that representation through the shared CLI/MCP implementation; adapters should
|
||||
not rebuild relationships or independently reinterpret raw reports. Keep lookup
|
||||
identity separate from display formatting. Reuse contracts when their intent
|
||||
matches, honoring requested detail and complete Evidence access.
|
||||
|
||||
For every cache or shared producer, state its key, owner, invalidation, retained
|
||||
size, and lifetime. Account for result-affecting target bytes, engine contents,
|
||||
analysis options, architecture/load mapping, and parser/schema versions where
|
||||
they affect reuse. Validate live state before calling a cached observation
|
||||
current. Cache a refusal only when it is deterministic for that validated key;
|
||||
malformed, partial, transient, or cancelled attempts need explicit retry policy.
|
||||
|
||||
One cancelled waiter must not invalidate a successful peer. Preserve producer
|
||||
failure context and each caller's cancellation reason. Retain ownership and
|
||||
capacity charges until shared work and cleanup settle; preserve completed
|
||||
observations when cleanup fails. Exercise invalidation, concurrent consumers,
|
||||
last-waiter cancellation, close, and cleanup retry when those paths change.
|
||||
|
||||
Bound the process family and retained state, not just a parent heap or worker
|
||||
count. Account for decoding and serialization expansion before allocation.
|
||||
Choose CPU, memory, concurrency, and disk limits from the host and workload;
|
||||
stop before OOM and verify owned cleanup. A guard-stopped task is incomplete.
|
||||
Do not replace a transport buffer cap with an unbounded retained inventory or
|
||||
hide partial results behind a success status.
|
||||
|
||||
## Compare completed tasks and limit the claim
|
||||
|
||||
Run the affected real public workflow before and after with equivalent inputs,
|
||||
results, coverage, and Evidence semantics. Include small inputs so setup costs
|
||||
and regressions are visible. Distinguish fresh sessions, warm runtime caches,
|
||||
and OS file caches. Alternate or repeat runs when variance affects the decision.
|
||||
Choose meaningful boundary regressions for cases the actual engine cannot
|
||||
reliably reproduce; a fake engine does not qualify a real provider.
|
||||
|
||||
Report task outcome, relevant phase timings, total elapsed and CPU time, peak
|
||||
process-family memory and its accounting method, resource stops, and cleanup.
|
||||
Check the payload exit status as well as the profiler's. Keep failed attempts
|
||||
and identify unmeasured costs; a passing retry does not explain a failure.
|
||||
|
||||
For token claims, count schemas/discovery, arguments, replies, retries, and
|
||||
continuations needed to finish the task. Label tokenizer estimates separately
|
||||
from actual model usage and record the client/catalog behavior. Smaller JSON
|
||||
alone establishes neither fewer model tokens nor better investigation quality.
|
||||
Do not claim a complex-software advantage without completed representative
|
||||
investigations and comparable correctness/coverage.
|
||||
|
||||
Finish with the bottleneck, owning change, preserved semantics, executed
|
||||
checks, before/after measurements, and remaining limits. Keep any integration
|
||||
record current with its tested revision and distinguish merged production work
|
||||
from experiment-only code. Stop when the demonstrated bottleneck is resolved
|
||||
or further benefit remains speculative.
|
||||
@@ -1,4 +0,0 @@
|
||||
interface:
|
||||
display_name: "REA analysis optimization"
|
||||
short_description: "Optimize analysis with preserved Evidence and measured costs"
|
||||
default_prompt: "Use $rea-analysis-optimization to find and verify a concrete analysis bottleneck while preserving correctness and Evidence."
|
||||
@@ -1,112 +0,0 @@
|
||||
# Kuna: reuse before adding machinery
|
||||
|
||||
Read this reference for concrete examples of the skill's decisions. The records
|
||||
below concern static Linux analysis with caller-supplied Kuna. They do not
|
||||
establish parity with another provider or a general advantage for Code Mode,
|
||||
atomic tools, or any particular orchestration design.
|
||||
|
||||
## Exact-address lookup: a measured, narrow improvement
|
||||
|
||||
[PR #4695](https://github.com/morluto/rea/pull/4695) reused the validated string
|
||||
inventory's numeric address order. Previously, each `list_strings(address)`
|
||||
filtered all cached facts. The owner now performs a binary lower-bound lookup
|
||||
and returns every encoding at that address: O(log n + matches), without a
|
||||
second retained lookup index.
|
||||
|
||||
The owning implementation is
|
||||
[`KunaStringInventory.ts`](../../../../src/kuna/KunaStringInventory.ts), consumed
|
||||
by [`KunaStringQueries.ts`](../../../../src/kuna/KunaStringQueries.ts). Inspect
|
||||
their current versions before adapting the example. Numeric u64 addresses,
|
||||
same-address encodings, duplicate rejection, source digests, caller selection,
|
||||
and the existing session lifetime were part of the contract. Full listings and
|
||||
literal/regex searches kept their semantics. An attempted removal of the
|
||||
duplicate-detection set was dropped: no stable memory benefit was established,
|
||||
and it delayed malformed-input rejection.
|
||||
|
||||
### Recorded workload and results
|
||||
|
||||
The benchmark used unmodified Kuna 1.833 and LLVM 16 on Linux x86-64, with
|
||||
602,544 string facts. Baseline was `be0b2d138`; the final feature was
|
||||
`5f564962da0e1b17f00a68a74a312539f0209a01`, subsequently integrated in
|
||||
`c6b62754b095a134d6d67877508fa34311a2eda2`. These are historical observations,
|
||||
not a benchmark of every subsequent main revision.
|
||||
|
||||
The same production stdio MCP task discovered the catalog, opened the binary,
|
||||
made one cold selected read and 28 warm exact reads, checked retained Evidence,
|
||||
and closed the session. Five selected addresses and two absent selectors were
|
||||
read four times. A native filtered scan selected the strings; an independent
|
||||
ELF load-header mapping checked the source bytes and terminators. Returned
|
||||
facts, source linkage, target digests, and cleanup were checked.
|
||||
|
||||
Three alternating fresh sessions per implementation used one CPU, a 4 GiB
|
||||
process-family hard limit, a 3 GiB early stop, and no swap. OS file caches were
|
||||
warm. Whole-harness costs include the independent native oracle, hashing,
|
||||
catalog/client work, and correctness checks.
|
||||
|
||||
| Median | Before | After |
|
||||
| ---------------------------------------- | ---------: | ---------: |
|
||||
| 28 warm exact MCP reads | 495.2 ms | 90.8 ms |
|
||||
| Cold selected MCP read | 11.30 s | 11.01 s |
|
||||
| Whole harness elapsed | 28.24 s | 27.43 s |
|
||||
| Whole process-family CPU | 27.71 s | 26.89 s |
|
||||
| Kernel family peak, including file cache | 2237.6 MiB | 2240.6 MiB |
|
||||
|
||||
Warm totals ranged from 491.7–544.3 ms before to 84.8–99.5 ms after: about
|
||||
5.5 times faster at this phase. Cold/whole-task timing and memory overlapped;
|
||||
no stable improvement in those costs was established. One small SQLite pair
|
||||
(7,985 facts) measured 20 warm reads at 88.6 → 80.5 ms and the whole harness at
|
||||
13.68 → 13.94 s. The large-inventory lookup benefit does not generalize to a
|
||||
whole-task speedup on small inputs.
|
||||
|
||||
Catalog hashes, call counts, and normalized facts were unchanged. A
|
||||
`cl100k_base` estimate over one pass of raw catalog JSON, arguments, and replies
|
||||
was about 846,000 tokens in both lanes, with about 802,562 from the catalog.
|
||||
This was transport serialization, not rendered model context or actual usage;
|
||||
client discovery, prompts, replay, and protocol framing were unmeasured.
|
||||
**No token reduction or improved agent investigation outcome was established.**
|
||||
|
||||
Pinned target SHA-256:
|
||||
`f62d254b7f2bf42df8c8b07d46ee3bb4c2cafeca436b2e6bc6ccbe4581f58f40`.
|
||||
Pinned engine SHA-256:
|
||||
`d858918b0766435cd96b999a77f2d5a31a0cc4f07256421c883b5b9dc4dd58e4`.
|
||||
All six normalized-result sequence digests were
|
||||
`4d2f90269da8568984090b20274163c4a7c2a0538aab63530bd2fe44840a0bba`.
|
||||
Selected byte agreement does not prove native discovery completeness; missing
|
||||
strings and relationships can remain unknown.
|
||||
|
||||
## Other transferable changes and their limits
|
||||
|
||||
- [Cached string inventory and search, #4095](https://github.com/morluto/rea/pull/4095):
|
||||
share validated facts across queries in a defined session. Audit key inputs
|
||||
and lifetime before extending reuse across targets or engine revisions.
|
||||
- [Shared single-function work, #4045](https://github.com/morluto/rea/pull/4045)
|
||||
and [repeat-request reuse, #4169](https://github.com/morluto/rea/pull/4169):
|
||||
avoid duplicate native work while retaining per-caller cancellation and
|
||||
cleanup ownership. A query count alone cannot reveal duplicate subprocesses.
|
||||
- [Streamed strings, #4318](https://github.com/morluto/rea/pull/4318)
|
||||
and [streamed call graph, #4329](https://github.com/morluto/rea/pull/4329):
|
||||
remove fixed aggregate transport-buffer failures through incremental parsing.
|
||||
Streaming transport does not bound every retained object or final reply.
|
||||
These links establish changes; no numerical speedup is asserted here.
|
||||
|
||||
Typed reference facts, native string-query projection, and shared native byte
|
||||
reads remained experimental at the 2026-10-17 checkpoint
|
||||
`65f2f9a22bc892f61d2b00f9a4611124f57d4bff` on
|
||||
`experiment/atomic-codemode-20261016`. Some experiments used a patched native
|
||||
engine. Check current integration and bring-your-own engine capabilities before
|
||||
proposing them; a working experiment is not a stock-provider guarantee.
|
||||
|
||||
## Applying the reasoning elsewhere
|
||||
|
||||
For another provider or format, inspect its actual representation and native
|
||||
capabilities before borrowing the implementation. Existing numeric ordering
|
||||
may support range reads; an on-disk native query may avoid materializing an
|
||||
inventory; an immutable parse may support several views. Measure the relevant
|
||||
alternative rather than requiring every provider to use Kuna's cached array.
|
||||
|
||||
For live observations, immutable artifact caching rules are insufficient:
|
||||
validate the process/session identity and freshness required by the caller.
|
||||
For compact replies, preserve access to source Evidence and report examined
|
||||
coverage; projection must not silently turn an unresolved edge into no edge.
|
||||
For batching, test mixed successes and failures and measure the entire task,
|
||||
including discovery, extra calls, native work, and serialization.
|
||||
@@ -68,10 +68,9 @@ closing-issue reporters. Grouping preserves all those credits in one line.
|
||||
When Highlights, detailed entries or migrations reference the same work,
|
||||
put its thanks in one of those entries, not all of them. Include issue links.
|
||||
Direct-commit bullets carry the commit link and known human credit.
|
||||
Exclude GitHub bots and known agent accounts. REA's observed Anthropic, Cursor,
|
||||
Codex, and Copilot co-author trailers resolve to `@claude`, `@cursoragent`,
|
||||
`@codex`, and `@Copilot` with GitHub type `User`; the credit policy excludes
|
||||
them while retaining their metadata.
|
||||
Exclude GitHub bots and known agent accounts. REA's observed Anthropic, Cursor
|
||||
and Codex co-author trailers resolve to `@claude`, `@cursoragent` and `@codex`
|
||||
with GitHub type `User`; the credit policy excludes them while retaining their metadata.
|
||||
At the REA maintainer's request, omit `@morluto` self-thanks while preserving
|
||||
authorship in the inventory. Other human contributors remain eligible.
|
||||
Use explicit GitHub links for cross-repository references to avoid ambiguous
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: rea-tool-design
|
||||
description: Audit, investigate, design, or change REA investigation tools, CLI/MCP contracts, provider capabilities, and Evidence semantics.
|
||||
description: Design or change REA investigation tools, CLI/MCP contracts, provider capabilities, and Evidence semantics.
|
||||
---
|
||||
|
||||
# REA tool design
|
||||
@@ -10,20 +10,6 @@ contract semantics, provider ownership, and discoverability. Inspect the current
|
||||
contract and nearest existing tool relevant to the analyst question; source,
|
||||
documentation, and live catalogs may describe different versions.
|
||||
|
||||
For provider-backed work, exercise the analyst's task through REA and compare
|
||||
relevant native provider and independent artifact observations where available.
|
||||
Check applicable partial results, recovery and session boundaries; report what
|
||||
was not exercised. For failure guidance, follow [design recovery around the
|
||||
failure](../../../docs/tool-design.md#design-recovery-around-the-failure).
|
||||
Use [workflow verification](../../../docs/testing.md#provider-backed-workflow-verification)
|
||||
to select evidence for the proposed contract instead of seeking provider API
|
||||
parity.
|
||||
|
||||
For an audit, follow [audit existing tools](../../../docs/tool-design.md#audit-existing-tools):
|
||||
scope representative tasks, exercise the live workflow, and distinguish
|
||||
reproduced bugs, supported limitations, design opportunities and unverified
|
||||
claims. Report working alternatives and evidence limits as well as findings.
|
||||
|
||||
For a design request, return the proposed boundary, input/result shape, Evidence
|
||||
semantics, nearest alternative, and verification approach. Implement when
|
||||
requested, preserving the user's scope and existing authorization.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
name: reverse-engineer-anything
|
||||
description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA, and build agent environments (AgentEnv bundles) of web applications from a live instance or a capture. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
|
||||
description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
|
||||
metadata:
|
||||
version: "59"
|
||||
version: "42"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
||||
Use REA when a claim depends on a shipped binary or package, decompilation,
|
||||
requested runtime evidence, or comparison with
|
||||
passive application runtime evidence, or comparison with
|
||||
behavior not established by available source. For ordinary analysis of a
|
||||
complete source repository, use normal repository tools and do not run REA
|
||||
readiness or provider commands.
|
||||
@@ -17,9 +17,6 @@ readiness or provider commands.
|
||||
|
||||
Installing this skill supplies instructions; it does not register the REA MCP
|
||||
server, install analysis engines, or add tools to an already-running agent.
|
||||
The exception is a Claude Code or Codex plugin install: the plugin bundles
|
||||
this skill and registers its own `rea` server, outside the state that
|
||||
`rea setup` and `rea doctor` manage.
|
||||
|
||||
If REA tools are available and their registration is not known to be stale,
|
||||
proceed directly to the target. Do not run diagnostics or setup before every
|
||||
@@ -33,30 +30,39 @@ unavailable, read [connection and recovery](references/connection-and-recovery.m
|
||||
Diagnose the observed blocker, continue useful supported work, and resume the
|
||||
original task after repair. Client approval, shell PATH, registration and engine
|
||||
availability have different remedies; avoid repeating setup for all of them.
|
||||
If the host times out without a REA result, do not repeat the unchanged call or
|
||||
assume work stopped. Read the recovery guide's deadline distinction before retrying.
|
||||
If the MCP connection drops, retry that call once only when it is safe to
|
||||
repeat or known not to have started. If it may already have changed the
|
||||
target, or it fails again, tell the user and ask them to restart the MCP
|
||||
client. If REA could not start its connection, retry once, then tell the user
|
||||
and ask them to restart the MCP client. Do not retry a startup failure that
|
||||
returns the same unchanged result. For plugin installs, follow
|
||||
[plugin recovery](references/connection-and-recovery.md#rea-installed-as-a-client-plugin).
|
||||
For setup-owned registrations, run `rea doctor` when REA could not start
|
||||
or tools stay missing after that restart. For `cleanup_incomplete`, a retryable
|
||||
capacity limit, or `configuration_invalid`, follow the recovery guide's ladder
|
||||
before asking for a restart. Read that guide before a second repair attempt.
|
||||
|
||||
Choose the installation owner before repairing registration. Plugin installs use
|
||||
[plugin recovery](references/connection-and-recovery.md#rea-installed-as-a-client-plugin);
|
||||
only setup-owned registrations use the guide's
|
||||
[registration repair plan](references/connection-and-recovery.md#repair-registration-when-needed).
|
||||
A plugin-only doctor result of `missing` does not call for `rea setup`.
|
||||
1. Diagnose without changing REA or client configuration. `npx` may download
|
||||
the package and write to npm's cache. For Codex, run
|
||||
`npx -y rea-agents@latest doctor --client codex --json`. Substitute the current
|
||||
supported client: `claude_code`, `claude_desktop`, `codex`, `cursor`,
|
||||
`gemini_cli`, `windsurf`, `devin`, `opencode`, `antigravity`, `copilot_cli`,
|
||||
`commandcode`, `vscode`, `grok_build`, `omp`, or `grok_bot`. `grok_bot` has no local
|
||||
file registration: doctor reports the Grok Bot chat step, and setup does not
|
||||
write `mcp.json`. If the client is unknown, use `doctor --json` and
|
||||
inspect its registration results before choosing a setup scope.
|
||||
2. Distinguish the reason. Missing, malformed, or stale registration needs a
|
||||
scoped configuration repair. An aligned registration with no tools in the
|
||||
active session needs a restart/reconnection; doctor cannot prove that the
|
||||
current agent has connected. A missing provider affects only tasks requiring
|
||||
that provider: static JavaScript inspection needs neither Hopper nor Ghidra,
|
||||
and Android inspection has separate bring-your-own JADX/Java prerequisites.
|
||||
3. For a configuration repair, prepare the read-only plan:
|
||||
`npx -y rea-agents@latest setup --client codex --dry-run --json`.
|
||||
Use the current client's ID, show its exact proposed paths, backups, and
|
||||
changes, and obtain approval before setup writes configuration or installs
|
||||
Hopper. Setup normally installs the matching bundled skill too; include that
|
||||
replacement in the reviewed plan. After approval, apply the same scope with
|
||||
`npx -y rea-agents@latest setup --client codex --yes`. Add `--install-hopper`
|
||||
only if that separate installation was needed and explicitly approved.
|
||||
4. Restart/reconnect the affected agent. Verify that REA tools actually appear
|
||||
in the session, then resume the original investigation. If they remain
|
||||
absent, inspect the client's MCP launch error rather than repeating setup.
|
||||
|
||||
REA setup never installs or upgrades Node.js, npm, Homebrew, Java, Ghidra, IDA,
|
||||
JADX, Kuna, Binwalk, or Unblob. Use existing prerequisites; do not install unrelated
|
||||
software to repair MCP registration. For an unsupported client, use manual stdio
|
||||
registration with an existing installation’s absolute runtime and entry script, or continue through the CLI.
|
||||
software to repair
|
||||
MCP registration. For an unsupported client, use manual stdio registration
|
||||
with a version-pinned `rea-agents` package or continue through the CLI.
|
||||
|
||||
A concrete CLI fallback for an operator-supplied JavaScript tree or ASAR is:
|
||||
|
||||
@@ -69,13 +75,11 @@ graph, limitations, and unknowns with the same care as an MCP result; the CLI
|
||||
returns the Evidence record directly. This fallback does not establish that
|
||||
MCP is configured. Native CLI tasks still require their selected engine.
|
||||
Prefer an existing `rea` CLI. When none is installed and REA package acquisition
|
||||
is within the authorized scope, use `npx -y rea-agents@latest` in its place from a
|
||||
trusted directory outside the target project, with absolute target paths. Managed
|
||||
setup binds the current installation directly; retain its runtime and package
|
||||
cache entry, or restore that installation before repairing its registration.
|
||||
For setup-owned registration, configuration repair starts with a read-only setup plan; show paths, backups and
|
||||
is within the authorized scope, use `npx -y rea-agents@latest` in its place.
|
||||
Configuration repair starts with a read-only setup plan; show paths, backups and
|
||||
skill replacement before applying authorized changes. Preserve existing approval
|
||||
and target-execution boundaries.
|
||||
and target-execution boundaries. REA setup never installs or upgrades unrelated
|
||||
software, including Node.js, npm, Homebrew, Java, Ghidra, IDA, JADX, Kuna, Binwalk or Unblob.
|
||||
|
||||
## Route the target first
|
||||
|
||||
@@ -85,22 +89,9 @@ explicit path or endpoint and do not need it.
|
||||
|
||||
- ASAR or extracted JavaScript/Electron tree:
|
||||
`analyze_javascript_application`.
|
||||
- Archive/package member inventory (ZIP/APK/IPA/MSIX/AppX, DMG, or a PyInstaller
|
||||
executable): `open_binary` with the supplied local path; use `inspect_artifact`
|
||||
when its graph and findings help answer the question. A PyInstaller CArchive
|
||||
is the application layer; the bootloader is only the native prefix. Start with
|
||||
`detail: "summary"` for large inventories, then use retained inventory views
|
||||
for selected member fields/pages. A complete small inventory can cost less
|
||||
than extra calls and schemas. Runtime options are metadata, not files.
|
||||
Compact delivery still inventories and hashes members. See the
|
||||
[Python packaged applications guide](https://github.com/morluto/rea/blob/main/docs/python-packaged-applications.md).
|
||||
- Bun `--compile` executable: `open_binary`, then `inspect_artifact`; the Bun
|
||||
module graph is the application layer. Extract stored modules and analyze the
|
||||
JavaScript. See [Bun standalone executables](https://github.com/morluto/rea/blob/main/docs/bun-standalone.md).
|
||||
- Apple app plist, Interface Builder, asset catalog, or keyed archive resources:
|
||||
`open_bundle_resources`, then the selected resource tool. This does not
|
||||
require `CFBundleExecutable` or a deep provider. Resource sessions have no
|
||||
executable digest or binary snapshot; use `open_binary` for native code.
|
||||
- Archive/package member inventory (ZIP/APK/IPA/MSIX/AppX or DMG):
|
||||
`open_binary` with the supplied local path; use `inspect_artifact` when its
|
||||
graph and findings help answer the question.
|
||||
- Android APK code, classes, methods, or incoming references:
|
||||
`inspect_android_package`, then focused Android tools when advertised.
|
||||
Archive member inventory still uses the archive route above.
|
||||
@@ -109,35 +100,21 @@ explicit path or endpoint and do not need it.
|
||||
`extract_firmware` when extraction is requested, with a caller-selected new
|
||||
absolute output directory. These tools use caller-supplied Binwalk/Unblob on
|
||||
Linux; see the [firmware guide](https://github.com/morluto/rea/blob/main/docs/firmware-analysis.md).
|
||||
- ESP32-family app or bootloader image (`0xE9` magic): `inspect_artifact`
|
||||
without a provider; see the ESP section of `references/native-and-artifacts.md`.
|
||||
- .NET NativeAOT PE/ELF: native Ghidra analysis. Native code can yield pseudocode;
|
||||
consult the NativeAOT workflow in `references/native-and-artifacts.md` for
|
||||
optional metadata recovery and its supported host/layout boundary.
|
||||
- Native program behavior, live calls, instrumentation, or command-run effects:
|
||||
choose the matching [native runtime workflow](references/runtime-observation.md#native-runtime-questions).
|
||||
`open_binary` and `trace_native_values` establish static facts; they do not
|
||||
observe execution. Runtime tools have distinct launch, attach, mutation and
|
||||
cleanup effects, and do not require a preliminary static session unless their
|
||||
own contract says so.
|
||||
- User-owned browser page already open: `list_browser_targets`.
|
||||
- Browser action the agent must perform (click, type, navigate):
|
||||
`capture_browser_scenario` with declared actions. See
|
||||
[runtime observation](references/runtime-observation.md) for connect/launch
|
||||
and the focused follow-up browser tools.
|
||||
- Agent environment (AgentEnv bundle) of a web application: read
|
||||
[environments](references/environments.md) before recording; it records with
|
||||
`capture_browser_scenario` and writes the bundle with `export_environment_bundle`.
|
||||
- Fiddler Everywhere traffic or a native `.saz` capture: read the
|
||||
[Fiddler workflow](references/runtime-observation.md#fiddler-everywhere-traffic-debugging).
|
||||
Inspect saved SAZ with `inspect_web_network_capture` and `format: "saz"`;
|
||||
connect Everywhere's separate MCP server for live traffic.
|
||||
- Retained HAR/SAZ/native mitmproxy/aioquic qlog capture: `inspect_web_network_capture`. Select
|
||||
original record ordinals when useful; inspect producer fields and byte/number
|
||||
sidecars without fetching recorded URLs or claiming live attribution. HAR/SAZ/qlog
|
||||
old-generation heap is `REA_CAPTURE_DECODER_OLD_GENERATION_MIB` (default 192 MiB,
|
||||
floor 8, maximum 16384), set where REA starts. Host memory and `NODE_OPTIONS` do not
|
||||
raise it. See
|
||||
sidecars without fetching recorded URLs or claiming live attribution. See
|
||||
[historical captures](https://github.com/morluto/rea/blob/main/docs/web-network-captures.md) for the exact upstream
|
||||
profile and credential exclusions. Qlog support starts with aioquic 1.5.0
|
||||
JSON 0.3; stream coverage and endpoint loss declarations do not recover bodies
|
||||
@@ -150,9 +127,7 @@ explicit path or endpoint and do not need it.
|
||||
and field names/types before choosing fields for `inspect_packet_capture`.
|
||||
Query/page the relevant records; registrations do not establish observed traffic.
|
||||
- Recorded packet captures: `inspect_packet_capture` with selected TShark fields,
|
||||
display filters and decode-as rules. Requires Linux, util-linux prlimit and
|
||||
TShark 4.x on an absolute `PATH` entry or via the optional absolute
|
||||
`REA_TSHARK_COMMAND` override; see
|
||||
display filters and decode-as rules. Requires Linux, util-linux prlimit and `REA_TSHARK_COMMAND`; see
|
||||
[capture guide](https://github.com/morluto/rea/blob/main/docs/packet-captures.md).
|
||||
Packet limits count input packets before filtering. Output defaults to 50 matching
|
||||
frames; use `match_offset` / `match_limit` for later pages, or explicit null
|
||||
@@ -179,18 +154,8 @@ explicit path or endpoint and do not need it.
|
||||
Select JavaScript glue and candidate paths explicitly and preserve URL/path
|
||||
ambiguity. No implicit fetch or target execution; WAT is not original source.
|
||||
See the [WASM guide](https://github.com/morluto/rea/blob/main/docs/wasm-artifacts.md).
|
||||
- Explicit local Android APK (or ZIP/AAB) signing: `inspect_apk_signature`.
|
||||
Prefer its summary or selected signer/digest/lineage page; retain complete
|
||||
Evidence and use `inspect_analysis_view` kind `apk-signature` for follow-ups.
|
||||
Reports which schemes are present (v1 JAR, v2, v3, v3.1), each signer's
|
||||
certificate SHA-256 fingerprint (v1 included, from its PKCS#7 block), v3 key-rotation lineage, source stamp, and
|
||||
whether each signed digest still matches the bytes (self-consistency). This
|
||||
is identity and integrity, not a trust or authenticity verdict.
|
||||
See the [APK signature guide](https://github.com/morluto/rea/blob/main/docs/apk-signature.md).
|
||||
- Explicit local EVM bytecode carrier: `inspect_evm_interface` with caller-selected
|
||||
`raw` or `hex` encoding. The owned worker deadline is `timeout_ms` (default
|
||||
30000 ms, range 1..2147483647, next call). It does not raise the fixed byte,
|
||||
address-space, or CPU allowances. Preserve carrier/decoded digests and treat selectors,
|
||||
`raw` or `hex` encoding. Preserve carrier/decoded digests and treat selectors,
|
||||
argument strings and mutability as inferred candidates; this performs no chain
|
||||
lookup or target execution. For the complete inferred interface without hex or
|
||||
duplicate stdout, discover support and select `detail: "summary"`; complete
|
||||
@@ -204,105 +169,14 @@ explicit path or endpoint and do not need it.
|
||||
absent metadata does not prove a binary is not Go, and unparsed text records
|
||||
remain explicit. See the
|
||||
[Go binary guide](https://github.com/morluto/rea/blob/main/docs/go-binaries.md).
|
||||
- Embedded CUDA/HIP code objects: `inspect_gpu_code_objects`; inventory targets
|
||||
first, then decode selected payloads and query retained GPU Evidence.
|
||||
- Bundled GGUF or safetensors model declarations: `inspect_model_artifact`
|
||||
with the explicit local file. Default header hashing leaves full-file identity
|
||||
unknown; `hash: "full"` additionally reads all weights for SHA-256. MCP returns
|
||||
retained summaries; optional `view` selects one page in the initial call. Use
|
||||
`inspect_analysis_view` kind `model-artifact` for
|
||||
batched metadata keys, tensor names or type totals, selected fields and pages.
|
||||
Preserve header-scoped indices and array preview omissions; do not infer model
|
||||
behavior from declared architecture or tensor types. See the
|
||||
[model file guide](https://github.com/morluto/rea/blob/main/docs/model-artifacts.md).
|
||||
- Unity IL2CPP names, member owners and string literals from `global-metadata.dat`:
|
||||
`inspect_il2cpp_metadata` with the explicit local file; MCP defaults to compact
|
||||
counts. Query sections, images, types, methods, fields, properties, events,
|
||||
parameters or string_literals through `inspect_analysis_view`
|
||||
with the returned `parent_evidence_id`, using exact filters, batch indices and
|
||||
selected fields. Parameters accept `method_indices`, avoiding a scan of all
|
||||
parameter pages. Missing collections in historical Evidence remain unknown.
|
||||
Versions 24, 27, 29, 31 and 39; no Unity, IL2CPP
|
||||
binary or disassembler is run. Unrecognized magic (possibly encrypted) and
|
||||
other versions are unsupported. Optional matching `binary_path` enables static native addresses and type references; query `method_addresses` with indices or addresses/states, preserving every shared-body method and both artifact identities. Without a linked binary native counts remain unknown. Byte-pattern fallback is inferred; no runtime behavior or same-build pairing is proved. See the
|
||||
[IL2CPP metadata guide](https://github.com/morluto/rea/blob/main/docs/il2cpp-metadata.md).
|
||||
- Embedded cargo-auditable Rust dependency metadata: `inspect_rust_binary`
|
||||
with the explicit local ELF, PE, or thin Mach-O file. This built-in
|
||||
target-free inspection requires no Rust installation or disassembler. Preserve
|
||||
package order, dependency indices, and the section file range. A missing
|
||||
`.dep-v0` section is null metadata, not proof the binary has no dependencies.
|
||||
A rustc commit hash is an inference from a read-only `/rustc/<40-hex>/` path.
|
||||
For selected crates, use `view: {collection: "packages", names: [...],
|
||||
fields: ["name", "version", "source"]}` on the first call; explicitly include
|
||||
`dependencies` for direct index references. Use `detail: "summary"` for counts
|
||||
and ranges, or retained `inspect_analysis_view` Rust package/edge pages for
|
||||
subsequent filters. Keep the complete parent Evidence accessible; CLI compact
|
||||
delivery requires `--evidence_output`.
|
||||
See the
|
||||
[Rust binary guide](https://github.com/morluto/rea/blob/main/docs/rust-binaries.md).
|
||||
- PE Authenticode certificate table: `inspect_pe_certificates` with the explicit
|
||||
local PE32/PE32+ file. This built-in target-free inspection requires no
|
||||
Windows SDK. Preserve WIN_CERTIFICATE ranges, types, payload SHA-256, and
|
||||
PKCS#7 signer/X.509 identity fields, including `subject_key_identifier_hex`
|
||||
when the signer uses that form. A missing security directory is observed
|
||||
absence of that table, not proof the image is unsigned in every catalog sense.
|
||||
Signatures are not verified. See the
|
||||
[PE certificate guide](https://github.com/morluto/rea/blob/main/docs/pe-certificates.md).
|
||||
- PE import and delay-load tables: `inspect_pe_imports` with the explicit
|
||||
local PE32/PE32+ file. This built-in target-free inspection requires no
|
||||
Windows SDK. Preserve DLL names, named and ordinal functions, ILT/IAT RVAs,
|
||||
delay-load descriptors, and the pefile import hash of the standard import
|
||||
table. A missing import directory is observed absence of that table, not
|
||||
proof the image never imported APIs through another table. Bound snapshots
|
||||
and loaded IAT contents remain unobserved. See the
|
||||
[PE import guide](https://github.com/morluto/rea/blob/main/docs/pe-imports.md).
|
||||
- PE TLS callbacks and load configuration: `inspect_pe_load_config` with the
|
||||
explicit local PE32/PE32+ file. This built-in target-free inspection requires
|
||||
no Windows SDK. Preserve callback VAs as strings with their RVA, file offset
|
||||
and section, and the size-covered security cookie, SafeSEH, CFG,
|
||||
EH-continuation and XFG fields. Guard-flag mitigation claims are inferred
|
||||
linker declarations, not proof of enforcement. A missing directory is observed
|
||||
absence of that declaration. See the
|
||||
[PE load configuration guide](https://github.com/morluto/rea/blob/main/docs/pe-load-config.md).
|
||||
- ELF note tables: `inspect_elf_notes` with the explicit local ELF32/ELF64
|
||||
file. This built-in target-free inspection requires no disassembler. Preserve
|
||||
PT_NOTE and SHT_NOTE ranges, owners, types, descriptor SHA-256, and GNU
|
||||
build-id or ABI-tag fields. A missing table is observed absence of those
|
||||
declarations, not proof the image never carried notes. See the
|
||||
[ELF notes guide](https://github.com/morluto/rea/blob/main/docs/elf-notes.md).
|
||||
- Apple `.ips` crash reports: `inspect_apple_ips` with the explicit local
|
||||
file. This built-in target-free inspection requires no Xcode, atos, or
|
||||
pwntools. Preserve first-line metadata, `bug_type` as a string, process and
|
||||
exception facts, lossless integer registers, usedImages UUIDs, and
|
||||
documented classification matches. Only `bug_type` `"309"` yields a crash
|
||||
body; other types, including `"288"` stackshots, return no crash result.
|
||||
Opened Mach-O and dSYM UUID joins remain unobserved. See the
|
||||
[Apple IPS guide](https://github.com/morluto/rea/blob/main/docs/apple-ips.md).
|
||||
- Linux kernel module metadata: `inspect_kernel_module` with the explicit local
|
||||
ELF32/ELF64 relocatable file. This built-in target-free inspection requires
|
||||
no Linux host or `modinfo`. Preserve `.modinfo` key order and duplicate keys,
|
||||
`__versions` CRC hex, section file ranges, and the appended signature
|
||||
trailer's PKCS#7 SHA-256. A missing section is observed absence when section
|
||||
names are known, not proof the module never carried that metadata.
|
||||
`struct module` is not decoded. The signature is not validated. gzip, xz, and
|
||||
zstd containers are unsupported; decompress into a separate file with an
|
||||
external codec utility, then inspect the decompressed `ET_REL` image. See the
|
||||
[kernel module guide](https://github.com/morluto/rea/blob/main/docs/kernel-modules.md).
|
||||
- CPython 3.7–3.13 `.pyc` header, nested marshal code objects and bytecode
|
||||
identity: `inspect_python_pyc` with the explicit local file. This built-in
|
||||
target-free inspection requires no Python installation. Preserve magic,
|
||||
PEP 552 hash or mtime/size, nested names, and bytecode SHA-256. Bytecode is
|
||||
not decompiled, and a decoded header does not prove the matching `.py` is
|
||||
present. See the
|
||||
[CPython pyc guide](https://github.com/morluto/rea/blob/main/docs/python-pyc.md).
|
||||
- Erlang BEAM IFF container: `inspect_erlang_beam` with the explicit local
|
||||
`.beam` file. This built-in target-free inspection requires no Erlang
|
||||
installation. Preserve chunk names, atom order, export/import/lambda tables,
|
||||
and the Code header. For selected facts, prefer `detail: "summary"` plus a
|
||||
`beam` view (collection, names/indices, fields and paging), retaining the full
|
||||
Evidence. CLI summary requires `--evidence_output`. Missing table counts stay
|
||||
null; decoded empty tables count zero. Bytecode is not executed. See the
|
||||
[Erlang BEAM guide](https://github.com/morluto/rea/blob/main/docs/erlang-beam.md).
|
||||
|
||||
- Explicit local SQLite database snapshot: `inspect_sqlite_database`. Inspect
|
||||
with Node.js 24.x >=24.15 or 26+ providing native SQLite limits. Read schema
|
||||
@@ -325,8 +199,6 @@ explicit path or endpoint and do not need it.
|
||||
- PE resource type/name/language trees and static icon group references:
|
||||
`inspect_pe_resources` with an explicit path. This portable operation requires
|
||||
no active target or engine; opaque payloads retain file ranges and SHA-256.
|
||||
- PE export directories and ELF dynamic-symbol exports: `inspect_native_exports`
|
||||
with an explicit path; portable, no active target or engine.
|
||||
- Native executable, library, or analysis database: `open_binary`, then
|
||||
use focused analysis tools directly; call `binary_overview` when metadata or
|
||||
inventory context is useful and available from the selected provider.
|
||||
@@ -338,11 +210,6 @@ leaves its database open. The modern headless profile creates and closes an
|
||||
owned private database without saving. Installation and registration reuse
|
||||
[mrexodia/ida-pro-mcp](https://github.com/mrexodia/ida-pro-mcp); see the
|
||||
[IDA provider guide](https://github.com/morluto/rea/blob/main/docs/ida-provider.md).
|
||||
For migration to the separate native IDA MCP plugin, follow the guide's
|
||||
[legacy attached migration](https://github.com/morluto/rea/blob/main/docs/ida-provider.md#migrate-a-legacy-attached-registration).
|
||||
Replace the upstream registration explicitly; modern mrexodia tools are not the
|
||||
native profile. Native attachment preserves the database but does not provide
|
||||
direct callers; check session availability and the guide's verification limits.
|
||||
Proceed directly to `analyze_function`, pseudocode, or function/string searches
|
||||
after opening; IDA does not supply `binary_overview`. Consult session availability
|
||||
when composing broader workflows. Modern direct callers and unsupported dossier
|
||||
@@ -373,24 +240,12 @@ native target whose analysis tool operates on the active target. Do not call an
|
||||
unadvertised tool; inspect `binary_session` with `{}` and its
|
||||
`result.tool_availability` for availability reasons and remediation. Expand each
|
||||
group's `names` into entries, inheriting the inventory-level client requirements
|
||||
unless the group overrides them. The grouped name union is the complete tool
|
||||
inventory; use `tools/list` for canonical tool schemas. The default summary keeps
|
||||
each provider candidate's availability and target-support status, code and
|
||||
reason; `omitted_fields` lists what it leaves out. Request `{"detail": "complete"}`
|
||||
only when you need an unavailable candidate's remediation, provider
|
||||
diagnostics, candidate capability sets, or tool surface, effect and annotation
|
||||
flags; in those closed flag vocabularies, listed
|
||||
flags are true and omitted known flags are false. Provider candidate facts
|
||||
describe possible providers, while top-level session capabilities describe the
|
||||
current route and `analysis_provider_binding` names the selected provider. Do
|
||||
not treat those scopes as interchangeable.
|
||||
|
||||
For a Ghidra startup timeout on a large native target, follow
|
||||
[deadline recovery](references/native-and-artifacts.md#ghidra-startup-deadline-recovery)
|
||||
before abandoning deep analysis or repeating the same cold import. The default
|
||||
330 seconds can be changed for new startup attempts through
|
||||
`update_analysis_settings` using `ghidra.startup_timeout_ms`. Changing the
|
||||
server's launch environment instead requires a new REA process.
|
||||
unless the group overrides them. In its closed effect and annotation vocabularies,
|
||||
listed flags are true and omitted known flags are false. The grouped name union
|
||||
is the complete tool inventory; use `tools/list` for canonical tool schemas.
|
||||
Provider candidate facts describe possible providers, while top-level session
|
||||
capabilities describe the current route and `analysis_provider_binding` names the
|
||||
selected provider. Do not treat those scopes as interchangeable.
|
||||
|
||||
## Work summary-first
|
||||
|
||||
@@ -398,22 +253,7 @@ Start with the default result and use its inline Evidence and graph context.
|
||||
For a JavaScript application or ELF layout whose size is unknown or large,
|
||||
call `analyze_javascript_application` or `inspect_binary_layout` with
|
||||
`"detail": "summary"`, then inspect the retained `parent_evidence_id` with
|
||||
`inspect_analysis_view`. Over MCP, `analyze_function` is summary-first without
|
||||
a flag: its default reply is the summary, and the complete dossier stays
|
||||
retained under `parent_evidence_id` for facet paging. Pass that ID as a
|
||||
`retained-evidence` reference to `compare_functions`, or inside
|
||||
`match_native_functions` `left_observations`/`right_observations` or
|
||||
`build_call_path.functions`; request `"detail": "complete"` only when the
|
||||
dossier must travel inline.
|
||||
Use the same exact-reference form in other advertised logical Evidence inputs,
|
||||
including optional sources, arrays and nested sources across domains. Each
|
||||
source can independently be inline or retained; preserve its source identity,
|
||||
coverage, partial facts and unknowns. A summary or inspection page has its own
|
||||
Evidence ID: use `normalized_result.parent_evidence_id` when the consumer needs
|
||||
the complete source. For portable CLI follow-ups, commands advertising
|
||||
`--evidence-bundle` resolve references from that selected exported bundle.
|
||||
See [Evidence composition](references/evidence-workflows.md) for scope and
|
||||
recovery; file selectors are supported only where explicitly advertised.
|
||||
`inspect_analysis_view`.
|
||||
Page module identities and select the returned `node_id` before requesting an
|
||||
item; a single file path can represent several graph roles. Do not repeat an
|
||||
identical analysis or rejected call without a changed input or resolved blocker.
|
||||
@@ -432,10 +272,7 @@ output also streams without a single MCP frame.
|
||||
When the retained parent ID is unknown, use `get_evidence_bundle` with
|
||||
`detail: "summary"` and exact filters for operation, target SHA-256,
|
||||
analysis-profile digest or canonical procedure address. Select its returned
|
||||
Evidence ID for the next view instead of requesting the complete bundle. When
|
||||
that record has no supported view (for example an annotation), read it with
|
||||
`detail: "records"` and an `evidence_id` filter; `missing_evidence_links`
|
||||
names linked sources this session does not retain.
|
||||
Evidence ID for the next view instead of requesting the complete bundle.
|
||||
`retention: "complete-record"` means the whole record is stored; native
|
||||
value-flow truncation and unknown metadata remain separate. For an exported
|
||||
portable bundle, use `rea inspect-evidence-bundle` with JSON containing `path`,
|
||||
@@ -504,22 +341,15 @@ as complete while required questions remain open.
|
||||
[references/javascript-applications.md](references/javascript-applications.md)
|
||||
- Android APK declarations, classes, methods, and static references:
|
||||
[references/android-applications.md](references/android-applications.md)
|
||||
- Native instrumentation, debugger and process capture; browser/Electron
|
||||
observation; static/runtime comparison:
|
||||
- Browser/Electron observation (passive and scenario) and static/runtime
|
||||
reconciliation:
|
||||
[references/runtime-observation.md](references/runtime-observation.md)
|
||||
- Evidence paging, comparisons, residual unknowns, and verification:
|
||||
[references/evidence-workflows.md](references/evidence-workflows.md)
|
||||
- Agent environments (AgentEnv bundles) of a web application, recorded from
|
||||
a live instance or a HAR:
|
||||
[references/environments.md](references/environments.md)
|
||||
|
||||
## Finish the task
|
||||
|
||||
Explain findings in plain language and tie them to returned evidence. When the
|
||||
user asks to build something, use normal coding tools and separate observed
|
||||
behavior from design choices. Close an opened native session with
|
||||
`close_binary` when its engine is no longer needed. Evidence references and Unknown
|
||||
history remain in memory on the same MCP connection. End the investigation with
|
||||
`reset_investigation`; supply `save_path` to checkpoint the complete bundle before
|
||||
clearing. Import the saved bundle to restore original references and Unknown
|
||||
history in a new process. Retention is bounded and is not automatic persistence.
|
||||
`close_binary` when the investigation is complete.
|
||||
@@ -13,47 +13,18 @@ own APK path and explicit selectors; do not open a native session first.
|
||||
Choose an `overload_index` from the class inventory when methods are ambiguous
|
||||
instead of guessing.
|
||||
|
||||
For JNI implementations, discover `inspect_android_jni_bindings`. Select raw
|
||||
DEX class/method identities and optional descriptors, ABI or logical library
|
||||
paths. Prefer `detail: "summary"`; complete Evidence remains retained and
|
||||
`android-jni` views page declarations, libraries, candidates or binding rows
|
||||
with exact fields. Global native-pair ambiguity survives class selection.
|
||||
Preserve slot/relocation provenance and source coverage: matched means observed
|
||||
candidate presence; partial scans do not establish uniqueness or absence.
|
||||
This reads DEX/ELF bytes without loading the APK into the decompiler or executing
|
||||
target code. Runtime registration and loading remain unknown. See the
|
||||
[JNI guide](https://github.com/morluto/rea/blob/main/docs/android-analysis.md#link-native-declarations-to-jni-candidates).
|
||||
|
||||
For an R8/ProGuard release build with its `mapping.txt`, pass the absolute
|
||||
`mapping_path` to `inspect_android_class` or `inspect_android_method`. Prefer
|
||||
`detail: "summary"` to receive normalized symbols or the selected body without
|
||||
duplicate raw class metadata. Complete Evidence stays retained under the linked
|
||||
parent ID and can be exported; CLI summary delivery requires `--evidence_output`.
|
||||
Omitted or complete detail preserves the full inline response. Keep
|
||||
selecting by observed names; the separate `mapping` block reports each symbol
|
||||
as `mapped`, `ambiguous` or `unmapped` after checking DEX types and prototypes.
|
||||
REA does not verify that the mapping belongs to the APK build.
|
||||
|
||||
The provider requires a separately supplied headless JADX MCP JAR 0.7.1 and
|
||||
Java 17 or newer. `REA_JADX_MCP_JAR` selects the absolute JAR path; `JAVA_HOME`
|
||||
can select Java. Use `rea doctor --provider jadx --json` to check the JADX
|
||||
configuration and obtain remediation for missing prerequisites. Java and the
|
||||
headless JADX MCP JAR remain caller-supplied; REA setup does not install or
|
||||
configure them. Configuration readiness does not establish successful analysis
|
||||
of a particular APK.
|
||||
can select Java. REA setup does not install or configure that JAR, and doctor
|
||||
provider scopes cover Hopper/Ghidra rather than JADX. Use existing tools and
|
||||
report missing prerequisites for the Android task specifically.
|
||||
|
||||
Linux and macOS use the POSIX owned-process boundary. Windows x64 requires the
|
||||
matching bundled native controls and uses an owned Job Object with bidirectional
|
||||
stdio. A full JDK, including `jdk.compiler`, is required on every host; Windows
|
||||
`JAVA_HOME` selects `bin\java.exe`. Consult the Android guide for actual host
|
||||
verification coverage. Calls use one worker and provider deadlines; optional
|
||||
`REA_JADX_HEAP_MIB` selects the JVM heap and `REA_JADX_OPERATION_TIMEOUT_MS`
|
||||
the per-operation deadline (integer milliseconds from 1 to 2147483647, default
|
||||
120000). To change that deadline on a running MCP server, set
|
||||
`jadx.operation_timeout_ms` through `update_analysis_settings`, then retry after
|
||||
work and cleanup settle. Newly accepted operations use the override; accepted
|
||||
work keeps its budget. Changing the launch environment requires a new process.
|
||||
The MCP client's tool timeout must also allow the selected deadline. The APK is never launched,
|
||||
`REA_JADX_HEAP_MIB` selects the JVM heap. The APK is never launched,
|
||||
and no emulator is required.
|
||||
|
||||
Manifest entries and decompiled text are static observations, not execution or
|
||||
|
||||
@@ -7,23 +7,18 @@ operation and the next useful step; after repair, continue the investigation.
|
||||
Commands below use an installed `rea`. If none is available, use the package
|
||||
invocation in `SKILL.md` when acquiring REA is authorized. Follow the connected
|
||||
server's schemas and the installed CLI's help rather than assuming repository
|
||||
main and an older release expose the same arguments. Tool-name prefixes belong
|
||||
to the client: find the canonical REA operation in its actual tool listing and
|
||||
server association before concluding that it is absent. A shared prefix alone
|
||||
does not establish which server an approval wildcard covers. See
|
||||
[client tool names and discovery](https://github.com/morluto/rea/blob/main/docs/client-configuration.md#client-tool-names-and-discovery).
|
||||
main and an older release expose the same arguments.
|
||||
|
||||
## Choose the remedy from the observed failure
|
||||
|
||||
| Observation | Meaning and next step |
|
||||
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| No REA tools in the active session | If REA was installed as a client plugin, follow the plugin section below. Otherwise inspect registration with `rea doctor --client codex --json`. A skill installation alone does not register MCP. |
|
||||
| Missing, malformed or stale registration | Prepare a scoped setup plan; apply only authorized changes with backups. |
|
||||
| Aligned registration, but no connected tools | Restart/reconnect the client. Doctor checks registration; it cannot establish the current agent's connection. Inspect the client's launch error if reconnection fails. |
|
||||
| MCP connection dropped or REA could not start | If the connection drops, retry that call once only when it is safe to repeat or known not to have started. If it may already have changed the target, or the same failure returns, tell the user REA's connection failed and ask them to restart the MCP client. If REA could not start its connection, retry once, then tell the user and ask them to restart the MCP client. Do not retry a startup failure that returns the same unchanged result. Run `rea doctor` when REA could not start or tools stay missing after restart. Doctor cannot restore a live connection by itself. |
|
||||
| `MCP tool call requires approval, but approval policy is never` | Codex rejected the call before REA executed it. Do not install an engine or repeat setup to solve this client-policy decision. See the approval section below. |
|
||||
| REA returns `provider_unavailable` or an unavailable tool entry | Read that operation's provider diagnostics or `binary_session.result.tool_availability`. Expand each group's tool names and inherit the shared client requirements unless overridden. Use the operation's availability, reason, and remediation; a complete tool catalog does not imply every operation is available. Request `{"detail": "complete"}` only for provider diagnostics. |
|
||||
| `rea` is missing in a child shell, or Node is incompatible | Inspect the actual shell PATH and launcher/runtime. A prefix installation may be absent from a login shell's PATH; this does not establish a broken MCP registration. |
|
||||
| Observation | Meaning and next step |
|
||||
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| No REA tools in the active session | Inspect registration with `rea doctor --client codex --json`. A skill installation alone does not register MCP. |
|
||||
| Missing, malformed or stale registration | Prepare a scoped setup plan; apply only authorized changes with backups. |
|
||||
| Aligned registration, but no connected tools | Restart/reconnect the client. Doctor checks registration; it cannot establish the current agent's connection. Inspect the client's launch error if reconnection fails. |
|
||||
| `MCP tool call requires approval, but approval policy is never` | Codex rejected the call before REA executed it. Do not install an engine or repeat setup to solve this client-policy decision. See the approval section below. |
|
||||
| REA returns `provider_unavailable` or an unavailable tool entry | Read that operation's provider diagnostics or `binary_session.result.tool_availability`. Expand each group's tool names and inherit the shared client requirements unless overridden. Use the operation's availability, reason, and remediation; a complete tool catalog does not imply every operation is available. |
|
||||
| `rea` is missing in a child shell, or Node is incompatible | Inspect the actual shell PATH and launcher/runtime. A prefix installation may be absent from a login shell's PATH; this does not establish a broken MCP registration. |
|
||||
|
||||
If the client cannot launch its shell/sandbox helper or read the installed skill,
|
||||
inspect its launch diagnostic. For example, Codex can refuse helper aliases when
|
||||
@@ -39,109 +34,6 @@ answers part of the question, and name the unavailable evidence separately.
|
||||
Do not substitute another target. Label observations from ordinary shell tools
|
||||
as such; only actual REA output supplies REA Evidence.
|
||||
|
||||
## Distinguish client and server deadlines
|
||||
|
||||
A host message such as `timed out awaiting tools/call` with no REA result means
|
||||
the client's deadline expired before a result arrived. It does not establish
|
||||
that REA finished, canceled successfully, or released all resources. Do not
|
||||
repeat the unchanged analysis. Inspect available session/provider state and
|
||||
retained Evidence only when useful; an idle `binary_session` is not proof that
|
||||
every target-free operation stopped. Never invent a missing Evidence reference.
|
||||
|
||||
Allow the host tool deadline to cover execution, cancellation/cleanup, and
|
||||
response delivery. In Codex, the setting is
|
||||
`mcp_servers.<server-id>.tool_timeout_sec` in **seconds**, not
|
||||
`startup_timeout_sec`. Select a value for the actual operation budget and
|
||||
headroom, not a universal timeout. See the
|
||||
[official MCP configuration reference](https://learn.chatgpt.com/docs/extend/mcp?surface=cli).
|
||||
Apply configuration changes only within the authorized scope, preserving
|
||||
unrelated entries and backups; reconnect/restart as required by the host.
|
||||
When configuration is not editable, report the setting needed or use an
|
||||
authorized CLI fallback. Do not silently broaden permissions or switch targets.
|
||||
|
||||
A returned REA `provider_timeout` instead means the **server/provider** reached
|
||||
its deadline. Follow its `remediation.action`, reuse accessible partial Evidence,
|
||||
and resolve reported incomplete cleanup before retrying. Change a request
|
||||
field or a `REA_*` setting only when the returned remediation explicitly
|
||||
recommends that change. Restart REA when that text says the value is read at
|
||||
startup. A mention that a control does not raise the deadline is not a
|
||||
recommendation to change it. Do not invent a setting the remediation does not
|
||||
name. Raising only the client deadline does not change
|
||||
this server budget. For static JavaScript,
|
||||
`analysis_timeout_ms` (CLI `rea analyze-javascript-application <path>
|
||||
--analysis-timeout-ms <milliseconds>`) controls each source's
|
||||
startup, analysis and transfer deadline in **milliseconds**. Increase that
|
||||
budget or reduce semantic work / select exact inventoried `source_paths` when
|
||||
the task permits. Omit `source_paths` for all sources; `[]` is invalid.
|
||||
`detail: "summary"` reduces delivery, not analysis work. A per-source budget
|
||||
does not bound the elapsed time for an entire multi-source application.
|
||||
Partial-Evidence inspection can avoid recomputation; an unavailable reader is
|
||||
not itself a reason to block a corrected static-analysis retry.
|
||||
The shorthand `rea analyze <path>` does not expose this timeout flag; use the
|
||||
explicit JavaScript command when changing that budget.
|
||||
|
||||
For invalid arguments, use the connected tool's current input schema, or the
|
||||
installed CLI's command help. Refresh the catalog if stale; do not guess fields
|
||||
from an older skill or return the entire catalog to repair one field. Correct
|
||||
all reported fields before retrying.
|
||||
|
||||
## Recovery ladder
|
||||
|
||||
Follow the returned remediation. Change a request field or a `REA_*` setting
|
||||
only when that text explicitly recommends the change. Restart REA when it says
|
||||
the value is read at startup. A mention that a control does not raise a limit
|
||||
is not a recommendation to change it. Do not invent a setting. Host memory and
|
||||
`NODE_OPTIONS` do not raise a REA admission unless that remediation says so.
|
||||
|
||||
- MCP connection dropped: retry that call once only when it is safe to repeat
|
||||
or known not to have started. If it may already have changed the target, or
|
||||
it fails again, tell the user REA's connection failed and ask them to restart
|
||||
the MCP client. Do not run `rea doctor` for the drop itself.
|
||||
- REA could not start its connection: retry once, then tell the user and ask
|
||||
them to restart the MCP client. Run `rea doctor` when REA could not start or
|
||||
tools stay missing after that restart. Do not retry a startup failure that
|
||||
returns the same unchanged result.
|
||||
- `cleanup_incomplete`, or retained cleanup owners in the error details: retry
|
||||
that exact cleanup owner. Do not start new work that needs those resources
|
||||
until that owner confirms release. Repeating the original call is not that
|
||||
retry.
|
||||
- Capacity, when the result is `resource_constraint` and `retryable` is true:
|
||||
wait for the active admission to finish, then retry within the reported
|
||||
limits. Do not restart REA to free the slot.
|
||||
- `configuration_invalid`: correct the named setting in the environment that
|
||||
starts REA (the shell or the MCP registration), then restart REA. Run
|
||||
`rea doctor` to see the `runtime-configuration:` check. Re-run `rea setup`
|
||||
for that client when the registration must carry the new setting, then
|
||||
reconnect.
|
||||
- `invalid_request`: correct the reported fields from the current schema. Do
|
||||
not repeat the unchanged call.
|
||||
|
||||
## REA installed as a client plugin
|
||||
|
||||
A Claude Code or Codex plugin owns its `rea` server and this skill. Doctor's
|
||||
`--client` and `--skill` checks and `rea setup` read and write only
|
||||
setup-owned state, so for a plugin-only install they report `missing`, and
|
||||
applying setup would add a second `rea` server. If this skill was loaded from
|
||||
a plugin, or the client lists a `rea` plugin, do not prepare a setup plan for
|
||||
missing tools. Instead:
|
||||
|
||||
- Check that the plugin is installed and enabled with the client's own
|
||||
listing: `claude plugin list` and `/mcp` in Claude Code, `codex plugin list`
|
||||
in Codex.
|
||||
- Restart or reconnect the client, then read its launch error for the `rea`
|
||||
server if tools are still absent. The plugin starts its bundled runtime; a missing build or version mismatch
|
||||
requires a reinstall, or a fresh build when developing from source.
|
||||
- Check analysis engines through the connected server's
|
||||
`binary_session.result.tool_availability`. `rea doctor --provider <id> --json`
|
||||
from a shell reports that shell's environment, which can differ from the
|
||||
environment the client gave the plugin server. If you still run it through
|
||||
the package runner, use the version pinned in the plugin manifest's server
|
||||
entry (`rea-agents@<version>`), not `@latest`, so it matches the server the
|
||||
plugin starts.
|
||||
|
||||
Use the setup repair below only when the user wants a setup-owned registration
|
||||
instead of the plugin, and remove or disable the plugin first.
|
||||
|
||||
## Repair registration when needed
|
||||
|
||||
For Codex:
|
||||
@@ -154,12 +46,9 @@ 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`, `freebuff`, or `grok_bot`. If unknown, inspect `doctor --json` before choosing a
|
||||
`hermes`, `qoder`, `droid`, `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 manual stdio registration with the selected
|
||||
installation’s absolute runtime and entry script, or continue through the CLI.
|
||||
Acquire packages from a trusted directory outside the target project. Keep a
|
||||
package-runner installation and its runtime available while registrations use it.
|
||||
For unsupported clients, use version-pinned manual stdio registration or CLI.
|
||||
|
||||
Show the plan's exact paths, backups, changes and bundled-skill replacement.
|
||||
Obtain approval for writes when that concrete scope is not already authorized,
|
||||
@@ -204,17 +93,6 @@ writing. Managed policy can constrain client settings. Consult the
|
||||
for the installed client's supported settings. This is an optional recovery
|
||||
choice, not a prerequisite for every REA task.
|
||||
|
||||
## Repair an editable REA limit
|
||||
|
||||
If `details.limit` names an editable setting and its bounds, call
|
||||
`update_analysis_settings` directly with a `changes` entry containing `setting`,
|
||||
`action: "set"` and `value`. `inspect_analysis_settings` is optional when those
|
||||
facts are already present. Retry the same input after work and cleanup settle.
|
||||
Runtime defaults affect new work only; explicit per-call arguments take
|
||||
precedence and must be corrected directly when selected. Reset restores launch
|
||||
values. Updates survive target close/open but disappear on REA restart. They do
|
||||
not edit host deadlines, response buffers, registrations or fixed limits.
|
||||
|
||||
## Continue through an installed CLI
|
||||
|
||||
Check the command environment only if the launcher fails:
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
# Agent environments from a live application
|
||||
|
||||
Use this when the user asks for an agent environment (an AgentEnv bundle) of a
|
||||
web application: a folder `agent-env` deploys, whose MCP tools answer from what
|
||||
one recording of the application observed and serve the writes that recording
|
||||
confirmed. The workflow records the application once with
|
||||
`capture_browser_scenario`, checks what the environment would serve, records
|
||||
again if a hint names a gap, then writes the bundle with
|
||||
`export_environment_bundle`. For a HAR the user already has, skip to
|
||||
[export](#export-and-hand-off) through `inspect_web_network_capture`, or use
|
||||
`rea environment-build <capture.har> <dir>`.
|
||||
|
||||
## Before recording
|
||||
|
||||
- Record only against an instance the user says is disposable or seeded for
|
||||
this purpose: every write step creates, edits or deletes its data. If the
|
||||
user has not said so, ask before any scenario that writes. REA does not
|
||||
start, seed or reset applications.
|
||||
- Credentials come from environment variables the user names. Declare each as a
|
||||
scenario secret, `{secret_id, environment_variable}`; the variable must be set
|
||||
in the environment REA's MCP server started with. Never ask for a password in
|
||||
chat. Declared secrets are redacted as the capture records.
|
||||
- Map the application first with `inspect_web_page` (and `capture_web_screenshot`
|
||||
when layout matters): the login form's locators, and the API calls behind the
|
||||
pages that list and edit the entities the user asked about.
|
||||
- `inspect_web_page` cannot sign in. To see pages behind sign-in, run a short
|
||||
`capture_browser_scenario` that signs in and opens them, with
|
||||
`"capture": {"after_each_step": ["accessibility"]}` (`"dom"` for markup).
|
||||
Read a step's tree with `inspect_analysis_view`, `view: {kind:
|
||||
"browser-scenario", collection: "text", step_index, artifact:
|
||||
"accessibility", offset: 0, limit: 2048}`, paging with `offset`. Capture
|
||||
options are only `after_each_step`, `at_end`, `events`, `network` and
|
||||
`limits`.
|
||||
|
||||
## Record a session an environment can learn writes from
|
||||
|
||||
One `capture_browser_scenario` records the whole session. Start from this
|
||||
shape, with the application's own locators, paths and headers:
|
||||
|
||||
```json
|
||||
{
|
||||
"browser": { "mode": "launch", "executable_path": "/usr/bin/chromium" },
|
||||
"start_url": { "url": "http://127.0.0.1:3000/login" },
|
||||
"secrets": [
|
||||
{ "secret_id": "admin_password", "environment_variable": "APP_PASSWORD" }
|
||||
],
|
||||
"capture": {
|
||||
"network": {
|
||||
"request_body": true,
|
||||
"response_body": true,
|
||||
"header_values": true
|
||||
}
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"step_id": "user",
|
||||
"action": "fill",
|
||||
"timeout_ms": 15000,
|
||||
"locator": { "kind": "css", "selector": "input[name=username]" },
|
||||
"value": { "source": "literal", "value": "admin" }
|
||||
},
|
||||
{
|
||||
"step_id": "password",
|
||||
"action": "fill",
|
||||
"timeout_ms": 15000,
|
||||
"locator": { "kind": "css", "selector": "input[name=password]" },
|
||||
"value": { "source": "secret", "secret_id": "admin_password" }
|
||||
},
|
||||
{
|
||||
"step_id": "sign-in",
|
||||
"action": "press",
|
||||
"timeout_ms": 15000,
|
||||
"locator": { "kind": "css", "selector": "input[name=password]" },
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step_id": "signed-in",
|
||||
"action": "wait_for",
|
||||
"timeout_ms": 15000,
|
||||
"locator": { "kind": "css", "selector": "nav a[href='/settings']" },
|
||||
"state": "visible"
|
||||
},
|
||||
{
|
||||
"step_id": "folders-1",
|
||||
"action": "request",
|
||||
"timeout_ms": 15000,
|
||||
"method": "GET",
|
||||
"path": "/api/folders",
|
||||
"expected_status": [200]
|
||||
},
|
||||
{
|
||||
"step_id": "new-folder-1",
|
||||
"action": "click",
|
||||
"timeout_ms": 15000,
|
||||
"locator": { "kind": "role", "role": "button", "name": "New folder" }
|
||||
},
|
||||
{
|
||||
"step_id": "folder-title-1",
|
||||
"action": "fill",
|
||||
"timeout_ms": 15000,
|
||||
"locator": { "kind": "css", "selector": "input[name=title]" },
|
||||
"value": { "source": "literal", "value": "Scratch folder 1" }
|
||||
},
|
||||
{
|
||||
"step_id": "create-folder-1",
|
||||
"action": "click",
|
||||
"timeout_ms": 15000,
|
||||
"locator": { "kind": "role", "role": "button", "name": "Create" },
|
||||
"save_response": {
|
||||
"method": "POST",
|
||||
"path": "/api/folders",
|
||||
"save": [{ "name": "folder_1", "json_pointer": "/id" }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": "read-folder-1",
|
||||
"action": "request",
|
||||
"timeout_ms": 15000,
|
||||
"method": "GET",
|
||||
"path": "/api/folders/{saved:folder_1}",
|
||||
"expected_status": [200]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Give each step that accepts `timeout_ms` a deadline. Without one, a step waits
|
||||
until it completes or the scenario is cancelled, so a locator that never
|
||||
matches hangs the capture and the client with it. The `wait_for_timeout` pause
|
||||
instead uses `duration_ms` and does not accept `timeout_ms`. Match `wait_for`
|
||||
locators to exactly one element.
|
||||
|
||||
1. Sign in through the UI: `fill` the user and password fields (the password
|
||||
as a secret reference), submit, then `wait_for` an element only a signed-in
|
||||
user sees.
|
||||
2. Read with `request` steps: each collection the environment needs, twice
|
||||
before any write, so fields that change on their own show up; then each
|
||||
object a list shows by id, up to ten per collection. An object known only
|
||||
from a list that leaves out fields its own read shows cannot be read by id
|
||||
or updated in the environment.
|
||||
3. Write the way the application's UI does: fill its forms and click its
|
||||
buttons, so each request carries the fields the UI sends, including ones
|
||||
the server would otherwise derive. Give the click or key press that submits
|
||||
a create `save_response` to keep the new object's ID, then `goto` its page
|
||||
(`/items/{saved:item_1}`) or read it with a `request` step. Never guess an
|
||||
ID, and never create a probe object to learn the next one. Write with
|
||||
`request` steps instead only when the UI cannot reach the operation (an
|
||||
API-only resource, or a UI that does not call the JSON API), or when coverage
|
||||
withholds the UI-shaped write as `unexplained-change` or `contradicted`;
|
||||
that counts as one of the two re-recordings. A `request` step write that
|
||||
coverage withholds that way usually lacks fields the UI sends: record it
|
||||
through the UI instead. If a journey cannot be scripted, leave the write
|
||||
unserved and report the limitation.
|
||||
Record two numbered scratch objects per write type,
|
||||
with varied free text. After each write, and before the next one, read the
|
||||
object back by id and then its list: create 1, read 1, list, create 2,
|
||||
read 2, list; never several writes before their reads.
|
||||
4. Leave more than a second between two creates and between a create and its
|
||||
first edit (`wait_for_timeout` with `duration_ms: 1100`), so lists sorted
|
||||
by creation time show them in order.
|
||||
5. Update both objects and read each back, changing the fields a person
|
||||
would change (a name, an email address, a status, a due date) rather than
|
||||
only a description: a served update accepts only the members a recorded
|
||||
update stored. Run an action that turns something on or off (star,
|
||||
bookmark, archive) twice in a row on one object, reading it back after
|
||||
each: the second call shows whether it sets or flips the field, and the
|
||||
environment serves the action only once a call has shown it. Delete both
|
||||
and read each back (expect 404, or the object marked deleted) and the list.
|
||||
6. Vary what the two creates send, however you write them: the first every
|
||||
field the application takes, the second only what a person must give (its
|
||||
title or name), with the other fields left out or empty. A served create
|
||||
requires each member both creates filled in and lets a caller leave out one
|
||||
a create left out or sent empty, so an agent that names only a title is not
|
||||
refused for fields it had no reason to set.
|
||||
|
||||
About `request` steps:
|
||||
|
||||
- `path` is on the start URL's origin; the page must be on that origin.
|
||||
- Header values are literals, declared secrets, a cookie the browser holds
|
||||
(`{source: "cookie", name}`), a local storage entry
|
||||
(`{source: "local_storage", key, json_pointer}`), a saved value
|
||||
(`{source: "saved", name}`), or Basic credentials
|
||||
(`{source: "basic_credentials", parts: [username, password]}`, sent as the
|
||||
base64 of `username:password` and redacted), with an optional `prefix` such as
|
||||
`"Bearer "` or `"Basic "`. Each credential part is a literal, secret, saved
|
||||
value, cookie or local storage entry, so a header the frontend builds from a
|
||||
login token (the username and the token) can be copied too; the password is
|
||||
a secret or saved value, never a literal. Copy the
|
||||
headers the application's own requests send (a CSRF header such as
|
||||
`X-CSRFToken` from the `csrftoken` cookie, a bearer token). Values read from the page are redacted from records made after they
|
||||
are read, not from the login response that returned them, and values
|
||||
shorter than 8 characters are not redacted at all.
|
||||
- `save: [{name, json_pointer, secret}]` keeps a string or number from the
|
||||
step's JSON response, such as the created object's `/id`, for later steps;
|
||||
`secret: true` redacts it, as for a token the API returns, so mark every
|
||||
saved token, key or session value secret. This also protects the selected
|
||||
source response before its body or headers are retained. Compressed or
|
||||
unknown-size UI responses cannot supply saved values. On a `click` or `press`,
|
||||
`save_response: {method, path, save}` keeps values from the response to the
|
||||
new request observed while the step waits (`{*}` in `path` matches one segment).
|
||||
Matching does not prove UI causality: choose a path distinct from polling;
|
||||
multiple observed matches fail as ambiguous. The step
|
||||
fails unless a 2xx JSON response arrives before its `timeout_ms`. Later
|
||||
steps send a saved value as `{saved:name}` in a request `path` (and its
|
||||
query) or a `goto` URL; in the `json` body, where a string that is only
|
||||
`{saved:name}` keeps the saved JSON type; or as `{source: "saved", name}` in
|
||||
a header or credential part.
|
||||
- Reading a page value makes later bodies costlier to keep: after a CSRF
|
||||
token, bodies over about 47 KiB are kept as `unavailable`. When the API
|
||||
returns larger responses, raise `capture.limits`
|
||||
(`max_network_body_allocation_bytes`, `max_total_event_allocation_bytes`).
|
||||
- Request steps follow no redirects: a redirect fails the step, and
|
||||
`expected_status` cannot name a 3xx status. Use the path the redirect names,
|
||||
such as the one with the trailing slash.
|
||||
- Set `expected_status` on every step, so a refused call fails its step and
|
||||
the later steps report not having run, instead of recording read-backs of
|
||||
nothing.
|
||||
|
||||
Keep it to about 60 to 160 requests. A scenario with no `request` step is
|
||||
recorded exactly as before.
|
||||
|
||||
The capture returns its whole Evidence, every recorded body included, which is
|
||||
often more than a client shows inline. Before checking coverage, confirm that
|
||||
every entry of `normalized_result.steps` has `status: "completed"`; if the
|
||||
client saved the result to a file, search it for `"status":"failed"` and read
|
||||
that step's `error`. A failed sign-in or write cancels every step after it.
|
||||
Use the result's `evidence_id` for coverage and export; do not paste the
|
||||
Evidence back into another tool.
|
||||
|
||||
## Check coverage
|
||||
|
||||
`inspect_analysis_view` with `source: {kind: "retained-evidence",
|
||||
evidence_id}` (the capture's id) and `view: {kind: "environment-coverage",
|
||||
template_rules: ["after-uid-literal", "opaque-token", "sibling-variance",
|
||||
"prefix-variance"]}`. It reports read tools
|
||||
answered from the state or replayed (`reads.recorded_reasons`), the served write
|
||||
tools, and each withheld write with its `reason` and `hint`. It does not apply
|
||||
the serve-time catalog and state size checks; the export reports those.
|
||||
|
||||
Re-record a withheld write at most twice, and only for a recording gap. Stop at
|
||||
once when the reason is application behaviour, and report the write as
|
||||
withheld with its hint:
|
||||
|
||||
| Reason | What to do |
|
||||
| --------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `no-state-target` | Check the path template and recorded views; record missing reads. The route form may not be supported as a mutation target. |
|
||||
| `no-observation` | Re-record: read the entity back by id after the write; read it first for an update or delete. |
|
||||
| `unconfirmed` | Re-record: read the entity and its list after the write. |
|
||||
| `incomplete-entities` | Re-record: read the entity through every view that lists it. |
|
||||
| `unexplained-change` | Re-record once with a second, different call of the same write. |
|
||||
| `no-call` | Re-record once with a write that succeeds. |
|
||||
| `side-effect` | Stop: the application changes other entities. |
|
||||
| `contradicted` | Stop: a later read disagrees with the modelled effect. |
|
||||
| `body-unusable` | Re-record a UI write once with `request` steps (its body was not exposed); else stop. |
|
||||
|
||||
Use new scratch names on each attempt, and delete what an earlier attempt
|
||||
created before recording again (through the UI, or a `request` step with the
|
||||
id that capture shows): the environment starts from what the recording first
|
||||
reads, so a leftover object becomes seeded data that write tasks then find.
|
||||
New browser scenario captures include request times, so a fully timed capture
|
||||
can learn a write clock and serve updates that stamp modification times. Older
|
||||
captures or ones missing a request start keep record order and learn no clock.
|
||||
When coverage reports that limitation, record again with the current version
|
||||
before concluding that the timestamp change cannot be served.
|
||||
|
||||
## Export and hand off
|
||||
|
||||
`export_environment_bundle` with `source: {kind: "retained-evidence",
|
||||
evidence_id}` (or `{kind: "evidence-file", path}`), an absolute
|
||||
`output_directory` that does not exist yet and whose parent does, and a `name`
|
||||
(required when the application runs on `localhost` or an IP address). It returns
|
||||
the coverage again, `tool_count`, the written files and warnings; `dry_run:
|
||||
true` reports without writing. The bundle installs `rea-agents` of this REA's
|
||||
version from the registry; when the user runs an unreleased REA build, pass the
|
||||
`package_tarball` they give you (a packed `rea-agents` tarball) instead.
|
||||
|
||||
Tell the user how many tools answer from the state, which writes are served and
|
||||
which are withheld and why, and the run command (`agent-env run <folder>`). Say
|
||||
that the bundle ships the recorded responses and the capture's local paths
|
||||
(for a browser scenario, the browser executable's path), and
|
||||
list any `credential-named-members` warning pointers for review. Never publish
|
||||
or share the bundle yourself.
|
||||
|
||||
See
|
||||
[agent environments](https://github.com/morluto/rea/blob/main/docs/agent-environments.md)
|
||||
for what the state, replay and the bundle establish, and
|
||||
[evidence workflows](evidence-workflows.md) for the manifest and state views.
|
||||
@@ -11,44 +11,6 @@ returned inline. When complete `inspect_binary_layout` or
|
||||
`inspect_analysis_view` for a summary, one section/symbol/module, or a stable
|
||||
page instead of repeating the producer.
|
||||
|
||||
For any logical Evidence source that advertises retained references, pass
|
||||
`{kind: "retained-evidence", evidence_id}` instead of copying the complete
|
||||
record back into a call. The same contract covers individual, optional, array
|
||||
and nested sources across native, process, JavaScript, managed, mobile and WASM
|
||||
workflows. Sources can independently mix inline records and exact references.
|
||||
Resolution preserves the source's identity, provenance, coverage, partial facts
|
||||
and unknowns; it neither reruns analysis nor makes incomplete facts complete.
|
||||
Keep domain role and identity requirements: a native dossier cannot substitute
|
||||
for a managed member observation, for example.
|
||||
|
||||
When starting from a summary or inspection page, use its
|
||||
`normalized_result.parent_evidence_id` to reference the complete source.
|
||||
The view's own `evidence_id` names
|
||||
only that view. `compare_artifacts` explicitly supports artifact summary inputs
|
||||
and resolves their parents; do not assume every consumer does. When a workflow
|
||||
follows source links, keep those linked records available too.
|
||||
|
||||
MCP references belong to the current connection's ledger. If the ID is known,
|
||||
pass it directly; no discovery call or provider setup is needed. If it is missing,
|
||||
import the exported bundle or supply complete inline Evidence. Do not retry an
|
||||
unchanged reference or rerun a producer merely to move an existing record.
|
||||
Export before closing the session when the source must survive it. For CLI JSON
|
||||
workflows that advertise `--evidence-bundle`, the same reference JSON resolves
|
||||
against one explicitly selected authenticated bundle:
|
||||
|
||||
```bash
|
||||
rea trace-application-feature request.json --evidence-bundle /absolute/path/evidence.json --json
|
||||
```
|
||||
|
||||
This includes JavaScript traces/comparisons and runtime reconciliation, managed
|
||||
reconstruction/native verification/graph projection, Android/Apple/Harmony graph
|
||||
projection, WASM interface comparison and retained analysis views. The managed
|
||||
member comparison CLI still takes two assembly paths; process comparison takes
|
||||
two capture Evidence JSON paths. Check command help for the installed version.
|
||||
An input bundle grants no implicit file lookup: `evidence-file` and historical
|
||||
graph file selectors are accepted only by contracts advertising them. See the
|
||||
[CLI reuse guide](https://github.com/morluto/rea/blob/main/docs/cli.md#import-export-and-compare-evidence).
|
||||
|
||||
Completed `inspect_artifact` inventory also supports a summary and
|
||||
`view: {kind: "inventory", collection: "occurrences", logical_paths: ["package.json", "main.js"]}`.
|
||||
Omit `logical_paths` to page all available occurrences; `[]` selects none.
|
||||
@@ -62,25 +24,6 @@ retain its complete source and receive that view immediately. Without that
|
||||
advertised choice, keep the existing complete call. CLI inspection returns
|
||||
portable complete Evidence; save it before projecting a selected inline view.
|
||||
|
||||
For native procedure/name/string inventories, select `detail: "summary"` when
|
||||
the connected tool advertises it and the task needs counts or selected rows.
|
||||
Use its parent Evidence ID with
|
||||
`view: {kind: "native-inventory", offset: 0, limit: 32, fields: ["value"]}`.
|
||||
Search tools retain the existing pattern/mode/case/document selection. Omit
|
||||
`fields` for complete records; `record.address` and `source_index` remain on
|
||||
selected rows. Selecting `value` keeps its string/decoding qualifications.
|
||||
Parent Evidence ID plus source index preserves same-address
|
||||
alternatives. Follow `coverage.next_offset` only as far as the question needs;
|
||||
exhausting retained rows does not establish complete program discovery.
|
||||
|
||||
`batch_decompile` returns every body unless you select `detail: "summary"` when
|
||||
the connected tool advertises it. Use its parent Evidence ID with
|
||||
`view: {kind: "batch-decompile", collection: "items"}` for body-free rows and
|
||||
exact `where.indices`, `addresses`, `procedure_addresses` or `statuses`. Add
|
||||
`include_pseudocode: true` for several chosen bodies, or use
|
||||
`collection: "pseudocode"` with one row's `index` to page a large body. Batch
|
||||
limitations aggregate every item, so do not attribute them to one successful row.
|
||||
|
||||
Use `record_unknown` to track unresolved questions in the user's investigation
|
||||
and name the authority or environment still required. Supply supporting and
|
||||
contradicting evidence IDs. It updates the session registry directly.
|
||||
@@ -92,22 +35,16 @@ Use comparisons with complete, compatible page sets when claiming equivalence
|
||||
or absence:
|
||||
|
||||
- `compare_artifacts` compares inventory pages by occurrence path, content,
|
||||
metadata, and graph relations. Name each side as
|
||||
`{kind: "retained-evidence", evidence_id}` (an `inspect_artifact` result,
|
||||
its nested inventory or its summary) or
|
||||
`{kind: "evidence-file", path, evidence_id?}` instead of resending Evidence.
|
||||
metadata, and graph relations.
|
||||
- `compare_functions` compares explicit function Evidence, not fuzzy
|
||||
whole-binary matches. It rejects an `analyze_function` summary reply; pass
|
||||
the summary's `parent_evidence_id` as a `retained-evidence` reference or
|
||||
the complete dossier.
|
||||
whole-binary matches.
|
||||
- `compare_bundles` compares canonical bundle membership and unknown history.
|
||||
- `find_changed_behavior` combines existing comparisons; static differences
|
||||
remain candidates, not causal proof. Its `comparisons` accept exact references.
|
||||
remain candidates, not causal proof.
|
||||
- `build_call_path` needs explicit function dossiers from one artifact/provider;
|
||||
pass complete parent IDs in `functions`. An incomplete frontier makes absence
|
||||
unknown.
|
||||
an incomplete frontier makes absence unknown.
|
||||
- `correlate_static_and_runtime` uses explicit mappings; matching patterns are
|
||||
hypotheses, not causality. Both comparison arrays accept exact references.
|
||||
hypotheses, not causality.
|
||||
- `verify_reconstruction` evaluates a finite typed specification. A pass covers
|
||||
only declared comparable claims, not global equivalence.
|
||||
|
||||
@@ -132,45 +69,3 @@ When the connected schema advertises `include_query_parameter_names`, select
|
||||
it in the view for distinct observed names with original record ordinals. Query
|
||||
values are omitted; the names establish neither required parameters nor a
|
||||
complete server schema. Keep the original parent Evidence for exact URLs.
|
||||
|
||||
Use `inspect_analysis_view` with `view: {kind: "environment-manifest"}` on the
|
||||
same retained HAR Evidence to project the primary origin's request lines into
|
||||
named tools with path templates, parameters, observed query names, value-free
|
||||
body shapes and record pointers for an agent environment. Opt into
|
||||
`template_rules` only when recorded identifiers need them; every template,
|
||||
parameter and merged sibling is an inference, no header, cookie or body value is
|
||||
copied, and the three stated unknowns stay unresolved.
|
||||
|
||||
Use `view: {kind: "environment-state"}` with the manifest's `template_rules` to
|
||||
extract the data those reads observed instead: entity types and identity
|
||||
fields, entities with recorded values, documents, and per-tool render plans
|
||||
checked against the recordings. The state comes only from reads before the
|
||||
first related write; types, identity and list membership are inferences. The
|
||||
view also gives each state-changing tool a write plan, served only when a later
|
||||
recorded read confirms its effect and none contradicts it.
|
||||
`rea environment-serve manifest.json --evidence capture.json` derives that
|
||||
state at startup, answers read tools from it, serves the write tools whose plan
|
||||
is served, each applying its effect to that state, and serves `data/reset`,
|
||||
`data/add` and `data/get` on `/agentenv`; other write tools are not served, and
|
||||
each plan states why in its `reason`. It replays the capture instead with
|
||||
`--replay-only` or when the state is unavailable (not derivable, or a catalog
|
||||
over the discovery budget), and its stopped document's `state` says which
|
||||
applied.
|
||||
Use `view: {kind: "environment-coverage"}` with the same rules to see what an
|
||||
environment built from a capture would serve: read tools answered from the
|
||||
state or replayed with their reason, and each write tool served or withheld
|
||||
with a hint about what to record next; it does not apply the serve-time
|
||||
catalog and state size checks. `rea environment-build <capture.har> <dir>`
|
||||
inspects, projects and exports in one step and prints the same coverage, or
|
||||
`null` when the bundle serves replay.
|
||||
|
||||
For a PE/ELF export question, `inspect_native_exports` accepts `detail: "summary"`
|
||||
and retains the complete table. Query `inspect_analysis_view` with
|
||||
`view: {kind: "native-exports", collection: "exports", names: ["selected_name"],
|
||||
fields: ["name"], offset: 0, limit: 32}` and the reported parent Evidence ID.
|
||||
All same-name versions and aliases survive; use `source_index` with the parent ID
|
||||
for stable identity. `collection: "issues"` pages interpretation failures. Page
|
||||
coverage describes selected retained rows; `summary.coverage` or `exports.coverage`
|
||||
still describes the source table, so an empty partial page does not prove absence.
|
||||
CLI `inspect-native-exports --detail summary --evidence_output PATH` saves the
|
||||
complete bundle for later `inspect-analysis-view --evidence-bundle PATH` requests.
|
||||
@@ -17,21 +17,10 @@ the complete artifact inventory and identity but reads source text and analyzes
|
||||
only selected files. The result and summary record `source_selection`; files
|
||||
outside scope and their relationships remain unknown, so partial application
|
||||
coverage does not mean the selected source failed. Use repeated `--source-path`
|
||||
on either static application CLI route, or `--source-paths-json` with a JSON
|
||||
file for selections too long for one command line. Omit the selection when the
|
||||
question requires complete application analysis; `source_paths: []` is invalid,
|
||||
not an unfiltered request. `detail: "summary"` reduces delivery
|
||||
on either static application CLI route. Omit the selection when the question
|
||||
requires complete application analysis. `detail: "summary"` reduces delivery
|
||||
and does not by itself reduce analysis work.
|
||||
|
||||
ASAR header/index admission defaults to 16 MiB / 16777216 bytes;
|
||||
an admission error names `asar.max_header_bytes` and its bounds (at most
|
||||
256 MiB). Call `update_analysis_settings` with that setting, `action: "set"`
|
||||
and enough bytes, then retry the same full-archive request. No inspection or
|
||||
REA restart is required. `REA_ASAR_MAX_HEADER_BYTES` seeds future REA processes.
|
||||
`source_paths`, `detail`, and `max_heap_mb` do not bypass or raise header admission.
|
||||
Runtime JavaScript timeout/heap defaults are also editable; explicit per-call
|
||||
`analysis_timeout_ms` and `max_heap_mb` take precedence.
|
||||
|
||||
Exact `#alias` specifiers resolve through the importer's
|
||||
enclosing package `imports` map to package-local files; nested `node_modules`
|
||||
packages use their own map. Wildcard, recursive, and external-package import
|
||||
@@ -46,9 +35,7 @@ the complete application Evidence inline when it fits.
|
||||
When the connected schema advertises `max_heap_mb` and `analysis_timeout_ms`,
|
||||
these select the isolated source analyzer's old-space heap and per-source
|
||||
deadline. Defaults are 1024 MiB and 300000 ms. They do not change the MCP
|
||||
process heap or the client's tool deadline. For a host timeout without REA
|
||||
output, use the [deadline recovery guide](connection-and-recovery.md#distinguish-client-and-server-deadlines)
|
||||
instead of raising the server budget. A Bun-hosted REA does not enforce the heap choice; on Windows it
|
||||
process heap. A Bun-hosted REA does not enforce the heap choice; on Windows it
|
||||
instead limits the analyzer's committed memory to that choice plus a 512 MiB
|
||||
runtime reserve.
|
||||
Resource-limited analyses preserve completed static facts and
|
||||
@@ -64,29 +51,6 @@ in the same session. A returned `error.details.partial_observation` reference
|
||||
can be supplied directly to `inspect_analysis_view`. If the SDK cancels locally, retrieve its recorded ID from
|
||||
the Evidence bundle before inspecting the partial analysis.
|
||||
|
||||
## Inventory route candidates without returning the application graph
|
||||
|
||||
When the advertised schema supports `view`, use `detail: "summary"` with
|
||||
`view: {kind: "javascript-routes", collection: "routes", fields: ["path", "confidence"], limit: 20}`.
|
||||
The same view works with `analyze_web_bundle` on already selected captured sources.
|
||||
The complete source Evidence is retained; use its `parent_evidence_id` with
|
||||
`inspect_analysis_view` for `summary`, `routes`, `occurrences`, `sources`, or
|
||||
`unknowns`. Filter and batch IDs before requesting more fields, and follow
|
||||
`coverage.next_offset`. CLI selections require `--evidence_output` to keep the
|
||||
complete record. Static patterns, inferred deployment bases, heuristic system
|
||||
classification, and unavailable sources never establish route reachability or
|
||||
the complete application route denominator. See the
|
||||
[route inventory guide](https://github.com/morluto/rea/blob/main/docs/javascript-route-inventory.md).
|
||||
|
||||
For captured endpoint observations and inference facts, `analyze_web_bundle`
|
||||
also accepts `view: {kind: "web-bundle", collection: "endpoints", limit: 32}`
|
||||
with `detail: "summary"`. Read later `endpoints`, `inferences`, or `summary`
|
||||
pages from the retained parent. Filter before requesting detail; retain
|
||||
`source_index` together with parent ID and collection, and request `location`
|
||||
or `basis` when tracing sources. Page exhaustion does not mean producer
|
||||
completeness: check `completeness`, structured `source_unknowns`, and limitations.
|
||||
Endpoint candidates remain inferences rather than confirmed network behavior.
|
||||
|
||||
## Read a module without repeating analysis
|
||||
|
||||
Use the actual returned parent ID, on the same connection, as `source.evidence_id`.
|
||||
@@ -169,10 +133,7 @@ leaving it, and `both` when the question needs both. Trace traversal includes
|
||||
containment relationships, so even a narrow seed can expand many nodes; a
|
||||
bidirectional trace can reach unrelated sibling modules. A module item or page
|
||||
is preferable when its observations already answer the question. Do not treat
|
||||
all reached nodes as participants in the feature. For a broad trace, pass
|
||||
`view: {"kind": "feature-trace", "collection": "paths"}` (or `summary`) to
|
||||
retain the complete trace and read terminal paths first; page `nodes` or `edges`
|
||||
with exact `where.node_ids` through `inspect_analysis_view` as needed.
|
||||
all reached nodes as participants in the feature.
|
||||
|
||||
Reuse retained parent Evidence when the server advertises it, or supply complete
|
||||
Evidence inline. Include Hopper or Ghidra Evidence only when its artifact digest
|
||||
@@ -234,33 +195,11 @@ accept complete inline Evidence or
|
||||
`{"kind":"retained-evidence","evidence_id":"ev_<64 lowercase hex characters>"}`
|
||||
for their application input (`application`, or `left`/`right`). `inspect_analysis_view`
|
||||
uses the same retained-reference form in `source`, or portable inline Evidence,
|
||||
to project a summary, module page, or one module with its recorded observations.
|
||||
This notation is
|
||||
to project a summary, module page, or one module with its recorded observations. This notation is
|
||||
a template: replace it with the actual returned ID. Versions before 4.1.0 accept only full
|
||||
inline Evidence. Use the exact ID
|
||||
returned by the producer on the same MCP connection. Resolution does not run
|
||||
analysis or select a provider; findings remain inline. For summary or inspection
|
||||
replies, select the complete `normalized_result.parent_evidence_id`. Native
|
||||
observation arrays and runtime reconciliation's `static_layers[].analysis` and
|
||||
`runtime_observations[]` also accept exact references where advertised. Each
|
||||
source can independently be inline or retained, preserving identity, coverage
|
||||
and unknowns. CLI workflows advertising `--evidence-bundle` resolve these inputs
|
||||
from the explicitly selected bundle. `close_binary` releases the engine while
|
||||
retaining references and complete Unknown history on the same MCP connection.
|
||||
`reset_investigation` explicitly clears them; optional `save_path` checkpoints the
|
||||
complete bundle before clearing. Import that bundle in another connection to
|
||||
restore its original references. Retention is bounded, in-memory and never silently
|
||||
evicted; `binary_session.investigation` reports the budget. A missing reference
|
||||
includes its ID and recovery guidance.
|
||||
|
||||
For exact file/native metadata or processing gaps, use
|
||||
`view: {kind: "javascript-facts", where: {paths: ["package.json"]}, property_keys: ["name", "operation", "analysis_status"], limit: 32}`.
|
||||
Rows retain canonical node/observation identity and original Evidence. Paths
|
||||
match Evidence artifact-path/source-range locations; missing properties remain
|
||||
omitted and source coverage stays separate from page exhaustion. Batch paths and
|
||||
select only needed property keys; complete Evidence remains accessible by export.
|
||||
If names are unknown, opt into `include_available_property_keys: true`; it lists
|
||||
the original keys per returned observation without unselected values. Keep each
|
||||
row's node/observation identity paired when composing facts.
|
||||
These rows select application-graph observations; semantic relationships and
|
||||
semantic unknown records remain in the complete parent Evidence and trace tools.
|
||||
analysis or select a provider; findings remain inline. Native observation
|
||||
arrays still take full Evidence. `close_binary` clears retained references;
|
||||
export a bundle before closing, or supply portable inline Evidence in another
|
||||
connection. A missing reference includes its ID and recovery guidance.
|
||||
@@ -1,71 +1,8 @@
|
||||
# Native, managed, and packaged artifacts
|
||||
|
||||
For release comparisons on one MCP connection, retain `list_procedures` Evidence
|
||||
from each target and reuse its exact IDs after target switches or `close_binary`.
|
||||
For a handoff or new process, export the complete bundle and import it there,
|
||||
including its source closure. `reset_investigation` explicitly clears retained
|
||||
records; optional `save_path` checkpoints them before clearing.
|
||||
`match_native_functions` pairs unique exact names and, when supplied,
|
||||
complete observed body bytes. Reuse optional same-context `list_names`,
|
||||
`procedure_info`, `read_bytes` and existing `analyze_function` dossiers as
|
||||
`left_observations`/`right_observations`; an MCP `analyze_function` summary
|
||||
reply contributes its `parent_evidence_id` as a `retained-evidence` reference,
|
||||
not the summary itself. Avoid decompiling every function merely to match it. Its summary retains a parent for
|
||||
`inspect_analysis_view` `function-matches` collections `matches` and `functions`.
|
||||
Select columns, batch IDs and page individual facts; aliases remain unresolved.
|
||||
Unpaired observations do not prove additions/removals. Existing matched dossier
|
||||
pairs include inputs for `compare_functions`; otherwise analyze just chosen pairs.
|
||||
|
||||
Aggregate completed comparisons with `find_changed_behavior`, selecting each
|
||||
comparison's exact `retained-evidence` ID instead of resending full records.
|
||||
Inline comparison Evidence remains accepted. References belong to the current
|
||||
server: after handoff, import the original bundle including its source closure.
|
||||
Do not substitute dossier IDs or summary views. Static differences remain
|
||||
behavior candidates, with runtime behavior unknown unless separately observed.
|
||||
|
||||
For a matching representation refusal, inspect `details.limit` and raise
|
||||
`native.matching_representation_bytes` with `update_analysis_settings` before
|
||||
retrying the unchanged MCP request. CLI launch seed:
|
||||
`REA_NATIVE_MATCHING_REPRESENTATION_BYTES`. The default is 96 MiB; the policy
|
||||
range is 1 byte to 1 GiB and does not bound process RSS. Keep the complete
|
||||
inventories: splitting them changes global name/body uniqueness.
|
||||
|
||||
## Apple app resources
|
||||
|
||||
Use `open_bundle_resources` with the selected app path, then `inspect_plist`,
|
||||
`decode_interface_builder`, `inspect_asset_catalog`, or `inspect_keyed_archive`.
|
||||
The resource session reads the app without resolving `CFBundleExecutable` or
|
||||
starting a deep provider. Preserve `resource_root` provenance, per-resource
|
||||
hashes where reported, and null executable identity; a directory path is not
|
||||
an executable digest. Export retained Evidence before closing or switching
|
||||
roots. Binary analysis snapshots do not apply. Use `open_binary` to switch to
|
||||
executable analysis, and `close_binary` to release either session kind.
|
||||
|
||||
The four matching CLI commands accept the app path directly. For an archive
|
||||
inside the app, use `inspect-keyed-archive APP_PATH RELATIVE_ARCHIVE_PATH`.
|
||||
Host requirements for plist and compiled asset/Interface Builder tooling still
|
||||
apply; a resource-only session does not establish provider availability.
|
||||
|
||||
## Desktop installation directories
|
||||
|
||||
When an installation directory is supplied and its runtime is unknown, use
|
||||
`inspect_desktop_application` with its absolute path. Default MCP summary
|
||||
delivery retains complete Evidence. Read `files`, `omissions`, `candidates`, `components` or
|
||||
`next_probes` with `inspect_analysis_view` kind `desktop-application`, selecting
|
||||
exact paths, batched file/omission IDs or families and only needed fields and pages.
|
||||
Summary `probe_issues` retains counts and bounded previews. Select files with
|
||||
`has_issues: true` and fields `path`, `read_status`, `issues` to retrieve exact
|
||||
failures directly; preview text is display data, while `file_id` selects full facts. Preserve source
|
||||
coverage, read-range identity and unknowns; directory snapshot identity is not
|
||||
a digest of every file's contents. Component path markers do not establish
|
||||
runtime presence. For compact CLI output, use
|
||||
`inspect-desktop-application PATH --detail summary --evidence_output BUNDLE`.
|
||||
See [desktop installations](https://github.com/morluto/rea/blob/main/docs/desktop-applications.md).
|
||||
|
||||
## Go build metadata
|
||||
|
||||
Use `inspect_go_binary` for an explicit local ELF, PE or Mach-O (universal
|
||||
files need `architecture`) when the
|
||||
Use `inspect_go_binary` for an explicit local ELF, PE or thin Mach-O when the
|
||||
question concerns the embedded compiler version, module/dependency versions,
|
||||
local replacements or build settings. No `open_binary`, Go installation or
|
||||
target execution is required. The CLI equivalent is `inspect-go-binary PATH`.
|
||||
@@ -74,38 +11,9 @@ unparsed module-text line. A missing record is an unknown, not proof of a
|
||||
non-Go binary; observed metadata does not authenticate its claims. Function,
|
||||
source and type recovery remain a separate native-analysis question.
|
||||
|
||||
## Model files
|
||||
|
||||
Use `inspect_model_artifact` (`inspect-model-artifact PATH`) for local GGUF v2/v3
|
||||
and safetensors declarations without a model runtime. Header-only mode does not
|
||||
read weights or establish full-file identity. `hash: "full"` explicitly streams
|
||||
all bytes. Retained `model-artifact` views page `metadata`, `tensors` and derived
|
||||
`types`, with exact batch filters and selected fields. Arrays retain typed counts
|
||||
and eight-element previews; omitted values, unknown types and runtime behavior
|
||||
stay explicit. See [model files](https://github.com/morluto/rea/blob/main/docs/model-artifacts.md).
|
||||
|
||||
## IL2CPP metadata
|
||||
|
||||
Use `inspect_il2cpp_metadata` for an explicit local Unity `global-metadata.dat`
|
||||
(versions 24, 27, 29, 31 and 39) when the question concerns assembly images,
|
||||
names, member owners, string literals, namespaces or index ranges. No `open_binary`, Unity installation or target
|
||||
execution is required. The CLI equivalent is `inspect-il2cpp-metadata PATH`.
|
||||
MCP defaults to counts with complete retained Evidence. Query `sections`,
|
||||
`images`, `types`, `methods`, `fields`, `properties`, `events`, `parameters` or
|
||||
`string_literals` through `inspect_analysis_view`; use exact names,
|
||||
batch indices and selected fields. Row indices are scoped by artifact SHA-256
|
||||
and collection; page offsets count filtered matches. Parameters support batch
|
||||
`method_indices` to read one method's parameters directly. Historical reports
|
||||
without member collections retain unknown counts rather than empty inventories. Preserve
|
||||
artifact SHA-256 and index widths. Unrecognized magic may mean encryption and
|
||||
is unsupported, not decrypted. Linking names to `GameAssembly` or
|
||||
`UnityFramework` code addresses remains a separate native-analysis question.
|
||||
See [IL2CPP metadata](https://github.com/morluto/rea/blob/main/docs/il2cpp-metadata.md).
|
||||
|
||||
## Rust dependency metadata
|
||||
|
||||
Use `inspect_rust_binary` for an explicit local ELF, PE, or Mach-O (universal
|
||||
files need `architecture`) when
|
||||
Use `inspect_rust_binary` for an explicit local ELF, PE, or thin Mach-O when
|
||||
the question concerns the cargo-auditable crate list embedded in `.dep-v0`
|
||||
(Mach-O: `__DATA,.dep-v0`). No `open_binary`, Rust installation, or target
|
||||
execution is required. The CLI equivalent is `inspect-rust-binary PATH`.
|
||||
@@ -115,112 +23,6 @@ dependencies. An omitted package kind stays null. A rustc commit hash is
|
||||
inferred from a read-only `/rustc/<40-hex>/` path and does not by itself
|
||||
identify the compiler. See [Rust binaries](https://github.com/morluto/rea/blob/main/docs/rust-binaries.md).
|
||||
|
||||
## PE Authenticode certificate tables
|
||||
|
||||
Use `inspect_pe_certificates` for an explicit local PE32/PE32+ image when the
|
||||
question concerns the Authenticode certificate table, WIN_CERTIFICATE types, or
|
||||
embedded PKCS#7 signer and X.509 identity fields. No `open_binary`, Windows SDK,
|
||||
or target execution is required. The CLI equivalent is
|
||||
`inspect-pe-certificates PATH`. Preserve artifact SHA-256, table file ranges,
|
||||
payload hashes, original validity-time spellings, and
|
||||
`subject_key_identifier_hex` when the signer uses that form. A missing
|
||||
`IMAGE_DIRECTORY_ENTRY_SECURITY` directory is observed absence of that table,
|
||||
not proof the image is unsigned in every catalog or overlay sense. Signatures,
|
||||
chains, timestamps, and Authenticode digest coverage remain unobserved. See
|
||||
[PE Authenticode certificate tables](https://github.com/morluto/rea/blob/main/docs/pe-certificates.md).
|
||||
|
||||
## PE import and delay-load tables
|
||||
|
||||
Use `inspect_pe_imports` for an explicit local PE32/PE32+ image when the
|
||||
question concerns imported DLL and function names, ordinals, delay-load
|
||||
descriptors, or the pefile import hash. No `open_binary`, Windows SDK, or
|
||||
target execution is required. The CLI equivalent is `inspect-pe-imports PATH`.
|
||||
Preserve artifact SHA-256, directory file ranges, original DLL spellings, and
|
||||
ILT/IAT RVAs. A missing `IMAGE_DIRECTORY_ENTRY_IMPORT` directory is observed
|
||||
absence of that table, not proof the image never imported APIs through another
|
||||
table or overlay. Bound snapshots, loaded IAT contents, and export-forward
|
||||
resolution remain unobserved. See
|
||||
[PE import and delay-load tables](https://github.com/morluto/rea/blob/main/docs/pe-imports.md).
|
||||
|
||||
## PE TLS callbacks and load configuration
|
||||
|
||||
Use `inspect_pe_load_config` for an explicit local PE32/PE32+ image when the
|
||||
question concerns code that runs before the entry point through TLS callbacks,
|
||||
or the security cookie, SafeSEH, CFG, EH-continuation and XFG declarations in
|
||||
the load configuration. No `open_binary`, Windows SDK, or target execution is
|
||||
required. The CLI equivalent is `inspect-pe-load-config PATH`. Preserve artifact
|
||||
SHA-256, directory and callback-array file ranges, and callback VAs as
|
||||
hexadecimal strings. Fields outside the declared `Size` are `null`. Guard-flag
|
||||
mitigations carry `evidence_kind: "inferred"`; enforcement remains unobserved.
|
||||
See
|
||||
[PE TLS callbacks and load configuration](https://github.com/morluto/rea/blob/main/docs/pe-load-config.md).
|
||||
|
||||
## ELF note tables
|
||||
|
||||
Use `inspect_elf_notes` for an explicit local ELF32/ELF64 image when the
|
||||
question concerns PT_NOTE/SHT_NOTE owners, GNU build-id, or GNU ABI-tag
|
||||
fields. No `open_binary` or target execution is required. The CLI equivalent
|
||||
is `inspect-elf-notes PATH`. Preserve artifact SHA-256, table file ranges,
|
||||
descriptor hashes, and original owner spellings. A missing table is observed
|
||||
absence of those declarations, not proof the image never carried notes in
|
||||
another container. See
|
||||
[ELF note tables](https://github.com/morluto/rea/blob/main/docs/elf-notes.md).
|
||||
|
||||
## Apple IPS crash reports
|
||||
|
||||
Use `inspect_apple_ips` for an explicit local Apple `.ips` file when the
|
||||
question concerns crash metadata, exception and termination facts, threads,
|
||||
usedImages, lossless registers, or documented crash classes. No `open_binary`,
|
||||
Xcode, atos, or target execution is required. The CLI equivalent is
|
||||
`inspect-apple-ips PATH`. Preserve artifact SHA-256, `bug_type` as a string,
|
||||
uppercase image UUIDs, and integer literals that exceed IEEE-754 range. Only
|
||||
`bug_type` `"309"` yields a crash body; other DiagnosticReports types return
|
||||
no crash result. Opened Mach-O and dSYM UUID joins remain unobserved. See
|
||||
[Apple IPS crash reports](https://github.com/morluto/rea/blob/main/docs/apple-ips.md).
|
||||
|
||||
## Linux kernel modules
|
||||
|
||||
Use `inspect_kernel_module` for an explicit local ELF32/ELF64 relocatable
|
||||
object when the question concerns `.modinfo` license, vermagic, alias,
|
||||
depends, or parm strings, the `__versions` symbol CRCs, or an appended module
|
||||
signature trailer. No `open_binary`, Linux host, or `modinfo` program is
|
||||
required. The CLI equivalent is `inspect-kernel-module PATH`. Preserve
|
||||
artifact SHA-256, key order, duplicate keys, CRC hex, and section file ranges.
|
||||
A missing section is observed absence when section names are known, not proof
|
||||
the module never carried that metadata. `.gnu.linkonce.this_module` is reported
|
||||
as present or absent; `struct module` stays unknown because its layout changes
|
||||
with the kernel version. `__ksymtab` records are not decoded. The PKCS#7 range
|
||||
and its SHA-256 are observed; certificate, signer, and digest coverage remain
|
||||
unvalidated. An absent trailer is not proof the module is unsigned in another
|
||||
container. gzip, xz, and zstd whole-file containers are unsupported and name
|
||||
`external_decompression` as the next probe: use an external codec utility to
|
||||
decompress into a separate file, then call `inspect_kernel_module` on that file. See
|
||||
[Linux kernel modules](https://github.com/morluto/rea/blob/main/docs/kernel-modules.md).
|
||||
|
||||
## CPython pyc files
|
||||
|
||||
Use `inspect_python_pyc` for an explicit local CPython 3.7–3.13 `.pyc` when the
|
||||
question concerns the header magic, PEP 552 source hash or timestamp, nested
|
||||
code-object names, or bytecode identity. No `open_binary`, Python installation,
|
||||
or target execution is required. The CLI equivalent is `inspect-python-pyc PATH`.
|
||||
Preserve artifact SHA-256, the 16-byte header fields, nested code names, and
|
||||
bytecode SHA-256. Bytecode is not decompiled. A decoded header does not prove
|
||||
the matching `.py` is present. See [CPython pyc inspection](https://github.com/morluto/rea/blob/main/docs/python-pyc.md).
|
||||
|
||||
## Erlang BEAM files
|
||||
|
||||
Use `inspect_erlang_beam` for an explicit local `FOR1`/`BEAM` file when the
|
||||
question concerns IFF chunks, atom names, exports, imports, lambdas, or the
|
||||
Code header. No `open_binary`, Erlang installation, or bytecode execution is
|
||||
required. The CLI equivalent is `inspect-erlang-beam PATH`. Preserve artifact
|
||||
SHA-256, chunk file ranges, 1-based atom indices, and original atom spellings.
|
||||
Missing optional tables stay null. For a few facts, use `detail: "summary"` with
|
||||
a `beam` view selecting a collection, exact names/indices and fields; read later
|
||||
pages from `parent_evidence_id` or the complete Evidence bundle. Same-name
|
||||
exports retain every arity and original atom index. CLI summary delivery requires
|
||||
`--evidence_output`. Null table counts remain distinct from decoded empty tables. See
|
||||
[Erlang BEAM files](https://github.com/morluto/rea/blob/main/docs/erlang-beam.md).
|
||||
|
||||
## SQLite snapshots
|
||||
|
||||
Use `inspect_sqlite_database` with an explicit local database snapshot for its
|
||||
@@ -275,30 +77,6 @@ and tool profile. Text offsets count UTF-16 code units; follow `next_offset`.
|
||||
The complete source remains retained after an oversized MCP reply. No target
|
||||
execution or network acquisition is implied, and WAT is decoded representation.
|
||||
|
||||
## Offline APK signatures
|
||||
|
||||
Use `inspect_apk_signature` for a selected local APK/ZIP/AAB. Default MCP summaries
|
||||
retain complete Evidence. Select `view.collection: signers/digests/lineage`,
|
||||
with scheme or ID batches, fields and paging; use `inspect_analysis_view` kind
|
||||
`apk-signature` for retained reads after deletion. IDs retain artifact/scheme
|
||||
scope and signer links; `fields: []` keeps identity links. Empty pages preserve
|
||||
partial decoding reasons and unknown authenticity. CLI compact delivery needs
|
||||
`--evidence_output` to preserve the complete parent.
|
||||
It reads the APK
|
||||
Signing Block (v2/v3/v3.1) and v1 `META-INF` signing without executing anything,
|
||||
and reports each signer's certificate facts (for v1, the certificate the PKCS#7
|
||||
block's SignerInfo names), the declared v3 rotation lineage,
|
||||
the declared source-stamp certificate and, for SHA-256/SHA-512 digests, whether
|
||||
the declared content digest matches the current bytes. That match is not a
|
||||
signature verification: anyone who changes the bytes can recompute the digest.
|
||||
`posture` summarises v1-only signing (which cannot tell a v1-only APK from one
|
||||
whose v2/v3 block was removed), observed signer agreement across schemes,
|
||||
whether the v1 signer equals the block-scheme signer (`false` is notable),
|
||||
whether every differing signer appears in a declared (unverified) rotation
|
||||
lineage, and the strongest scheme.
|
||||
It never claims authenticity: no signature or lineage is cryptographically
|
||||
verified.
|
||||
|
||||
## Native targets
|
||||
|
||||
After `open_binary`, use focused search, procedure, or function tools directly.
|
||||
@@ -311,9 +89,6 @@ unknown rather than false.
|
||||
For a completed `analyze_function` dossier, use `inspect_analysis_view` with
|
||||
`view: {kind: "native", facet: "value_flow_operations", offset: 0, limit: 32}`
|
||||
and the exact retained Evidence ID to read selected facts without reanalysis.
|
||||
The default MCP reply is already a summary of that dossier: its
|
||||
`normalized_result.parent_evidence_id` is the retained ID to page, and its
|
||||
null counts mark facets the provider did not record or declared unavailable.
|
||||
Native facets also include procedure, pseudocode, assembly, basic blocks,
|
||||
comments, callers/callees, references, unresolved calls, referenced strings and
|
||||
names, the native API record, and value-flow summaries. Pseudocode offsets and limits count UTF-16
|
||||
@@ -333,142 +108,22 @@ run, use `observe_native_calls` with explicit breakpoints and a bounded
|
||||
`duration_ms`/`max_events`. It launches a new process under LLDB, so the target
|
||||
runs and may change files or use the network. Hardened-runtime targets need the
|
||||
`get-task-allow` entitlement.
|
||||
Function breakpoints use exact LLDB runtime names: pass an `inspect_macho` export
|
||||
or import `runtime_name` (`main`), not its symbol-table `name` (`_main`). REA does
|
||||
not translate breakpoint names. Set `module` to the image
|
||||
file name when selecting that image only; otherwise loaded system images also
|
||||
match. Inspect retained locations and module paths before attributing a call.
|
||||
|
||||
Use `binary_session` with no arguments to check the open target, selected
|
||||
provider, and alignment. Its default summary `result.tool_availability` carries
|
||||
the complete tool inventory grouped by current status. Expand each group's
|
||||
`names` and inherit the inventory-level client requirements unless a group
|
||||
overrides them; availability, reasons, remediation, and modes remain inline.
|
||||
Provider candidates keep availability and target-support status, code, and
|
||||
reason. `omitted_fields` names what the summary leaves out; `{"detail":
|
||||
"complete"}` adds each unavailable candidate's `remediation`, provider
|
||||
diagnostics, candidate capability sets, and tool surface, `effects`, and
|
||||
`annotations`. Those arrays name true flags from fixed
|
||||
vocabularies, so an omitted known flag is false. `tools/list` remains the
|
||||
canonical source for full tool schemas when target or provider state changes.
|
||||
provider, and alignment. Its default `result.tool_availability` carries a
|
||||
complete inventory grouped by shared facts. Expand each group's `names` and
|
||||
inherit the inventory-level client requirements unless a group overrides them;
|
||||
availability, reasons, remediation, modes, and flags remain inline. The
|
||||
`effects` and `annotations` arrays name true flags from fixed vocabularies, so an
|
||||
omitted known flag is false. `tools/list` remains the canonical source for full
|
||||
tool schemas when target or provider state changes.
|
||||
|
||||
Provider capability groups carry provider identity and ordered limitations.
|
||||
Prepend each set's `shared_limitations` to a group's `limitations` to recover the
|
||||
operation's full ordered list. Candidate capabilities, returned with complete
|
||||
detail, describe possible providers. Top-level session capabilities describe the current route, and
|
||||
operation's full ordered list. Candidate capabilities describe possible
|
||||
providers. Top-level session capabilities describe the current route, and
|
||||
`analysis_provider_binding` identifies the selected provider.
|
||||
|
||||
## Reuse caller-selected Ghidra analysis
|
||||
|
||||
When the caller supplies an expanded project, configure `REA_GHIDRA_PROJECT_PATH`
|
||||
(the `.gpr` marker) and optionally `REA_GHIDRA_PROGRAM_NAME` (one root program),
|
||||
then restart/reconnect before `open_binary`. CLI uses the same configuration.
|
||||
REA opens only a fingerprint-bound owned copy with `-process -noanalysis -readOnly`;
|
||||
original project files are not opened by the engine or saved. Database edits,
|
||||
import overrides and repair scripts are unavailable in this mode. Treat original
|
||||
analysis completeness/settings as unknown and prior edits as caller-project state.
|
||||
The program's recorded import digest must match the target; changed project content
|
||||
requires reopening for a fresh profile. See the
|
||||
[setup and preparation guide](https://github.com/morluto/rea/blob/main/docs/installation.md#reuse-an-existing-ghidra-project).
|
||||
|
||||
## Ghidra startup deadline recovery
|
||||
|
||||
`provider_timeout` during the first Ghidra query can cover import, auto-analysis,
|
||||
bridge startup, and readiness. The default 330,000 ms is configurable through
|
||||
`REA_GHIDRA_STARTUP_TIMEOUT_MS`; a smaller query still needs the same cold import.
|
||||
|
||||
When the error names `ghidra.startup_timeout_ms`, call
|
||||
`update_analysis_settings` with `action: "set"` and a larger value within the
|
||||
reported bounds. Resolve retained cleanup before the next startup attempt.
|
||||
No REA restart is needed; an already accepted startup keeps its original
|
||||
budget. A new attempt repeats import. Preserve useful Evidence before closing
|
||||
and reopening the target when remediation requires that lifecycle step.
|
||||
|
||||
For a per-function timeout, update `ghidra.decompile_timeout_seconds` and retry
|
||||
the same function on the existing Program. Explicit `null` removes that
|
||||
per-function deadline. Inspection is optional when the error supplies the
|
||||
setting and bounds. Runtime updates disappear when REA exits; environment
|
||||
changes only seed future REA processes.
|
||||
|
||||
The client's tool-call deadline must allow startup plus the requested query;
|
||||
server settings and progress notifications cannot override it. CLI analysis
|
||||
avoids the MCP client deadline, but retains the provider deadline. For example:
|
||||
|
||||
```sh
|
||||
REA_GHIDRA_STARTUP_TIMEOUT_MS=1800000 rea analyze /absolute/path/to/app.exe --provider ghidra --json
|
||||
```
|
||||
|
||||
Thirty minutes is an example budget, not a completion guarantee. Keep host CPU
|
||||
and memory controls in place. Disable specific expensive analyzers only when
|
||||
their missing coverage is acceptable and retain the resulting limitations.
|
||||
Exported C and embedded resources remain useful independent evidence, but a
|
||||
timeout does not establish complete analysis or absence of code. See the
|
||||
[deadline and client guide](https://github.com/morluto/rea/blob/main/docs/mcp-contracts.md#ghidra-first-query-deadlines-and-recovery).
|
||||
|
||||
## Bridge request ceiling
|
||||
|
||||
Hopper and Cutter share `REA_BRIDGE_MAX_REQUEST_BYTES`. Unset, both sides use
|
||||
16777216 bytes. The payload budget excludes the terminating LF. A decimal
|
||||
integer from 7464 to 536870887 raises or lowers that ceiling. The floor fits
|
||||
the largest bounded symbol-name command. A tight Cutter status still returns
|
||||
its session id and up to 64 completion receipts. Its path is included when it
|
||||
fits, and is unknown when it does not, so an older descriptor path is not
|
||||
paired with the new generation. The maximum is the encoded payload, one byte under Node's string ceiling so
|
||||
the framing LF still fits. Cutter measures, signs, and writes a fitting
|
||||
response in bounded chunks. The bridge searches only the newly read bytes
|
||||
for the framing LF. The client unescapes each socket chunk into one response
|
||||
string before parsing it, so this ceiling does not keep the paged wire frame
|
||||
and another concatenated copy. Invalid values are configuration errors and do not fall back to 16777216. Set the variable in the environment that starts REA, restart REA,
|
||||
and start Hopper or Cutter from that same environment. An out-of-band bridge
|
||||
without the setting stays at 16777216 bytes. The client reports that mismatch
|
||||
during discovery only after the hello proves a live authenticated bridge, and
|
||||
does not send a status request or the command. A stale descriptor is ignored.
|
||||
Discovery fails if any such live ceiling differs, even when another live
|
||||
session matches. A command response discarded for that ceiling stays an
|
||||
unknown execution with `output_truncated`. A hello
|
||||
that never sends its authenticated frame does not hold the accept loop.
|
||||
At most 8 connection threads run; a further accept is closed, and a failure
|
||||
to start a thread does not stop the listener. `completed_connection` is the
|
||||
longest prefix of connections that have terminated. Dense scalar replies are
|
||||
encoded in bounded batches.
|
||||
The 25000 ms Cutter request deadline and the 256-candidate discovery budget
|
||||
still have no runtime override.
|
||||
|
||||
## Selected native facts and call graphs
|
||||
|
||||
For a large `get_call_graph`, or repeated `procedure_address`,
|
||||
`procedure_callees`, `procedure_callers` and `resolve_containing_procedure`
|
||||
queries, select `detail: "summary"` when the first 64 recorded observations
|
||||
suffice. Read further `native-observation` pages from the returned parent Evidence
|
||||
ID. Graph message IDs index that page's exact-text dictionary; keep level,
|
||||
source index, failed nodes, and partial/unknown relationship coverage.
|
||||
Read referenced source context through `native-context` facets `overview`,
|
||||
`analysis-profile`, `limitations` and `result-limitations`. Reuse inspected
|
||||
context/profile values only for the same digest; until caveats are read,
|
||||
interpretation beyond the returned facts remains unknown. Complete Evidence
|
||||
remains retained and exportable. Small complete replies can cost fewer tokens
|
||||
than additional context/page calls; count the completed task and discovery.
|
||||
|
||||
## Static memory and original file bytes
|
||||
|
||||
For Ghidra, `read_bytes` reads initialized analysis memory, including loader
|
||||
fixups; it does not read a running process. Check `returned_bytes` and `complete`
|
||||
before decoding a table. An incomplete read is missing coverage, not a zero-filled
|
||||
tail. Use `list_segments` to inspect the selected block's initialization and address-space
|
||||
facts, and retain unavailable facts as unknown.
|
||||
|
||||
Use `address_to_file_offset` to anchor one observed address to original file
|
||||
bytes. A mapping for the first address does not establish a contiguous mapping
|
||||
for an entire table, and loader fixups can make analysis bytes differ from source
|
||||
bytes. Keep source ranges and analysis-memory observations separate.
|
||||
|
||||
A pointer table can refer to writable slots populated by a constructor rather
|
||||
than directly to immutable resources. An uninitialized block or an observed zero
|
||||
does not establish the slot's later runtime value. Follow `xrefs` and the relevant
|
||||
writer's assembly or pseudocode; distinguish recorded assignments from inferred
|
||||
execution order. Do not replace unreadable bytes with zeros or conclude that a
|
||||
resource is absent without the necessary initialization evidence.
|
||||
|
||||
## DOS MZ with Ghidra
|
||||
|
||||
For an admitted DOS MZ target, use `inspect_native_load_image` to check measured
|
||||
@@ -476,18 +131,12 @@ loaded bytes, file mappings, relocations and the entry against the immutable
|
||||
snapshot. Keep `mismatch` and `unsupported` explicit. Verified import covers the
|
||||
reported static image, not runtime DOS or PC-98 hardware behavior.
|
||||
|
||||
For a large load-image report, select `detail: "summary"` and query its
|
||||
`parent_evidence_id` with `inspect_analysis_view` kind `native-load-image`.
|
||||
Choose a collection such as `relocations` or `mappings`, select only needed
|
||||
fields, and page with offset/limit. Original indices preserve same-address
|
||||
alternatives. Null collections remain unrecorded or inapplicable; paging does
|
||||
not upgrade unsupported verification. CLI requires `--evidence_output` for
|
||||
summary delivery so complete source Evidence remains accessible.
|
||||
|
||||
For Ghidra decompilation, `procedure_pseudo_code` also accepts `procedure: "file-offset:0x..."`; CLI `rea decompile <path> --file-offset=0x... --provider ghidra` resolves the source byte offset internally. An unresolved 0x-prefixed numeric procedure selector also falls back to a unique file mapping. Existing symbols and analysis addresses win; multiple mappings fail as ambiguous. Preserve the returned file-offset and address locations and raw selection observations. ELF virtual addresses are distinct from file offsets; never assume an arbitrary offset plus image base is an analysis address.
|
||||
|
||||
Complete function body ranges use inclusive ends and can contain gaps. Do not
|
||||
treat the enclosing span as code.
|
||||
Use `read_bytes` for initialized analysis memory and `address_to_file_offset` to
|
||||
anchor one observed address to original file bytes. Loader fixups can change a
|
||||
word; a source offset does not imply byte equality. Complete function body ranges
|
||||
use inclusive ends and can contain gaps. Do not treat the enclosing span as code.
|
||||
For packed targets, retain the original and separately derived artifact identities;
|
||||
decompiling an unpacking stub does not recover the unpacked program.
|
||||
|
||||
@@ -527,11 +176,6 @@ names), coverage and per-collection counts. Page rows with
|
||||
`view: {kind: "managed-members", collection: "call_edges", offset: 0, limit: 64}`
|
||||
over `types`, `exported_types`, `fields`, `methods`, `member_refs`, `call_edges`,
|
||||
`field_accesses` or `user_strings`.
|
||||
Use exact `names` and `declaring_types` filters before paging supported
|
||||
collections. For a compact method lookup, request `fields: ["name",
|
||||
"declaring_type", "signature"]`; omit `body` to avoid anchors. Identity tokens,
|
||||
original `source_index`, module/MVID and source coverage remain. Check
|
||||
`unresolved_filter_rows` before interpreting an empty match as absence.
|
||||
|
||||
### .NET NativeAOT
|
||||
|
||||
@@ -582,72 +226,37 @@ every executable in the bundle. It also lists missing or weak loads and earlier
|
||||
`architecture`. System paths stay undetermined because the dyld shared cache
|
||||
provides them.
|
||||
|
||||
Use `extract_artifact` when materialized files from a supported format are needed.
|
||||
It extracts the active target and accepts optional `integrity_policy`; see the [MCP contracts](https://github.com/morluto/rea/blob/main/docs/mcp-contracts.md).
|
||||
Use `extract_artifact` when materialized files are needed. It extracts the active
|
||||
target and accepts optional `integrity_policy`; see the [MCP contracts](https://github.com/morluto/rea/blob/main/docs/mcp-contracts.md).
|
||||
It materializes all regular files into a fresh temporary directory chosen by
|
||||
REA. Symlinks and encrypted entries are inventory facts, not extractable files.
|
||||
The result reports the materialized directory; callers do not choose the
|
||||
destination. No separate permission grant is needed.
|
||||
|
||||
For classic iNES cartridges, `open_binary` recognizes the byte signature without
|
||||
an executable architecture. `inspect_artifact` retains header facts and each
|
||||
PRG/CHR file unit's offset, length and SHA-256; its nested inventory's
|
||||
`ines.reconstruction` verifies the actual ordered region bytes against the
|
||||
whole-file digest. Extraction includes the original header, optional trainer
|
||||
and uninterpreted trailing bytes. These file units are not mapper/CPU bank
|
||||
maps. NES 2.0, UNIF, dirty legacy headers and truncated payloads are explicitly
|
||||
refused. No emulator or 6502 analysis is performed. See the
|
||||
[iNES guide](https://github.com/morluto/rea/blob/main/docs/ines-containers.md).
|
||||
|
||||
Native DMG traversal automatically uses a read-only mount on supported macOS
|
||||
hosts, with an owned temporary mount directory and cleanup. It needs no approval
|
||||
flag. Unsupported hosts or mount failures remain explicit limitations; do not
|
||||
claim child inventory when only root identity is available. DMG extraction is
|
||||
unavailable through `extract_artifact`. To access DMG files on macOS, create a
|
||||
read-only mount with your own tools, select files from that mount for analysis,
|
||||
and detach the mount you created when finished. REA's inspection mount is
|
||||
temporary, not a retained export of the image's contents.
|
||||
claim child inventory when only root identity is available. Extraction is a
|
||||
separate filesystem-writing operation.
|
||||
|
||||
For headerless DOS COM, explicitly open with `format: "dos-com"` (CLI
|
||||
`--target-format dos-com`). Inspect the load image before trusting function
|
||||
analysis: the entry is `0x10100`, file offset zero, with imposed real-mode segment
|
||||
context. PSP/stack/device state remains unmodeled. See [DOS guide](https://github.com/morluto/rea/blob/main/docs/ghidra-dos.md).
|
||||
|
||||
Use `inspect_byte_entropy` to triage high- and low-entropy ranges before extraction.
|
||||
|
||||
For other headerless images (embedded firmware without ELF/Mach-O/PE/MZ
|
||||
headers), open with `format: "raw-binary"`, `architecture` (`x86`, `x86_64`,
|
||||
`arm`, `arm64`, `6502`, or `m68k`), and `entry_point` (`0x` hex). File offset 0 is mapped at
|
||||
`arm`, `arm64`, or `6502`), and `entry_point` (`0x` hex). File offset 0 is mapped at
|
||||
that address. Hopper and IDA refuse this interpretation; Ghidra uses
|
||||
`BinaryLoader` with a generic language: big-endian for `m68k`, little-endian
|
||||
otherwise. Big-endian ARM, 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. `m68k` requires an even entry and decodes the 68040
|
||||
superset of the 68000 family; Amiga/Atari containers and vector-table entry
|
||||
discovery 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).
|
||||
|
||||
Intel HEX, S-record, UF2 and DfuSe files are containers, not raw images:
|
||||
`inspect_artifact` lists address regions (UF2 per familyID, DfuSe per element
|
||||
with its alternate setting), and `extract_artifact` writes each region. Open an
|
||||
extracted region as `raw-binary` with its start address as `entry_point`. UF2
|
||||
numbering gaps leave coverage unknown; a DfuSe suffix CRC mismatch follows
|
||||
`integrity_policy`. See [UF2 and DfuSe](https://github.com/morluto/rea/blob/main/docs/uf2-dfuse.md).
|
||||
`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).
|
||||
|
||||
### Function annotations in Ghidra
|
||||
|
||||
Fresh-import sessions admit annotations; existing-project reuse is read-only.
|
||||
Use `annotate_native_function` for one function name and/or entry comments, with at least one explicit change. Review its annotation readback and refreshed dossier inline. Changes are atomic and session-scoped; empty comments clear them, omitted fields preserve them. Later MCP calls observe edits until close. CLI `annotate-native-function` returns the updated analysis before discarding the session. Original executable bytes are unchanged; edits invalidate immutable snapshots. Windows P0 does not admit database mutations. For naming only, `set_address_name` / `set_addresses_names` also work with Ghidra: they name function entries or data addresses (primary labels) and return compact per-address success booleans instead of a dossier. Ghidra rejects batch addresses that request the same name or a name another selected address holds; `raw_result.failures` gives each failure's reason.
|
||||
|
||||
For `compare_functions`, obtain `analyze_function` Evidence before and after the
|
||||
edit while the MCP session is open; annotation Evidence is not a comparison
|
||||
input. Export the needed Evidence before close if comparison will happen later.
|
||||
Authenticated same-artifact coordinates preserve renamed entries as
|
||||
`matched / same-entry`, with the canonical `entry_address` inline; name and
|
||||
exact-text changes remain separate dimensions. This requires compatible
|
||||
architecture, provider-owned mapping and selected-image commitments. Equal
|
||||
numeric addresses alone cannot establish correspondence; inspect retained
|
||||
source Evidence and coverage when the match remains ambiguous.
|
||||
Use `annotate_native_function` for one function name and/or entry comments, with at least one explicit change. Review its annotation readback and refreshed dossier inline. Changes are atomic and session-scoped; empty comments clear them, omitted fields preserve them. Later MCP calls observe edits until close. CLI `annotate-native-function` returns the updated analysis before discarding the session. Original executable bytes are unchanged; edits invalidate immutable snapshots. Windows P0 does not admit database mutations. For naming only, `set_address_name` / `set_addresses_names` also work with Ghidra: they name function entries or data addresses (primary labels) and return compact per-address success booleans instead of a dossier.
|
||||
|
||||
## Offline PE resource inventory
|
||||
|
||||
@@ -661,120 +270,8 @@ Other payloads are opaque, and this operation does not decode images or simulate
|
||||
Windows resource loading. Consult the budgets and supported tree profile in
|
||||
[offline binary diagnostics](https://github.com/morluto/rea/blob/main/docs/binary-diagnostics.md).
|
||||
|
||||
## Offline PE and ELF export tables
|
||||
|
||||
Use `inspect_native_exports` with an absolute `path`, or
|
||||
`rea inspect-native-exports ./libexample.so --json`, to read a PE export
|
||||
directory or ELF dynamic symbol table on any host without opening a native
|
||||
target. Ordinal-only, forwarded and hidden-version exports stay distinct;
|
||||
partial or unsupported coverage means the inventory is incomplete, not that
|
||||
exports are absent. Use `inspect_macho` for Mach-O. See
|
||||
[offline binary diagnostics](https://github.com/morluto/rea/blob/main/docs/binary-diagnostics.md).
|
||||
|
||||
For exact core-module import/export differences, use `compare_wasm_interfaces`
|
||||
with two complete inspection Evidence records (or current-session retained
|
||||
references) and explicit scope. Preserve unknown types/duplicate names; static
|
||||
interface equality is not runtime compatibility. CLI accepts complete inline
|
||||
Evidence in `rea compare-wasm-interfaces input.json --json`, or exact references
|
||||
with `--evidence-bundle /absolute/path/evidence.json`. Use the complete parent
|
||||
ID when the inspection returned a summary.
|
||||
|
||||
## Godot PCK
|
||||
|
||||
For a standalone PCK, `open_binary` then `inspect_artifact` with
|
||||
`detail: "summary"` retains the complete static inventory. Use the returned
|
||||
`parent_evidence_id` with `inspect_analysis_view` inventory occurrences filtered
|
||||
by exact `logical_paths` or `occurrence_ids`, and join `artifact_id` to nodes
|
||||
filtered by `artifact_ids`. Select only needed fields; identity links remain.
|
||||
Prefer `inspect-artifact --detail summary --evidence_output PATH` for CLI tasks;
|
||||
the same CLI discovers embedded packs via their EOF trailer without opening a
|
||||
native analysis provider. Normal native opening remains unchanged.
|
||||
|
||||
Distinguish declared MD5 from observed SHA-256 and graph/page completeness from
|
||||
`source_coverage`. Encrypted directories and sparse bundles have unknown entry
|
||||
coverage; encrypted members have unknown content identity. An empty filtered
|
||||
page cannot prove absence in unknown coverage. Retained views are historical,
|
||||
not fresh filesystem checks. No decryption, Godot resource decoding, GDScript
|
||||
decompilation or target execution is provided. See
|
||||
[Godot PCK](https://github.com/morluto/rea/blob/main/docs/godot-pck.md).
|
||||
|
||||
UnityFS AssetBundle containers (`.bundle`, `.ab`, YooAsset packages and other
|
||||
byte-identified files) list `CAB-*` SerializedFiles and `.resS` members with
|
||||
header and node facts; none/LZ4/LZ4HC member blocks stream on demand. Bundles
|
||||
declaring asset-bundle encryption keep header facts with unknown directory
|
||||
coverage; LZMA member bytes retain named occurrences with unavailable hashes.
|
||||
Virtual member offsets are not physical carrier locations. Format 6–8 readers
|
||||
cache one decoded block and support retained filtered/paged views. No
|
||||
decryption or SerializedFile object decoding is provided. See
|
||||
[UnityFS](https://github.com/morluto/rea/blob/main/docs/unityfs.md).
|
||||
|
||||
U-Boot legacy uImages use the same inspection/extraction workflows. Prefer a
|
||||
retained summary and selected inventory views for header facts or component
|
||||
IDs. Header addresses/CPU/OS are declarations; payloads remain as stored.
|
||||
Multi-file components retain their aligned physical ranges; the full payload
|
||||
is also materialized. Unknown name text retains its original 32 bytes. See
|
||||
[U-Boot uImage](https://github.com/morluto/rea/blob/main/docs/uimage.md).
|
||||
|
||||
## ESP images
|
||||
|
||||
For an ESP32-family app or bootloader image, `open_binary` then
|
||||
`inspect_artifact` with `detail: "summary"`. `root_format_metadata` holds the
|
||||
observed `header` and `descriptor` (its `kind` is the app or bootloader role)
|
||||
and the derived `chip`, `checksum`, `appended_sha256` and
|
||||
`undescribed_trailing` ranges. Segment rows (`segment-NN.bin`) carry
|
||||
`load_addr`, `classification` (`mapped`, `loaded`, `not-loaded` or `unknown`,
|
||||
per ESP-IDF `should_map`/`should_load`; RAM ranges are not checked) and
|
||||
`source_location`; select them with `inspect_analysis_view` and
|
||||
`logical_paths`. Signature blocks are `unverified`; `public_key_sha256`
|
||||
matches eFuse key digests.
|
||||
|
||||
Unknown `source_coverage` means truncation or undescribed trailing bytes, such
|
||||
as the flash segments of a `--ram-only-header` image; do not infer absence
|
||||
from it. Erased flash is described. Integrity mismatches are facts, not
|
||||
refusals. ESP8266 images are refused, and flash-encrypted images are not
|
||||
recognized. Segments are not disassembled, and RISC-V or Xtensa segments
|
||||
cannot be opened as raw-binary. See
|
||||
[ESP images](https://github.com/morluto/rea/blob/main/docs/esp-image.md).
|
||||
|
||||
## ESP flash images
|
||||
|
||||
A whole-flash dump opens as `esp-flash-image`. `root_format_metadata` holds
|
||||
`partition_table` (its `method`: `default`, `caller` or a derived
|
||||
`scan`), `bootloader` and `apps` ESP image facts, the `otadata` records,
|
||||
`partitions_outside_dump` and `boot_selection`. Every table has a
|
||||
`boot_selection`: its `basis` names the bootloader's branch (`no-otadata`
|
||||
without otadata, `none` when the bootloader rejects the table, `unknown`
|
||||
when a record is missing), and it lists its `assumptions`;
|
||||
`running_partition` is always `unknown`. Each image names its entry
|
||||
`path`. With `detail: "summary"` the root kind is `esp-flash-image-summary`
|
||||
and each image is a compact `summary` (chip, entry point, segment count,
|
||||
classified load ranges, signature block count, descriptor identity,
|
||||
integrity); complete detail keeps every segment. Partition rows
|
||||
(`partitions/<label>.bin`) carry the declared `type`, `subtype`, names,
|
||||
`flash_address`, `size` and flags; `encrypted` is a storage flag, not
|
||||
proof of ciphertext. `written_offset`/`written_end` bound the bytes that
|
||||
are not erased: an app slot whose facts say `erased` can still hold an older
|
||||
image, and a limitation says so. For a dump read from another flash address, call
|
||||
`open_binary` with `format: "esp-flash-image"` and `flash_offset`; an
|
||||
`esp-image` whose `flash_dump` is set names that offset in a next probe; a
|
||||
relocated table can be named with `partition_table_offset`. No table found
|
||||
means unknown, often flash encryption. Extraction writes `nvs_keys` as
|
||||
ordinary data. See
|
||||
[ESP flash images](https://github.com/morluto/rea/blob/main/docs/esp-flash-image.md).
|
||||
|
||||
## Bun standalone executables
|
||||
|
||||
A `bun build --compile` executable stores its module graph in Mach-O
|
||||
`__BUN,__bun`, a PE or ELF `.bun` section, or an appended trailer.
|
||||
`open_binary` then `inspect_artifact` reports root format `bun-standalone` with
|
||||
the carrier, structural layout, module count, entry point, raw flags and
|
||||
compile exec-argv; each module keeps its `/$bunfs/...` source name, loader,
|
||||
encoding, sizes and SHA-256, with sourcemap, bytecode and module-info regions as
|
||||
separate children. Use summary root metadata `entry_point_logical_path` as the
|
||||
exact inventory `logical_paths` selector, then read the selected content node by
|
||||
its artifact ID; the reader owns virtual-name normalization and indexed fallback. `extract-artifact` writes stored contents through the safe
|
||||
output tree; route extracted JavaScript to `analyze_javascript_application`.
|
||||
Layout labels describe record shapes, not a detected compiler version; unknown
|
||||
or ambiguous layouts are unsupported. Bytecode is retained, not decoded, and the
|
||||
executable is never run. See
|
||||
[Bun standalone executables](https://github.com/morluto/rea/blob/main/docs/bun-standalone.md).
|
||||
Evidence in `rea compare-wasm-interfaces input.json --json`.
|
||||
@@ -1,52 +1,4 @@
|
||||
# Runtime observation
|
||||
|
||||
## Native runtime questions
|
||||
|
||||
Start from the requested observation. State the selected target, input, expected
|
||||
result, failure signal and stopping condition before a run. Reuse static Evidence
|
||||
to choose a function or hypothesis; `trace_native_values` follows recovered
|
||||
static value dependencies and does not measure runtime values. Prefer static
|
||||
inspection or existing capture comparison when it already answers the question.
|
||||
|
||||
| Question | Existing workflow | Host and effects |
|
||||
| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Which selected native entry is reached, with which integer argument registers? | `observe_native_calls` on the active Mach-O, with exact function/Objective-C breakpoints and a duration/event bound | macOS with LLDB and required debugger access. Launches a new owned process, stops at entries, records and continues; target execution may write files, show UI or use the network. It never attaches to an existing process. Inspect termination and partial coverage after the run. |
|
||||
| What does caller-authored instrumentation observe? | `list_frida_devices` / `list_frida_processes` when discovery is needed; `start_frida_session`, `load_frida_script`, then status/unload/close | Requires compatible installed Frida bindings and selected device access; the real REA verifier covers Linux. Attach selects an existing PID; spawn selects a program and starts it paused until `resume_frida_session`. Scripts execute in the target and can change memory, files, network activity and behavior. This is not passive inspection. |
|
||||
| Which debugger state or controlled execution explains the behavior? | `start_gdb_session` and `gdb_console`, or `start_rizin_debug_session` and `rizin_debug_command` | Persistent debugger workflows are Linux-only. GDB starts a debugger; console commands choose/load/attach/run targets. Rizin debug start launches the selected target and requires a durable `inferior_tty`. Both consoles are unrestricted and can change target memory/registers, run shell commands, write files and use the network. |
|
||||
| How do terminal output, process lifetime and selected files differ between runs? | `capture_process_scenario`, then `compare_process_captures` with compatible recorded inputs | New captures require Linux or macOS and a working native PTY backend; macOS also needs `xcrun`/Swift for ownership inspection. Capture launches an owned command with ordinary user permissions. Observation paths select snapshots, not a filesystem sandbox. Comparing retained captures does not execute targets and is also available on Windows. |
|
||||
| What does an existing desktop window show, or what changes after a declared UI action? | `observe_native_ui`; use `capture_native_ui_scenario` only for requested actions | macOS 14+ with required Screen Recording/Accessibility access. Select an existing PID/window. Observation does not launch or own the app; scenario actions can change its data or trigger network activity, and leave its state as-is. |
|
||||
|
||||
Close each persistent session with its matching tool. `close_frida_session`
|
||||
unloads scripts and detaches; it does not kill the target. A still-paused spawn
|
||||
is resumed only after instrumentation cleanup is confirmed. GDB and Rizin close
|
||||
can detach a live inferior and let it continue only after verifying its durable
|
||||
output terminal. When disposition or cleanup is unresolved, preserve the owned
|
||||
session and reported recovery; do not assume that closing the debugger terminated
|
||||
the target. Process capture and LLDB call observation instead own their launched
|
||||
processes and report their termination/cleanup outcome. Cancellation is not proof
|
||||
that cleanup completed.
|
||||
|
||||
Unrestricted GDB/Rizin commands and relevant start/close operations retire an
|
||||
active binary-analysis session in the same MCP server. Close an active Ghidra
|
||||
session explicitly first; after target effects settle, reopen the artifact before
|
||||
new static analysis. A Cutter bridge command is likewise unrestricted and may
|
||||
affect its GUI/target; it is not a passive native observation substitute.
|
||||
|
||||
To test a static hypothesis across two builds, retain compatible static
|
||||
comparison Evidence and `compare_process_captures` Evidence. Supply explicit
|
||||
finding/dimension mappings to `correlate_static_and_runtime`. It does not ingest
|
||||
arbitrary Frida, LLDB or debugger traces, auto-match names, or prove causality
|
||||
from cochange. Preserve those traces as their own Evidence and leave unobserved
|
||||
behavior unknown. Record truncation, unmatched breakpoints, changed target
|
||||
identity and cleanup uncertainty as limitations, rather than treating no retained
|
||||
event as proof that the behavior never occurs.
|
||||
|
||||
Detailed contracts and prerequisites: [Frida](https://github.com/morluto/rea/blob/main/docs/frida-instrumentation.md),
|
||||
[GDB/Rizin/Cutter](https://github.com/morluto/rea/blob/main/docs/reverse-engineering-tools.md),
|
||||
[process capture](https://github.com/morluto/rea/blob/main/docs/process-capture.md),
|
||||
and [native calls/UI](https://github.com/morluto/rea/blob/main/docs/native-investigation.md#native-desktop-observation).
|
||||
|
||||
## Browser and Electron
|
||||
# Browser and Electron runtime observation
|
||||
|
||||
Supply the existing browser or Electron instance's literal-port loopback CDP
|
||||
endpoint in the request. No separate permission configuration or per-call
|
||||
@@ -86,8 +38,6 @@ Page-declared WebMCP tools are untrusted inventory and are never invoked by REA.
|
||||
browser and profile. Check `browser.cleanup` for the outcome.
|
||||
Request only the DOM, accessibility, or network details needed for the question.
|
||||
The default final capture contains the URL; step outcomes are reported separately.
|
||||
For multi-step flows with trees or events, select `detail: "summary"` and page
|
||||
exact steps, transactions, text slices or events with `inspect_analysis_view` kind `browser-scenario`.
|
||||
Add a wait action for the expected page state before capturing it. Report that
|
||||
REA performed the declared actions.
|
||||
|
||||
@@ -119,12 +69,6 @@ with `inspect_analysis_view` and `view: {kind: "capture", detail: "preview", off
|
||||
Select complete record pages only for needed headers, metadata or payloads; views
|
||||
read the historical parent without repeating capture decoding.
|
||||
|
||||
For selected protobuf/gRPC bodies, keep raw bytes as the source and use
|
||||
`inspect_analysis_view` kind `protobuf`, collection `bodies`, `frames` or
|
||||
`fields`. Batch exact record ordinals and field numbers/IDs, and select
|
||||
`schema_value` or `text` only when needed. Supply a local descriptor set and
|
||||
message type for schema interpretation; compressed payloads remain unknown.
|
||||
|
||||
Inspect native `.saz` snapshots with `inspect_web_network_capture` and
|
||||
`format: "saz"`; encrypted archives are unsupported. For live
|
||||
traffic analysis, connect Everywhere's separate MCP server and use its
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
||||
"name": "rea",
|
||||
"description": "Reverse engineer anything with agents, from app behavior down to native binaries.",
|
||||
"owner": {
|
||||
"name": "morluto",
|
||||
"url": "https://github.com/morluto"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "rea",
|
||||
"description": "Reverse engineer anything with agents, from app behavior down to native binaries.",
|
||||
"source": {
|
||||
"source": "npm",
|
||||
"package": "rea-agents",
|
||||
"version": "6.8.0"
|
||||
},
|
||||
"category": "development"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "rea",
|
||||
"version": "6.8.0",
|
||||
"description": "Reverse engineer anything with agents, from app behavior down to native binaries.",
|
||||
"author": {
|
||||
"name": "morluto",
|
||||
"url": "https://github.com/morluto"
|
||||
},
|
||||
"homepage": "https://rea.tools/",
|
||||
"repository": "https://github.com/morluto/rea",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"reverse-engineering",
|
||||
"binary-analysis",
|
||||
"decompiler",
|
||||
"mcp-server"
|
||||
],
|
||||
"skills": "./skills",
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"${CLAUDE_PLUGIN_ROOT}/scripts/plugin-mcp-launcher.mjs",
|
||||
"6.8.0"
|
||||
]
|
||||
}
|
||||
},
|
||||
"icon": "./assets/rea-plugin.png"
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"./scripts/plugin-mcp-launcher.mjs",
|
||||
"6.8.0"
|
||||
],
|
||||
"cwd": ".",
|
||||
"startup_timeout_sec": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "rea",
|
||||
"version": "6.8.0",
|
||||
"description": "Reverse engineer anything with agents, from app behavior down to native binaries.",
|
||||
"author": {
|
||||
"name": "morluto",
|
||||
"url": "https://github.com/morluto"
|
||||
},
|
||||
"homepage": "https://rea.tools/",
|
||||
"repository": "https://github.com/morluto/rea",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"reverse-engineering",
|
||||
"binary-analysis",
|
||||
"decompiler",
|
||||
"mcp-server"
|
||||
],
|
||||
"skills": "./skills",
|
||||
"mcpServers": "./.codex-plugin/mcp.json",
|
||||
"interface": {
|
||||
"displayName": "REA",
|
||||
"shortDescription": "Reverse engineer anything",
|
||||
"longDescription": "Reverse engineer anything with agents, from app behavior down to native binaries.",
|
||||
"developerName": "morluto",
|
||||
"category": "Developer Tools",
|
||||
"capabilities": [
|
||||
"Interactive",
|
||||
"Read",
|
||||
"Write"
|
||||
],
|
||||
"websiteURL": "https://rea.tools/",
|
||||
"defaultPrompt": [
|
||||
"Use REA to explain how this binary implements a feature.",
|
||||
"Use REA to compare two versions of this application.",
|
||||
"Use REA to recover the sources from this JavaScript bundle."
|
||||
],
|
||||
"composerIcon": "./assets/rea-plugin.png",
|
||||
"logo": "./assets/rea-plugin.png"
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
# Review changes to security-sensitive automation and dependency inputs.
|
||||
/.github/CODEOWNERS @morluto @kaoru0822-kitauji @N0zoM1z0 @gensokyo-dev
|
||||
/.github/dependabot.yml @morluto @kaoru0822-kitauji @N0zoM1z0 @gensokyo-dev
|
||||
/.github/workflows/ @morluto @kaoru0822-kitauji @N0zoM1z0 @gensokyo-dev
|
||||
/.github/actions/ @morluto @kaoru0822-kitauji @N0zoM1z0 @gensokyo-dev
|
||||
/package.json @morluto @kaoru0822-kitauji @N0zoM1z0 @gensokyo-dev
|
||||
/package-lock.json @morluto @kaoru0822-kitauji @N0zoM1z0 @gensokyo-dev
|
||||
/scripts/verify-tools/skills/package.json @morluto @kaoru0822-kitauji @N0zoM1z0 @gensokyo-dev
|
||||
/scripts/verify-tools/skills/package-lock.json @morluto @kaoru0822-kitauji @N0zoM1z0 @gensokyo-dev
|
||||
/scripts/fixtures/http3-lab-requirements.txt @morluto @kaoru0822-kitauji @N0zoM1z0 @gensokyo-dev
|
||||
/website/requirements.txt @morluto @kaoru0822-kitauji @N0zoM1z0 @gensokyo-dev
|
||||
/third_party/ @morluto @kaoru0822-kitauji @N0zoM1z0 @gensokyo-dev
|
||||
@@ -1,10 +1,5 @@
|
||||
name: Cache npm package downloads
|
||||
description: Restore npm's download cache. Trusted default-branch jobs that opt in may save it.
|
||||
inputs:
|
||||
save:
|
||||
description: Save the shared download cache when this event is allowed to write it.
|
||||
required: false
|
||||
default: "false"
|
||||
description: Restore npm's download cache and save it only from trusted default-branch events.
|
||||
outputs:
|
||||
can-save:
|
||||
description: Whether this run may update the shared npm download cache.
|
||||
@@ -28,25 +23,20 @@ runs:
|
||||
can_save=true
|
||||
fi
|
||||
cache_key="npm-download-${RUNNER_OS}-${runtime_arch}-${LOCKFILES_HASH}"
|
||||
# Same platform only. A lockfile change can reuse tarballs from the previous key.
|
||||
restore_key="npm-download-${RUNNER_OS}-${runtime_arch}-"
|
||||
{
|
||||
printf 'can-save=%s\n' "${can_save}"
|
||||
printf 'path=%s\n' "${cache_path}"
|
||||
printf 'key=%s\n' "${cache_key}"
|
||||
printf 'restore-key=%s\n' "${restore_key}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
- name: Restore and save npm package downloads
|
||||
if: steps.context.outputs.can-save == 'true' && inputs.save == 'true'
|
||||
if: steps.context.outputs.can-save == 'true'
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ steps.context.outputs.path }}
|
||||
key: ${{ steps.context.outputs.key }}
|
||||
restore-keys: ${{ steps.context.outputs.restore-key }}
|
||||
- name: Restore npm package downloads without saving
|
||||
if: steps.context.outputs.can-save != 'true' || inputs.save != 'true'
|
||||
if: steps.context.outputs.can-save != 'true'
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ steps.context.outputs.path }}
|
||||
key: ${{ steps.context.outputs.key }}
|
||||
restore-keys: ${{ steps.context.outputs.restore-key }}
|
||||
@@ -24,7 +24,19 @@ runs:
|
||||
BUILD_CACHE_PREFIX: rea-portable-build-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.npmrc', '.nvmrc', 'package-lock.json') }}
|
||||
run: |
|
||||
npm run --silent test:prepare -- --dry=json |
|
||||
node scripts/ci/portable-build-cache.mjs key >> "$GITHUB_OUTPUT"
|
||||
node --input-type=module -e '
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
const { tasks } = JSON.parse(readFileSync(0, "utf8"));
|
||||
const hashes = tasks.map(({ taskId, hash }) => {
|
||||
if (typeof hash !== "string")
|
||||
throw new Error(`Build task ${taskId} must have an eager Turbo hash`);
|
||||
return `${taskId}:${hash}`;
|
||||
}).sort();
|
||||
const hash = createHash("sha256").update(hashes.join("\n")).digest("hex");
|
||||
console.log(`key=${process.env.BUILD_CACHE_PREFIX}-${hash}`);
|
||||
console.log(`prefix=${process.env.BUILD_CACHE_PREFIX}-`);
|
||||
' >> "$GITHUB_OUTPUT"
|
||||
- name: Restore portable build outputs
|
||||
if: inputs.build_cache_artifact == '' && inputs.save != 'true'
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
name: REA production code
|
||||
|
||||
# Match the existing default-setup scan's local-input threat model.
|
||||
threat-models: [local]
|
||||
|
||||
# Retain root traversal, including future production roots. Hidden directories
|
||||
# need their own root; use a directory so Python can share this configuration.
|
||||
paths:
|
||||
- .
|
||||
- docs/.vitepress
|
||||
|
||||
# Keep the default query suite and all production roots, including bridge/,
|
||||
# install.sh, runtime scripts, release controllers, and GitHub Actions.
|
||||
# Exclude test inputs at extraction time so they cannot taint production callers.
|
||||
paths-ignore:
|
||||
- tests
|
||||
- scripts/fixtures
|
||||
- "**/*.test.*"
|
||||
- "**/*.fixture.*"
|
||||
- "**/test_*.py"
|
||||
- "**/*_test.py"
|
||||
@@ -1,33 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directories:
|
||||
- /
|
||||
- /scripts/verify-tools/skills
|
||||
schedule:
|
||||
interval: weekly
|
||||
cooldown:
|
||||
default-days: 7
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
npm-minor-and-patch:
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
|
||||
- package-ecosystem: pip
|
||||
directory: /website
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
actions-minor-and-patch:
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
@@ -17,10 +17,7 @@ describe the use case and observable outcome. -->
|
||||
## Change and scope
|
||||
|
||||
<!-- Explain the chosen approach, why it fits REA's architecture, and what is
|
||||
intentionally not included. If adding complexity for a speculative or vanishingly
|
||||
unlikely race or security edge case, record the concrete failure mode, likelihood,
|
||||
complexity cost, and the user's buy-in obtained before implementation. Do not
|
||||
silently expand scope for theoretical robustness. -->
|
||||
intentionally not included. -->
|
||||
|
||||
Follow the [contribution scope](https://github.com/morluto/rea/blob/main/CONTRIBUTING.md#contribution-scope).
|
||||
Unresolved scope, authority, side effects, or required verification block
|
||||
@@ -36,9 +33,6 @@ unchecked. Maintainers can help resolve open questions.
|
||||
Use "none" or "not applicable" only when established. -->
|
||||
|
||||
- Semantic owner and earliest changed stage:
|
||||
<!-- Name the producer of parsed facts or resources and its affected consumers.
|
||||
Summarize superseded paths removed and any distinct policy, lifecycle,
|
||||
lazy-loading, or compatibility responsibility retained. -->
|
||||
- Contribution scope fit and linked maintainer decision:
|
||||
- CLI and MCP/tool-catalog contract:
|
||||
- Provider, bridge, target-format, or platform compatibility:
|
||||
@@ -71,10 +65,6 @@ source, link, or rendered-document checks. -->
|
||||
- User-visible CLI/MCP output (if applicable):
|
||||
- Remaining proof gaps:
|
||||
|
||||
For boundary fixes, identify the applicable [boundary regressions](https://github.com/morluto/rea/blob/main/docs/testing.md#boundary-regressions)
|
||||
and [lifecycle regressions](https://github.com/morluto/rea/blob/main/docs/testing.md#lifecycle-regressions)
|
||||
covered by the evidence above, and explain relevant gaps.
|
||||
|
||||
For evidence-bearing changes:
|
||||
|
||||
- [ ] Observed, derived, and inferred claims remain distinguishable.
|
||||
@@ -106,8 +96,7 @@ limits, security/privacy/containment implications, and meaningful proof gaps. --
|
||||
|
||||
## Author readiness
|
||||
|
||||
<!-- The human owner completes these declarations before requesting review,
|
||||
unless the PR author is exempt as listed in CONTRIBUTING.md.
|
||||
<!-- The human owner completes these declarations before requesting review.
|
||||
The PR author is the owner by default. A different owner must explicitly accept
|
||||
ownership on this PR.
|
||||
AI agents must leave these declarations to a human to dictate to them or there could be catastrophic consequences for many users of REA.
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
name: Cache retention
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI"]
|
||||
types: [completed]
|
||||
|
||||
# Qualification calls ci.yml with actions: read. A nested workflow cannot raise
|
||||
# that token, so cache deletion stays in this workflow.
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
|
||||
concurrency:
|
||||
group: cache-retention-${{ github.event.workflow_run.head_sha }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
retain:
|
||||
name: Retain Turbo snapshots and npm download caches
|
||||
# Restrict the triggering-source checkout to same-repository default-branch
|
||||
# CI events. The helper runs from this trusted workflow revision; triggering
|
||||
# source is used only for cache identity and is never executed. Static and
|
||||
# build save snapshots before later lanes finish, so failed, timed-out, or
|
||||
# cancelled runs still retain those snapshots.
|
||||
if: >-
|
||||
(github.event.workflow_run.conclusion == 'success' ||
|
||||
github.event.workflow_run.conclusion == 'failure' ||
|
||||
github.event.workflow_run.conclusion == 'timed_out' ||
|
||||
github.event.workflow_run.conclusion == 'cancelled') &&
|
||||
github.event.workflow_run.head_repository.full_name == github.repository &&
|
||||
github.event.workflow_run.head_branch == github.event.repository.default_branch &&
|
||||
(github.event.workflow_run.event == 'push' ||
|
||||
github.event.workflow_run.event == 'schedule' ||
|
||||
github.event.workflow_run.event == 'workflow_dispatch')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
ref: ${{ github.workflow_sha }}
|
||||
path: trusted-retention
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
path: triggering-source
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: trusted-retention/.nvmrc
|
||||
- name: Retain current Turbo snapshots and npm download caches
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
CI_REPOSITORY: ${{ github.repository }}
|
||||
CACHE_PREFIX: rea-static-v1-${{ runner.os }}-${{ runner.arch }}-
|
||||
CACHE_KEEP_KEY: rea-static-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('triggering-source/.nvmrc', 'triggering-source/.npmrc', 'triggering-source/package-lock.json') }}-${{ github.event.workflow_run.head_sha }}
|
||||
CACHE_DEEP_PREFIX: rea-static-deep-v1-${{ runner.os }}-${{ runner.arch }}-
|
||||
CACHE_DEEP_KEEP_KEY: rea-static-deep-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('triggering-source/.nvmrc', 'triggering-source/.npmrc', 'triggering-source/package-lock.json') }}-${{ github.event.workflow_run.head_sha }}
|
||||
CACHE_KNIP_PREFIX: knip-meta-v1-${{ runner.os }}-
|
||||
CACHE_KNIP_KEEP_KEY: knip-meta-v1-${{ runner.os }}-${{ hashFiles('triggering-source/knip.json', 'triggering-source/tsconfig.json', 'triggering-source/package.json', 'triggering-source/package-lock.json') }}-${{ github.event.workflow_run.head_sha }}
|
||||
CACHE_PORTABLE_PREFIX: rea-portable-build-v2-${{ runner.os }}-${{ runner.arch }}-
|
||||
DEDUPE_NPM_DOWNLOADS: "true"
|
||||
run: node trusted-retention/scripts/ci/cache-retention.mjs
|
||||
+37
-293
@@ -15,15 +15,6 @@ on:
|
||||
description: Exact qualification source; must match the caller revision.
|
||||
required: true
|
||||
type: string
|
||||
remote_cache_write:
|
||||
description: Trusted release qualification may publish the remote cache.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
TURBO_TOKEN:
|
||||
description: Remote cache token. Only trusted release qualification passes it.
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -38,15 +29,6 @@ concurrency:
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
# Set Actions secret TURBO_TOKEN and variable TURBO_TEAM. OIDC can mint
|
||||
# TURBO_TOKEN via vercel/setup-turborepo-remote-cache-action; do not commit a token.
|
||||
# A called workflow sees the caller event, so workflow_call is not an event
|
||||
# name here. Release qualification sets remote_cache_write and passes the
|
||||
# token. Open pull requests get an empty token and the local cache only.
|
||||
# Remote reads need a credential, and pull requests do not receive one.
|
||||
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
@@ -61,9 +43,6 @@ jobs:
|
||||
qualification_run_id: ${{ steps.qualification.outputs.run_id }}
|
||||
code: ${{ steps.plan.outputs.code }}
|
||||
reverse_tools: ${{ steps.plan.outputs.reverse_tools }}
|
||||
python: ${{ steps.plan.outputs.python }}
|
||||
packet_capture: ${{ steps.plan.outputs.packet_capture }}
|
||||
signal_recording: ${{ steps.plan.outputs.signal_recording }}
|
||||
sqlite: ${{ steps.plan.outputs.sqlite }}
|
||||
runtime: ${{ steps.plan.outputs.runtime }}
|
||||
package: ${{ steps.plan.outputs.package }}
|
||||
@@ -75,13 +54,11 @@ jobs:
|
||||
browser: ${{ steps.plan.outputs.browser }}
|
||||
binary_layout: ${{ steps.plan.outputs.binary_layout }}
|
||||
evm: ${{ steps.plan.outputs.evm }}
|
||||
apk_signature: ${{ steps.plan.outputs.apk_signature }}
|
||||
javascript_recovery: ${{ steps.plan.outputs.javascript_recovery }}
|
||||
recorded_crash: ${{ steps.plan.outputs.recorded_crash }}
|
||||
web_network: ${{ steps.plan.outputs.web_network }}
|
||||
web_runtime: ${{ steps.plan.outputs.web_runtime }}
|
||||
web_source_map: ${{ steps.plan.outputs.web_source_map }}
|
||||
agentenv: ${{ steps.plan.outputs.agentenv }}
|
||||
nativeaot: ${{ steps.plan.outputs.nativeaot }}
|
||||
ghidra_bridge: ${{ steps.plan.outputs.ghidra_bridge }}
|
||||
website: ${{ steps.plan.outputs.website }}
|
||||
@@ -147,15 +124,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Validate source architecture before dependency installation
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
run: node scripts/verify/architecture-guards.mjs
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
with:
|
||||
# Linux x64 writer. Parallel Linux jobs restore this key.
|
||||
save: "true"
|
||||
- run: npm ci
|
||||
- name: Restore static task cache
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
@@ -179,10 +150,14 @@ jobs:
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
env:
|
||||
CI_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
CI_STATIC_CONTEXT_SCRIPT: check:ci:fast:tasks
|
||||
run: npm run ci:static-context
|
||||
- if: needs.changes.outputs.code == 'true'
|
||||
run: npm run check:ci:fast
|
||||
run: npm run check:ci
|
||||
- if: needs.changes.outputs.code == 'true'
|
||||
run: npm run verify:test-discovery
|
||||
- name: Check LLDB producer retention and launch contracts
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
run: npm run test:python
|
||||
- if: needs.changes.outputs.code != 'true'
|
||||
run: npm run format:check
|
||||
- name: Save validated static task cache
|
||||
@@ -204,93 +179,6 @@ jobs:
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
|
||||
static-deep:
|
||||
name: Deep static checks
|
||||
needs: changes
|
||||
# Full qualification only. Provider lanes wait on fast static, not this job.
|
||||
if: needs.changes.outputs.tier == 'full' && needs.changes.outputs.code == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- name: Restore deep static task cache
|
||||
id: static-cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: .turbo/cache/
|
||||
key: rea-static-deep-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc', '.npmrc', 'package-lock.json') }}-${{ github.sha }}
|
||||
restore-keys: rea-static-deep-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc', '.npmrc', 'package-lock.json') }}-
|
||||
- name: Restore Knip analysis cache
|
||||
id: knip-cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: node_modules/.cache/knip
|
||||
# Each trusted commit publishes a new entry. Restore still matches the
|
||||
# configuration prefix from the previous successful full run.
|
||||
key: knip-meta-v1-${{ runner.os }}-${{ hashFiles('knip.json', 'tsconfig.json', 'package.json', 'package-lock.json') }}-${{ github.sha }}
|
||||
restore-keys: knip-meta-v1-${{ runner.os }}-${{ hashFiles('knip.json', 'tsconfig.json', 'package.json', 'package-lock.json') }}-
|
||||
- name: Record deep static task source and execution identity
|
||||
continue-on-error: true
|
||||
env:
|
||||
CI_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
CI_STATIC_CONTEXT_SCRIPT: check:ci:deep:tasks
|
||||
run: npm run ci:static-context
|
||||
- name: Run deep static checks
|
||||
run: npm run check:ci:deep
|
||||
- name: Save validated deep static task cache
|
||||
if: >-
|
||||
success() && steps.npm-cache.outputs.can-save == 'true' &&
|
||||
steps.static-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: .turbo/cache/
|
||||
key: ${{ steps.static-cache.outputs.cache-primary-key }}
|
||||
- name: Save Knip analysis cache
|
||||
if: >-
|
||||
success() && steps.npm-cache.outputs.can-save == 'true' &&
|
||||
steps.knip-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: node_modules/.cache/knip
|
||||
key: ${{ steps.knip-cache.outputs.cache-primary-key }}
|
||||
- name: Upload complete deep static diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: static-deep-check-reports
|
||||
path: .cache/static-checks/
|
||||
include-hidden-files: true
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
|
||||
python:
|
||||
name: Python producer tests
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true' && needs.changes.outputs.tier == 'fast'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
# Analysis-efficiency checks import compiled process modules.
|
||||
- name: Reuse portable build outputs across runs
|
||||
uses: ./.github/actions/portable-build-cache
|
||||
- run: npm run test:prepare
|
||||
- run: npm run test:python
|
||||
|
||||
test-source:
|
||||
name: Source tests
|
||||
needs: changes
|
||||
@@ -330,11 +218,9 @@ jobs:
|
||||
# every shard's diagnostics and coverage. Passing PRs run the full suite.
|
||||
fail-fast: ${{ github.event_name == 'pull_request' && needs.changes.outputs.tier == 'fast' }}
|
||||
matrix:
|
||||
# Fast feedback uses one source group and eight built groups so the
|
||||
# slowest built shard job stays inside a ten-minute execution budget.
|
||||
# Classification, build, and report merge sit outside that job.
|
||||
# Full qualification retains six complete shards with coverage.
|
||||
shard: ${{ fromJSON(needs.changes.outputs.tier == 'fast' && '["1/8","2/8","3/8","4/8","5/8","6/8","7/8","8/8"]' || '["1/6","2/6","3/6","4/6","5/6","6/6"]') }}
|
||||
# Fast feedback spends the same six test jobs on one source group and
|
||||
# five built groups; full qualification retains six complete shards.
|
||||
shard: ${{ fromJSON(needs.changes.outputs.tier == 'fast' && '["1/5","2/5","3/5","4/5","5/5"]' || '["1/6","2/6","3/6","4/6","5/6","6/6"]') }}
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
@@ -412,15 +298,11 @@ jobs:
|
||||
needs.changes.outputs.browser == 'true' ||
|
||||
needs.changes.outputs.binary_layout == 'true' ||
|
||||
needs.changes.outputs.evm == 'true' ||
|
||||
needs.changes.outputs.apk_signature == 'true' ||
|
||||
needs.changes.outputs.javascript_recovery == 'true' ||
|
||||
needs.changes.outputs.recorded_crash == 'true' ||
|
||||
needs.changes.outputs.web_network == 'true' ||
|
||||
needs.changes.outputs.web_runtime == 'true' ||
|
||||
needs.changes.outputs.web_source_map == 'true' ||
|
||||
needs.changes.outputs.agentenv == 'true' ||
|
||||
needs.changes.outputs.packet_capture == 'true' ||
|
||||
needs.changes.outputs.signal_recording == 'true'
|
||||
needs.changes.outputs.web_source_map == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
@@ -440,9 +322,7 @@ jobs:
|
||||
with:
|
||||
save: ${{ github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
||||
- run: npm run test:prepare
|
||||
# Fast and full shards measure different file costs. Download only the
|
||||
# matching history so one tier cannot pack the other with its weights.
|
||||
- name: Find available trusted timing history
|
||||
- name: Find available trusted full-suite timing history
|
||||
id: history
|
||||
continue-on-error: true
|
||||
env:
|
||||
@@ -450,14 +330,13 @@ jobs:
|
||||
CI_REPOSITORY: ${{ github.repository }}
|
||||
CI_REPOSITORY_ID: ${{ github.repository_id }}
|
||||
CI_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
CI_TIMING_TIER: ${{ needs.changes.outputs.tier }}
|
||||
run: node scripts/ci/test-history.mjs
|
||||
- name: Download previous timing report
|
||||
if: steps.history.outputs.run-id != ''
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: ${{ steps.history.outputs.artifact-name }}
|
||||
name: test-timings
|
||||
run-id: ${{ steps.history.outputs.run-id }}
|
||||
github-token: ${{ github.token }}
|
||||
path: .cache/ci-test-history
|
||||
@@ -478,23 +357,17 @@ jobs:
|
||||
# test:prepare caches only portable JS, skill, and catalog outputs.
|
||||
# Native package jobs still run their build and prepack checks; Turbo
|
||||
# restores matching outputs or rebuilds when their inputs differ.
|
||||
- name: Stage current portable build task cache
|
||||
shell: bash
|
||||
run: |
|
||||
# A new Turbo dry run can evict the entries this build just used.
|
||||
node scripts/ci/portable-build-cache.mjs snapshot < .cache/ci-build-plan.json
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: rea-build-cache
|
||||
path: .cache/ci-build-cache/
|
||||
path: .turbo/cache/
|
||||
include-hidden-files: true
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
sqlite-runtime:
|
||||
# Admit expensive host/provider work after fast static validation
|
||||
# (typecheck, lint, format). Deep static does not gate these lanes.
|
||||
# Portable tests still provide early feedback.
|
||||
# Preserve all selected coverage, but admit expensive host/provider work
|
||||
# only after static validation. Portable tests still provide early feedback.
|
||||
needs: [changes, static, build]
|
||||
if: needs.changes.outputs.sqlite == 'true'
|
||||
uses: ./.github/workflows/sqlite-runtime.yml
|
||||
@@ -589,22 +462,11 @@ jobs:
|
||||
include-hidden-files: true
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
# Default-branch runs only. Pull-request artifacts share this name and
|
||||
# the lookup reads one page, so publishing them would hide main.
|
||||
- name: Publish successful timing history
|
||||
if: >-
|
||||
success() &&
|
||||
needs.changes.outputs.code == 'true' &&
|
||||
needs.test-shard.result == 'success' &&
|
||||
(
|
||||
needs.changes.outputs.tier == 'full' ||
|
||||
(needs.changes.outputs.tier == 'fast' && needs.test-source.result == 'success')
|
||||
) &&
|
||||
github.ref == format('refs/heads/{0}', github.event.repository.default_branch) &&
|
||||
(github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
|
||||
if: needs.changes.outputs.code == 'true' && needs.changes.outputs.tier == 'full' && success() && needs.test-shard.result == 'success'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: test-timings-${{ needs.changes.outputs.tier }}
|
||||
name: test-timings
|
||||
path: .cache/ci-test-history/test-results.json
|
||||
include-hidden-files: true
|
||||
if-no-files-found: error
|
||||
@@ -708,9 +570,6 @@ jobs:
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
with:
|
||||
# ubuntu-latest is the Linux x64 writer above. Other hosts save their own key.
|
||||
save: ${{ matrix.os != 'ubuntu-latest' }}
|
||||
- name: Verify native runtime identity
|
||||
env:
|
||||
EXPECTED_CI_PLATFORM: ${{ matrix.platform }}
|
||||
@@ -779,23 +638,9 @@ jobs:
|
||||
npm run verify:legacy-simulator
|
||||
npm run verify:apple-dispatch
|
||||
npm run verify:interface-builder
|
||||
- name: Verify portable iNES CLI, MCP and source boundaries
|
||||
run: >-
|
||||
npm run test:focused --
|
||||
tests/acceptance/inesContainer.test.ts
|
||||
tests/boundary/filesystem/inesArtifact.test.ts
|
||||
tests/boundary/filesystem/inesCancellation.test.ts
|
||||
- name: Run native Apple and process boundary regressions
|
||||
run: >-
|
||||
npm run test:focused --
|
||||
tests/boundary/providers/hopper/hopperMacLaunchArguments.test.ts
|
||||
tests/acceptance/providers/hopperStartupRecovery.test.ts
|
||||
tests/boundary/providers/hopper/hopperClient.helperFailure.test.ts
|
||||
tests/boundary/providers/hopper/bridgeBootstrapCleanup.test.ts
|
||||
tests/boundary/providers/hopper/bridgeTargetLease.integration.test.ts
|
||||
tests/acceptance/artifactDmgAttachmentRecoveryCli.test.ts
|
||||
tests/boundary/providers/native/nativeDmgArtifactReader.test.ts
|
||||
tests/process-global/dmgCancellationReason.test.ts
|
||||
tests/boundary/filesystem/installScript.test.ts
|
||||
tests/boundary/filesystem/nibNativeHierarchy.test.ts
|
||||
tests/boundary/filesystem/interfaceBuilderBundle.test.ts
|
||||
@@ -803,15 +648,9 @@ jobs:
|
||||
tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts
|
||||
tests/boundary/cli/nativePlistSelectionFailureCli.test.ts
|
||||
tests/boundary/cli/nativeDemangleCli.test.ts
|
||||
tests/boundary/cli/nativeDispatchPropertyAttributes.test.ts
|
||||
tests/boundary/mcp/nativeDemangleMcp.test.ts
|
||||
tests/boundary/mcp/nativeMachoSummaryMcp.test.ts
|
||||
tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts
|
||||
tests/boundary/providers/native/nativeUiOffscreenWindow.test.ts
|
||||
tests/boundary/process/nativeUiVerifierLane.test.ts
|
||||
tests/boundary/cli/nativeSignatureCli.test.ts
|
||||
tests/boundary/cli/nativeMachoArm64_32Cli.test.ts
|
||||
tests/boundary/cli/nativeMachoEncryptionCli.test.ts
|
||||
tests/boundary/mcp/nativeSignatureTargetBindingMcp.test.ts
|
||||
src/process/capture/FilesystemSnapshot.test.ts
|
||||
tests/process-global/filesystemSnapshotFallbackCancellation.test.ts
|
||||
@@ -836,7 +675,7 @@ jobs:
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- name: Verify static CLI/MCP journeys and synthetic report conformance
|
||||
- name: Run the public CLI/MCP reconstruction journey
|
||||
env:
|
||||
REA_READINESS_REPORT_PATH: readiness-report.json
|
||||
run: |
|
||||
@@ -866,24 +705,17 @@ jobs:
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
with:
|
||||
save: "true"
|
||||
- run: npm ci
|
||||
- name: Typecheck with the official Windows compiler
|
||||
env:
|
||||
GOMAXPROCS: "2"
|
||||
GOMEMLIMIT: "3GiB"
|
||||
run: npm run typecheck -- --checkers 2
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: rea-build-cache
|
||||
path: .turbo/cache/
|
||||
- run: npm run build:cached
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: windows-native-controls
|
||||
path: native/windows/build/
|
||||
- run: npm run verify:windows-native:artifact
|
||||
- run: npm run build:cached
|
||||
- name: Record named Windows host capabilities
|
||||
run: node scripts/windows-capabilities.mjs | Out-File -Encoding utf8NoBOM windows-capabilities.json
|
||||
- name: Validate Windows host capability report
|
||||
@@ -899,53 +731,24 @@ jobs:
|
||||
# The matching native artifact establishes real Job Object and private
|
||||
# runtime boundaries for these fixtures. Ghidra and Java are not needed;
|
||||
# real engine claims remain in the separate controlled-provider workflow.
|
||||
# Run native proofs before the broad curated suite so an unrelated
|
||||
# fixture failure cannot hide the real control and recovery checks.
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: windows-process-boundary-fixture
|
||||
path: .cache/windows-process-boundary/
|
||||
# Passing the fixture adds the breakaway, caller/child token, environment
|
||||
# semantics and ancestor reparse observations, which the lane skips
|
||||
# entirely when it is absent.
|
||||
- run: >-
|
||||
npm run verify:windows-native -- .
|
||||
"${{ github.workspace }}\.cache\windows-process-boundary\process-boundary.exe"
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: windows-native-inspection-fixture
|
||||
path: .cache/windows-native-inspection-fixture/
|
||||
- name: Verify native inspection failure ownership and recovery
|
||||
run: node scripts/verify-windows-native-inspection.mjs
|
||||
- name: Run curated Windows capability tests
|
||||
run: >-
|
||||
npx vitest run --coverage.enabled=false
|
||||
src/process/capture/FilesystemSnapshot.test.ts
|
||||
tests/process-global/filesystemSnapshotFallbackCancellation.test.ts
|
||||
tests/boundary/filesystem/typescriptCompiler.test.ts
|
||||
tests/boundary/filesystem/releaseProposal.test.ts
|
||||
tests/boundary/filesystem/binaryTarget.test.ts
|
||||
src/contracts/sessionPathInputs.test.ts
|
||||
tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts
|
||||
tests/boundary/mcp/rawBinaryMcp.test.ts
|
||||
tests/boundary/filesystem/safeOutputTree.test.ts
|
||||
tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts
|
||||
tests/boundary/providers/native/artifactProvider.extractionCompleteness.test.ts
|
||||
tests/boundary/filesystem/setupUnavailableProvider.test.ts
|
||||
tests/boundary/filesystem/windowsNpxRegistration.test.ts
|
||||
tests/boundary/filesystem/claudeDesktopSkillUninstallWindows.test.ts
|
||||
tests/boundary/filesystem/clientConfigurationEnvironment.test.ts
|
||||
tests/boundary/filesystem/clientConfigurationLayeredEnvironment.test.ts
|
||||
tests/acceptance/setup/registrationRuntimeEnvironmentCli.test.ts
|
||||
tests/boundary/android/workspaceGuardianRetry.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/boundary/process/packageNpmInvocation.test.ts
|
||||
tests/boundary/process/packedBridgeVerification.test.ts
|
||||
tests/boundary/process/windowsOutputCapture.test.ts
|
||||
tests/boundary/process/windowsExecutableSelection.test.ts
|
||||
tests/acceptance/webModuleCli.test.ts
|
||||
tests/acceptance/webSourceLocationCli.test.ts
|
||||
tests/boundary/cli/referenceSourceImportStatus.test.ts
|
||||
@@ -967,9 +770,23 @@ jobs:
|
||||
src/process/ProcessOwnership.validation.test.ts
|
||||
src/application/Update.test.ts
|
||||
tests/process-global/updateMaintenance.test.ts
|
||||
tests/process-global/runnerCancellation.test.ts
|
||||
tests/process-global/windowsOwnedCommand.test.ts
|
||||
tests/conformance/providers/ghidra/realGhidraWindowsWorkflow.test.ts
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: windows-process-boundary-fixture
|
||||
path: .cache/windows-process-boundary/
|
||||
# Passing the fixture adds the breakaway, caller/child token, environment
|
||||
# semantics and ancestor reparse observations, which the lane skips
|
||||
# entirely when it is absent.
|
||||
- run: >-
|
||||
npm run verify:windows-native -- .
|
||||
"${{ github.workspace }}\.cache\windows-process-boundary\process-boundary.exe"
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: windows-native-inspection-fixture
|
||||
path: .cache/windows-native-inspection-fixture/
|
||||
- name: Verify native inspection failure ownership and recovery
|
||||
run: node scripts/verify-windows-native-inspection.mjs
|
||||
- name: Verify real Node Inspector discovery and observation
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
@@ -985,7 +802,7 @@ jobs:
|
||||
tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22.23.4
|
||||
node-version: 22.19.0
|
||||
- name: Verify native path boundaries on Node 22
|
||||
run: >-
|
||||
npx vitest run --coverage.enabled=false
|
||||
@@ -1001,7 +818,7 @@ jobs:
|
||||
retention-days: 30
|
||||
|
||||
docs:
|
||||
name: Documentation
|
||||
name: Docs Generation
|
||||
needs: changes
|
||||
if: needs.changes.outputs.docs == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
@@ -1016,8 +833,6 @@ jobs:
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- run: npm run docs:check
|
||||
- name: Render documentation and validate page links
|
||||
run: npm run docs:build
|
||||
- name: Generation leaves authored files unchanged
|
||||
run: git diff --exit-code
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
@@ -1029,126 +844,61 @@ jobs:
|
||||
skills/reverse-engineer-anything
|
||||
if-no-files-found: error
|
||||
|
||||
# Provider lanes wait for fast static and the portable build, not deep static.
|
||||
browser:
|
||||
needs: [changes, static, build]
|
||||
if: needs.changes.outputs.browser == 'true'
|
||||
secrets:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
uses: ./.github/workflows/real-browser.yml
|
||||
with:
|
||||
build_cache_artifact: rea-build-cache
|
||||
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
|
||||
|
||||
binary_layout:
|
||||
needs: [changes, static, build]
|
||||
if: needs.changes.outputs.binary_layout == 'true'
|
||||
secrets:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
uses: ./.github/workflows/real-binary-layout.yml
|
||||
with:
|
||||
build_cache_artifact: rea-build-cache
|
||||
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
|
||||
|
||||
evm:
|
||||
needs: [changes, static, build]
|
||||
if: needs.changes.outputs.evm == 'true'
|
||||
secrets:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
uses: ./.github/workflows/real-evm-interface.yml
|
||||
with:
|
||||
build_cache_artifact: rea-build-cache
|
||||
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
|
||||
|
||||
apk_signature:
|
||||
needs: [changes, static, build]
|
||||
if: needs.changes.outputs.apk_signature == 'true'
|
||||
secrets:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
uses: ./.github/workflows/real-apk-signature.yml
|
||||
with:
|
||||
build_cache_artifact: rea-build-cache
|
||||
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
|
||||
|
||||
javascript_recovery:
|
||||
needs: [changes, static, build]
|
||||
if: needs.changes.outputs.javascript_recovery == 'true'
|
||||
secrets:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
uses: ./.github/workflows/real-javascript-recovery.yml
|
||||
with:
|
||||
build_cache_artifact: rea-build-cache
|
||||
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
|
||||
|
||||
recorded_crash:
|
||||
needs: [changes, static, build]
|
||||
if: needs.changes.outputs.recorded_crash == 'true'
|
||||
secrets:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
uses: ./.github/workflows/real-recorded-crash.yml
|
||||
with:
|
||||
build_cache_artifact: rea-build-cache
|
||||
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
|
||||
|
||||
web_network:
|
||||
needs: [changes, static, build]
|
||||
if: needs.changes.outputs.web_network == 'true'
|
||||
secrets:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
uses: ./.github/workflows/real-web-network-captures.yml
|
||||
with:
|
||||
build_cache_artifact: rea-build-cache
|
||||
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
|
||||
|
||||
web_runtime:
|
||||
needs: [changes, static, build]
|
||||
if: needs.changes.outputs.web_runtime == 'true'
|
||||
secrets:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
uses: ./.github/workflows/real-web-runtime.yml
|
||||
with:
|
||||
build_cache_artifact: rea-build-cache
|
||||
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
|
||||
|
||||
web_source_map:
|
||||
needs: [changes, static, build]
|
||||
if: needs.changes.outputs.web_source_map == 'true'
|
||||
secrets:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
uses: ./.github/workflows/real-web-source-map.yml
|
||||
with:
|
||||
build_cache_artifact: rea-build-cache
|
||||
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
|
||||
|
||||
agentenv:
|
||||
needs: [changes, static, build]
|
||||
if: needs.changes.outputs.agentenv == 'true'
|
||||
secrets:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
uses: ./.github/workflows/real-agentenv.yml
|
||||
with:
|
||||
build_cache_artifact: rea-build-cache
|
||||
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
|
||||
|
||||
packet_capture:
|
||||
needs: [changes, static, build]
|
||||
if: needs.changes.outputs.packet_capture == 'true'
|
||||
secrets:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
uses: ./.github/workflows/real-packet-capture.yml
|
||||
with:
|
||||
build_cache_artifact: rea-build-cache
|
||||
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
|
||||
|
||||
signal_recording:
|
||||
needs: [changes, static, build]
|
||||
if: needs.changes.outputs.signal_recording == 'true'
|
||||
secrets:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
uses: ./.github/workflows/real-signal-recording.yml
|
||||
with:
|
||||
build_cache_artifact: rea-build-cache
|
||||
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
|
||||
|
||||
nativeaot:
|
||||
needs: [changes, static]
|
||||
@@ -1173,12 +923,8 @@ jobs:
|
||||
[
|
||||
changes,
|
||||
static,
|
||||
static-deep,
|
||||
build,
|
||||
reverse-tools-quality,
|
||||
python,
|
||||
packet_capture,
|
||||
signal_recording,
|
||||
test-shard,
|
||||
test-source,
|
||||
sqlite-runtime,
|
||||
@@ -1193,13 +939,11 @@ jobs:
|
||||
browser,
|
||||
binary_layout,
|
||||
evm,
|
||||
apk_signature,
|
||||
javascript_recovery,
|
||||
recorded_crash,
|
||||
web_network,
|
||||
web_runtime,
|
||||
web_source_map,
|
||||
agentenv,
|
||||
nativeaot,
|
||||
ghidra_bridge,
|
||||
website,
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
name: CodeQL production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: "37 5 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: codeql-production-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
# A repository administrator must disable default setup before enabling
|
||||
# this workflow; default setup rejects advanced CodeQL result uploads.
|
||||
if: vars.REA_CODEQL_ADVANCED == 'true'
|
||||
name: Analyze production (${{ matrix.language }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
language: [javascript-typescript, python, actions]
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Initialize production analysis
|
||||
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: none
|
||||
config-file: .github/codeql/production.yml
|
||||
threads: 2
|
||||
ram: 4096
|
||||
- name: Analyze production code
|
||||
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4
|
||||
with:
|
||||
category: /language:${{ matrix.language }}
|
||||
output: ${{ runner.temp }}/codeql-production-results
|
||||
threads: 2
|
||||
ram: 4096
|
||||
- name: Retain complete analysis evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: codeql-production-${{ matrix.language }}
|
||||
path: ${{ runner.temp }}/codeql-production-results
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
@@ -25,16 +25,16 @@ jobs:
|
||||
node-version-file: .nvmrc
|
||||
- name: Validate the SDK pin against the provider's verified release
|
||||
run: node scripts/verify-ghidra-bridge-sources.mjs --check-sdk-pin >> "$GITHUB_ENV"
|
||||
# The bridge probe imports only source-owned ARM/Thumb bytes and never
|
||||
# executes target code. All SDK/project state stays in runner storage.
|
||||
- name: Acquire checksum-verified Ghidra SDK in isolated runner storage
|
||||
# Only the jars and application.properties are extracted: the check
|
||||
# compiles against them and never launches Ghidra or imports a target.
|
||||
- name: Acquire checksum-verified Ghidra jars in isolated runner storage
|
||||
shell: bash
|
||||
run: |
|
||||
ghidra_root="${RUNNER_TEMP}/rea-ghidra"
|
||||
mkdir -p "${ghidra_root}"
|
||||
curl -fsSL --retry 3 --write-out 'ghidra_download_bytes=%{size_download} ghidra_download_seconds=%{time_total}\n' -o "${ghidra_root}/ghidra.zip" "${GHIDRA_RELEASE_URL}"
|
||||
echo "${GHIDRA_RELEASE_SHA256} ${ghidra_root}/ghidra.zip" | sha256sum -c -
|
||||
unzip -q "${ghidra_root}/ghidra.zip" -d "${ghidra_root}"
|
||||
unzip -q "${ghidra_root}/ghidra.zip" '*/Ghidra/*.jar' '*/Ghidra/application.properties' -d "${ghidra_root}"
|
||||
rm "${ghidra_root}/ghidra.zip"
|
||||
properties="$(find "${ghidra_root}" -path '*/Ghidra/application.properties' -print -quit)"
|
||||
test -n "${properties}"
|
||||
@@ -46,52 +46,3 @@ jobs:
|
||||
java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64"
|
||||
test -n "${!java_home_variable}"
|
||||
JAVA_HOME="${!java_home_variable}" npm run verify:ghidra:bridge-sources -- --verified-sdk
|
||||
- name: Inspect real ARM and Thumb instruction context through the bridge
|
||||
shell: bash
|
||||
run: |
|
||||
java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64"
|
||||
export JAVA_HOME="${!java_home_variable}"
|
||||
probe_root="${RUNNER_TEMP}/rea-arm-context"
|
||||
mkdir -p "${probe_root}/project"
|
||||
printf '\036\377\057\341\160\107\000\000' > "${probe_root}/arm-thumb.bin"
|
||||
timeout 180s "${GHIDRA_INSTALL_DIR}/support/analyzeHeadless" "${probe_root}/project" ArmContext \
|
||||
-import "${probe_root}/arm-thumb.bin" -loader BinaryLoader -processor ARM:LE:32:v7 \
|
||||
-noanalysis -readOnly -deleteProject \
|
||||
-scriptPath "${GITHUB_WORKSPACE}/tests/conformance/ghidra;${GITHUB_WORKSPACE}/bridge/ghidra" \
|
||||
-postScript ReaArmContextProbe.java "${GITHUB_WORKSPACE}/bridge/ghidra" \
|
||||
> "${probe_root}/output.log" 2>&1 || { cat "${probe_root}/output.log"; exit 1; }
|
||||
cat "${probe_root}/output.log"
|
||||
grep -F 'REA_ARM_CONTEXT_OK arm=0 thumb=1 unavailable=null (injected provider value)' "${probe_root}/output.log"
|
||||
# Each probe imports source-owned bytes at 0x1000, away from the Cortex-M
|
||||
# vector symbols that ARMCortex.pspec defines at 0, and decodes them from
|
||||
# the language's default context, so M-profile Thumb state must hold at
|
||||
# the first byte. The bytes are never executed.
|
||||
- name: Decode M-profile ARM and Xtensa bytes in their default context
|
||||
shell: bash
|
||||
run: |
|
||||
java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64"
|
||||
export JAVA_HOME="${!java_home_variable}"
|
||||
probe_root="${RUNNER_TEMP}/rea-decode"
|
||||
mkdir -p "${probe_root}"
|
||||
# mrs r0, msp; bx lr
|
||||
printf '\357\363\010\200\160\107' > "${probe_root}/cortex.bin"
|
||||
# mrs r0, msplim (ARMv8-M only); bx lr
|
||||
printf '\357\363\012\200\160\107' > "${probe_root}/v8m.bin"
|
||||
# entry a1, 32; retw.n
|
||||
printf '\066\101\000\035\360' > "${probe_root}/xtensa.bin"
|
||||
decode() {
|
||||
local name="$1" language="$2"
|
||||
shift 2
|
||||
mkdir -p "${probe_root}/${name}-project"
|
||||
timeout 180s "${GHIDRA_INSTALL_DIR}/support/analyzeHeadless" "${probe_root}/${name}-project" Decode \
|
||||
-import "${probe_root}/${name}.bin" -loader BinaryLoader -loader-baseAddr 0x1000 \
|
||||
-processor "${language}" -noanalysis -readOnly -deleteProject \
|
||||
-scriptPath "${GITHUB_WORKSPACE}/tests/conformance/ghidra" \
|
||||
-postScript ReaDecodeProbe.java "${language}" "$@" \
|
||||
> "${probe_root}/${name}.log" 2>&1 || { cat "${probe_root}/${name}.log"; exit 1; }
|
||||
cat "${probe_root}/${name}.log"
|
||||
grep -F "REA_DECODE_OK ${language} $*" "${probe_root}/${name}.log"
|
||||
}
|
||||
decode cortex ARM:LE:32:Cortex mrs/4/msp bx/2/lr
|
||||
decode v8m ARM:LE:32:v8-m mrs/4/msplim bx/2/lr
|
||||
decode xtensa Xtensa:LE:32:default entry/3/a1 retw.n/2
|
||||
@@ -14,11 +14,6 @@ concurrency:
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
# Writes only on the default branch for push, schedule, manual dispatch,
|
||||
# and workflow_call. Pull requests get an empty token and remote read-only mode.
|
||||
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
|
||||
|
||||
jobs:
|
||||
mutation:
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
name: Real agentenv environment bundle
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
build_cache_artifact:
|
||||
description: Portable build cache artifact from the calling CI run.
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
remote_cache_write:
|
||||
description: Trusted release qualification may publish the remote cache.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
TURBO_TOKEN:
|
||||
description: Remote cache token. Only trusted callers pass it.
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
# A called workflow sees the caller event, so workflow_call is not an event
|
||||
# name here. Writes when remote_cache_write is set, or on the default branch
|
||||
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
|
||||
# and use the local cache only.
|
||||
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
|
||||
NODE_OPTIONS: --max-old-space-size=768
|
||||
PIP_DISABLE_PIP_VERSION_CHECK: "1"
|
||||
|
||||
jobs:
|
||||
real-agentenv:
|
||||
name: Export and run an environment bundle with the agentenv runner
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- uses: ./.github/actions/portable-build-cache
|
||||
with:
|
||||
build_cache_artifact: ${{ inputs.build_cache_artifact }}
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
# Public ECR caps anonymous pulls by data per source IP and hosted runners share IPs, so the
|
||||
# framework's image builds can fail here for hours. mirror.gcr.io serves the same Docker
|
||||
# official images under the same digests, so each base image the pinned framework builds from
|
||||
# is pulled there and named as its Dockerfile names it, as the framework's own CI does.
|
||||
# Digest-pinned names are set in containerd, whose images Docker reads only on the containerd
|
||||
# image store; switching stores restarts dockerd, so it happens before anything uses Docker.
|
||||
- name: Use the containerd image store
|
||||
run: |
|
||||
[ -s /etc/docker/daemon.json ] || echo '{}' | sudo tee /etc/docker/daemon.json >/dev/null
|
||||
jq '.features["containerd-snapshotter"] = true' /etc/docker/daemon.json > "$RUNNER_TEMP/daemon.json"
|
||||
sudo mv "$RUNNER_TEMP/daemon.json" /etc/docker/daemon.json
|
||||
sudo systemctl restart docker
|
||||
docker info --format '{{json .DriverStatus}}' | grep -q io.containerd.snapshotter
|
||||
- name: Acquire the pinned agentenv runner in isolated runner storage and check host Docker
|
||||
shell: bash
|
||||
run: |
|
||||
runner_root="${RUNNER_TEMP}/rea-agentenv-runner"
|
||||
python -m venv "${runner_root}"
|
||||
"${runner_root}/bin/python" -m pip install --require-virtualenv --no-cache-dir 'agentenv-framework==0.9.1334'
|
||||
"${runner_root}/bin/agent-env" --version
|
||||
docker version --format '{{.Server.Version}}'
|
||||
echo "REA_AGENT_ENV_COMMAND=${runner_root}/bin/agent-env" >> "${GITHUB_ENV}"
|
||||
echo "REA_DOCKER_COMMAND=$(command -v docker)" >> "${GITHUB_ENV}"
|
||||
- name: Pull the pinned framework's public ECR base images from mirror.gcr.io
|
||||
shell: bash
|
||||
run: |
|
||||
package="$("${REA_AGENT_ENV_COMMAND%/agent-env}/python" -c 'import agent_env, pathlib; print(pathlib.Path(agent_env.__file__).parent)')"
|
||||
grep -rhoE '^FROM +public\.ecr\.aws/docker/library/[^ ]+' --include='Dockerfile*' "${package}" | awk '{print $2}' | sort -u |
|
||||
while read -r image; do
|
||||
name=${image#public.ecr.aws/docker/library/}
|
||||
source=mirror.gcr.io/library/$name
|
||||
case $name in *@*) source=mirror.gcr.io/library/${name%%[:@]*}@${name#*@} ;; esac
|
||||
for attempt in 1 2 3; do
|
||||
docker pull --quiet "$source" && break
|
||||
if [ "$attempt" = 3 ]; then echo "::warning::couldn't pull $source; $image's build will pull it"; continue 2; fi
|
||||
sleep $((attempt * 10))
|
||||
done
|
||||
case ${name%@*} in *:*) docker tag "$source" "${image%@*}" ;; esac
|
||||
case $image in *@*) sudo ctr --namespace moby images tag "$source" "$image" >/dev/null ;; esac
|
||||
done
|
||||
- name: Export a bundle from the source-owned capture and run it with the agentenv runner
|
||||
run: npm run verify:agentenv:environment
|
||||
@@ -1,63 +0,0 @@
|
||||
name: Real Android metadata bridge
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["**"]
|
||||
paths:
|
||||
- .github/workflows/real-android-metadata.yml
|
||||
- bridge/android/**
|
||||
- src/android/JadxRelease.ts
|
||||
- scripts/fixtures/android/**
|
||||
- tests/conformance/android/**
|
||||
- tests/fixtures/android/static-initializers/**
|
||||
- tests/fixtures/android/direct-invokes/**
|
||||
- tests/fixtures/android/external-invokes/**
|
||||
- .nvmrc
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: android-metadata-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
android-metadata:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Fetch checksum-pinned engine and public APK fixture
|
||||
run: node scripts/fixtures/android/fetch.mjs "$RUNNER_TEMP/rea-android"
|
||||
- name: Compile and exercise the real metadata bridge
|
||||
shell: bash
|
||||
run: |
|
||||
export JAVA_HOME="$JAVA_HOME_21_X64"
|
||||
android_root="$RUNNER_TEMP/rea-android"
|
||||
classes="$android_root/classes"
|
||||
jar="$android_root/jadx-headless-mcp-0.7.1-all.jar"
|
||||
mkdir -p "$classes"
|
||||
"$JAVA_HOME/bin/javac" -J-Xmx512m -J-XX:ActiveProcessorCount=2 \
|
||||
-classpath "$jar" -d "$classes" \
|
||||
bridge/android/ReaJadxBridge.java \
|
||||
tests/conformance/android/ReaJadxMetadataRegression.java \
|
||||
tests/conformance/android/ReaJadxExternalReferencesRegression.java
|
||||
"$JAVA_HOME/bin/java" -Xmx512m -XX:ActiveProcessorCount=2 \
|
||||
-classpath "$classes:$jar" ReaJadxMetadataRegression \
|
||||
"$android_root/ApiDemos-debug.apk" \
|
||||
"$GITHUB_WORKSPACE/tests/fixtures/android/static-initializers" \
|
||||
"$classes" "$GITHUB_WORKSPACE/tests/fixtures/android/direct-invokes" \
|
||||
| tee "$android_root/metadata.log"
|
||||
grep -F 'RAW_CLASS_SELECTOR_REGRESSION_PASS nested=true metadata=true references=true' \
|
||||
"$android_root/metadata.log"
|
||||
"$JAVA_HOME/bin/java" -Xmx512m -XX:ActiveProcessorCount=2 \
|
||||
-classpath "$classes:$jar" ReaJadxExternalReferencesRegression \
|
||||
"$GITHUB_WORKSPACE/tests/fixtures/android/external-invokes" "$classes" \
|
||||
| tee "$android_root/external-references.log"
|
||||
grep -F 'EXTERNAL_REFERENCE_REGRESSION_PASS prototypes=2 using_nodes=3 sites=4' \
|
||||
"$android_root/external-references.log"
|
||||
@@ -1,97 +0,0 @@
|
||||
name: Real APK signature inspection
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
build_cache_artifact:
|
||||
description: Portable build cache artifact from the calling CI run.
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
remote_cache_write:
|
||||
description: Trusted release qualification may publish the remote cache.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
TURBO_TOKEN:
|
||||
description: Remote cache token. Only trusted callers pass it.
|
||||
required: false
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
# A called workflow sees the caller event, so workflow_call is not an event
|
||||
# name here. Writes when remote_cache_write is set, or on the default branch
|
||||
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
|
||||
# and use the local cache only.
|
||||
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
|
||||
NODE_OPTIONS: "--max-old-space-size=768"
|
||||
RAYON_NUM_THREADS: "1"
|
||||
GOMAXPROCS: "2"
|
||||
UV_THREADPOOL_SIZE: "2"
|
||||
JAVA_TOOL_OPTIONS: "-Xmx512m -XX:ActiveProcessorCount=2"
|
||||
|
||||
jobs:
|
||||
real-apk-signature:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 12
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- uses: ./.github/actions/portable-build-cache
|
||||
with:
|
||||
build_cache_artifact: ${{ inputs.build_cache_artifact }}
|
||||
- name: Prepare pinned SDK oracle without installing host software
|
||||
shell: bash
|
||||
run: |
|
||||
python3 - <<'PYTHON'
|
||||
import hashlib, os, urllib.request, zipfile
|
||||
from pathlib import Path
|
||||
root = Path(os.environ["RUNNER_TEMP"]) / "rea-apk-signature-sdk"
|
||||
packages = [
|
||||
("build-tools_r36.1_linux.zip", "a7b5889e4a79fcf3b0976bef40d401f4240fb1eed891d9d91169da1111e11d78", 64195204, "build-tools/36.1.0", {"apksigner", "aapt2", "lib/apksigner.jar", "source.properties"}),
|
||||
("platform-36_r02.zip", "37607369a28c5b640b3a7998868d45898ebcb777565a0e85f9acf36f29631d2e", 65878410, "platforms/android-36", {"android.jar", "source.properties"}),
|
||||
]
|
||||
for name, expected, expected_size, directory, selected in packages:
|
||||
archive = root / name
|
||||
archive.parent.mkdir(parents=True, exist_ok=True)
|
||||
digest = hashlib.sha256()
|
||||
total = 0
|
||||
with urllib.request.urlopen("https://dl.google.com/android/repository/" + name, timeout=60) as response, archive.open("wb") as output:
|
||||
while chunk := response.read(1024 * 1024):
|
||||
total += len(chunk)
|
||||
if total > expected_size:
|
||||
raise RuntimeError("SDK archive exceeds the pinned package size budget")
|
||||
digest.update(chunk)
|
||||
output.write(chunk)
|
||||
if total != expected_size or digest.hexdigest() != expected:
|
||||
raise RuntimeError("SDK oracle archive digest mismatch")
|
||||
extracted = set()
|
||||
with zipfile.ZipFile(archive) as source:
|
||||
for entry in source.infolist():
|
||||
relative = entry.filename.partition("/")[2]
|
||||
if relative not in selected:
|
||||
continue
|
||||
target = root / directory / relative
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(source.read(entry))
|
||||
target.chmod((entry.external_attr >> 16) & 0o777 or 0o644)
|
||||
extracted.add(relative)
|
||||
if extracted != selected:
|
||||
raise RuntimeError("Pinned SDK package is missing required oracle files")
|
||||
with Path(os.environ["GITHUB_ENV"]).open("a") as environment:
|
||||
environment.write("ANDROID_HOME=" + str(root) + "\n")
|
||||
PYTHON
|
||||
- run: npm run verify:apk-signature -- --require-tools
|
||||
@@ -14,15 +14,6 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
remote_cache_write:
|
||||
description: Trusted release qualification may publish the remote cache.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
TURBO_TOKEN:
|
||||
description: Remote cache token. Only trusted callers pass it.
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -30,13 +21,6 @@ permissions:
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=768
|
||||
HUSKY: "0"
|
||||
# A called workflow sees the caller event, so workflow_call is not an event
|
||||
# name here. Writes when remote_cache_write is set, or on the default branch
|
||||
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
|
||||
# and use the local cache only.
|
||||
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
|
||||
REA_VERIFY_LARGE_ELF_MCP: ${{ inputs.large_mcp && '1' || '0' }}
|
||||
|
||||
jobs:
|
||||
@@ -55,12 +39,6 @@ jobs:
|
||||
- uses: ./.github/actions/portable-build-cache
|
||||
with:
|
||||
build_cache_artifact: ${{ inputs.build_cache_artifact }}
|
||||
- name: Install AArch64 and ELF32 cross toolchains for source-owned fixtures
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes --no-install-recommends \
|
||||
gcc-aarch64-linux-gnu gcc-i686-linux-gnu gcc-arm-linux-gnueabi gcc-mips-linux-gnu gcc-mipsel-linux-gnu \
|
||||
libc6-dev-arm64-cross libc6-dev-i386-cross libc6-dev-armel-cross libc6-dev-mips-cross libc6-dev-mipsel-cross
|
||||
- name: Acquire exact upstream libraries in an isolated runner environment
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -8,26 +8,10 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
remote_cache_write:
|
||||
description: Trusted release qualification may publish the remote cache.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
TURBO_TOKEN:
|
||||
description: Remote cache token. Only trusted callers pass it.
|
||||
required: false
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
# A called workflow sees the caller event, so workflow_call is not an event
|
||||
# name here. Writes when remote_cache_write is set, or on the default branch
|
||||
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
|
||||
# and use the local cache only.
|
||||
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -48,13 +32,7 @@ jobs:
|
||||
- uses: ./.github/actions/portable-build-cache
|
||||
with:
|
||||
build_cache_artifact: ${{ inputs.build_cache_artifact }}
|
||||
# Own a fresh bus so Chromium never depends on an inherited session address.
|
||||
- run: dbus-run-session -- npm run verify:browser
|
||||
env:
|
||||
REA_BROWSER_EXECUTABLE: /usr/bin/google-chrome
|
||||
REA_BROWSER_NO_SANDBOX: "true"
|
||||
# The preceding verifier builds the runtime used by this request journey.
|
||||
- run: dbus-run-session -- node scripts/verify-browser-request.mjs
|
||||
- run: npm run verify:browser
|
||||
env:
|
||||
REA_BROWSER_EXECUTABLE: /usr/bin/google-chrome
|
||||
REA_BROWSER_NO_SANDBOX: "true"
|
||||
@@ -8,14 +8,6 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
# Writes only on the default branch for push, schedule, manual dispatch,
|
||||
# and workflow_call. Pull requests get an empty token and remote read-only mode.
|
||||
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
|
||||
|
||||
jobs:
|
||||
windows-native-artifact:
|
||||
name: Build Windows bridge security boundary
|
||||
|
||||
@@ -9,15 +9,6 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
remote_cache_write:
|
||||
description: Trusted release qualification may publish the remote cache.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
TURBO_TOKEN:
|
||||
description: Remote cache token. Only trusted callers pass it.
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -25,13 +16,6 @@ permissions:
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=768
|
||||
HUSKY: "0"
|
||||
# A called workflow sees the caller event, so workflow_call is not an event
|
||||
# name here. Writes when remote_cache_write is set, or on the default branch
|
||||
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
|
||||
# and use the local cache only.
|
||||
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
|
||||
|
||||
jobs:
|
||||
real-evm-interface:
|
||||
|
||||
@@ -6,11 +6,6 @@ on:
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
# Writes only on the default branch for push, schedule, manual dispatch,
|
||||
# and workflow_call. Pull requests get an empty token and remote read-only mode.
|
||||
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -13,8 +13,20 @@ on:
|
||||
- "src/composition/go.ts"
|
||||
- "src/cli/goCommands.ts"
|
||||
- "src/server/registerGoTools.ts"
|
||||
- "src/server/createServer.ts"
|
||||
- "src/cli.ts"
|
||||
- "src/cliCommandNames.ts"
|
||||
- "src/contracts/toolContracts.ts"
|
||||
- "src/contracts/toolEffects.ts"
|
||||
- "src/artifacts/readStableArtifact*.ts"
|
||||
- "src/artifacts/ArtifactReader.ts"
|
||||
- "src/process/PrivateRuntimeRoot.ts"
|
||||
- "scripts/verify-go-binary.mjs"
|
||||
- "scripts/lib/mcp-verifier-results.mjs"
|
||||
- "scripts/rea.mjs"
|
||||
- "package.json"
|
||||
- "package-lock.json"
|
||||
- ".nvmrc"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -26,11 +38,6 @@ concurrency:
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=768
|
||||
HUSKY: "0"
|
||||
# Writes only on the default branch for push, schedule, manual dispatch,
|
||||
# and workflow_call. Pull requests get an empty token and remote read-only mode.
|
||||
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
|
||||
|
||||
jobs:
|
||||
real-go-binary:
|
||||
|
||||
@@ -5,11 +5,6 @@ on:
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
# Writes only on the default branch for push, schedule, manual dispatch,
|
||||
# and workflow_call. Pull requests get an empty token and remote read-only mode.
|
||||
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -5,11 +5,6 @@ on:
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
# Writes only on the default branch for push, schedule, manual dispatch,
|
||||
# and workflow_call. Pull requests get an empty token and remote read-only mode.
|
||||
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -8,15 +8,6 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
remote_cache_write:
|
||||
description: Trusted release qualification may publish the remote cache.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
TURBO_TOKEN:
|
||||
description: Remote cache token. Only trusted callers pass it.
|
||||
required: false
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -24,13 +15,6 @@ permissions:
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
# A called workflow sees the caller event, so workflow_call is not an event
|
||||
# name here. Writes when remote_cache_write is set, or on the default branch
|
||||
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
|
||||
# and use the local cache only.
|
||||
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
|
||||
NODE_OPTIONS: "--max-old-space-size=768"
|
||||
RAYON_NUM_THREADS: "1"
|
||||
|
||||
|
||||
@@ -1,35 +1,43 @@
|
||||
name: Real packet capture
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
build_cache_artifact:
|
||||
description: Portable build cache artifact from the calling CI run.
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
remote_cache_write:
|
||||
description: Trusted release qualification may publish the remote cache.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
TURBO_TOKEN:
|
||||
description: Remote cache token. Only trusted callers pass it.
|
||||
required: false
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
# A called workflow sees the caller event, so workflow_call is not an event
|
||||
# name here. Writes when remote_cache_write is set, or on the default branch
|
||||
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
|
||||
# and use the local cache only.
|
||||
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "tests/boundary/captures/**"
|
||||
- "tests/boundary/cli/captureCleanup.test.ts"
|
||||
- "tests/boundary/mcp/packetCaptureMcp.test.ts"
|
||||
- "src/captures/**"
|
||||
- "src/pcap/**"
|
||||
- "src/domain/pcap/**"
|
||||
- "src/domain/analysisView/pcapCaptureView.ts"
|
||||
- "src/application/pcap/**"
|
||||
- "src/contracts/pcap/**"
|
||||
- "src/server/registerPcapTools.ts"
|
||||
- "tests/boundary/pcap/**"
|
||||
- "src/domain/captures/**"
|
||||
- "src/application/captures/**"
|
||||
- "src/contracts/captures/**"
|
||||
- "src/composition/captures.ts"
|
||||
- "src/cli/captureCommands.ts"
|
||||
- "src/server/registerCaptureTools.ts"
|
||||
- "src/server/createServer.ts"
|
||||
- "src/server/sessionAvailabilityPolicy.ts"
|
||||
- "src/application/CapabilityInventory.ts"
|
||||
- "src/contracts/toolContracts.ts"
|
||||
- "src/contracts/toolEffects.ts"
|
||||
- "src/cli.ts"
|
||||
- "src/cliCommandNames.ts"
|
||||
- "src/process/**"
|
||||
- "src/artifacts/readStableArtifact.ts"
|
||||
- "scripts/verify-packet-capture.mjs"
|
||||
- ".github/workflows/real-packet-capture.yml"
|
||||
- "package*.json"
|
||||
- ".nvmrc"
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: real-packet-capture-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
tshark:
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -44,18 +52,8 @@ jobs:
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y tshark util-linux python3
|
||||
- run: npm ci
|
||||
- uses: ./.github/actions/portable-build-cache
|
||||
with:
|
||||
build_cache_artifact: ${{ inputs.build_cache_artifact }}
|
||||
- run: npm run build:cached
|
||||
- name: Run focused tests and require Linux resource and cleanup cases
|
||||
run: |
|
||||
report=".cache/rea-packet-required-tests.json"
|
||||
mkdir -p .cache
|
||||
rm -f "$report"
|
||||
node scripts/run-exact-tests.mjs run --reporter=default --reporter=json --outputFile.json="$report" \
|
||||
tests/boundary/captures/fieldDiscovery.test.ts src/process/LinuxResourceLimits.test.ts tests/boundary/captures/captureProvider.test.ts tests/boundary/captures/captureDescriptorCleanup.test.ts tests/boundary/pcap/pcapProvider.test.ts tests/boundary/pcap/pcapProviderCrossPlatform.test.ts tests/boundary/captures/resourceLimits.test.ts tests/boundary/captures/packetOutput.test.ts tests/boundary/mcp/packetCaptureMcp.test.ts tests/boundary/cli/captureCleanup.test.ts tests/acceptance/pcapVerifier.test.ts
|
||||
node scripts/ci/require-provider-tests.mjs packet "$report"
|
||||
- run: npm run test:focused -- tests/boundary/captures/fieldDiscovery.test.ts src/process/LinuxResourceLimits.test.ts tests/boundary/captures/captureProvider.test.ts tests/boundary/captures/captureDescriptorCleanup.test.ts tests/boundary/captures/resourceLimits.test.ts tests/boundary/captures/packetOutput.test.ts tests/boundary/mcp/packetCaptureMcp.test.ts tests/boundary/cli/captureCleanup.test.ts
|
||||
- run: node scripts/verify-packet-capture.mjs
|
||||
env:
|
||||
REA_TSHARK_COMMAND: /usr/bin/tshark
|
||||
|
||||
@@ -9,15 +9,6 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
remote_cache_write:
|
||||
description: Trusted release qualification may publish the remote cache.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
TURBO_TOKEN:
|
||||
description: Remote cache token. Only trusted callers pass it.
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -25,13 +16,6 @@ permissions:
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=768
|
||||
HUSKY: "0"
|
||||
# A called workflow sees the caller event, so workflow_call is not an event
|
||||
# name here. Writes when remote_cache_write is set, or on the default branch
|
||||
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
|
||||
# and use the local cache only.
|
||||
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
|
||||
UV_CONCURRENT_BUILDS: "1"
|
||||
UV_CONCURRENT_INSTALLS: "1"
|
||||
UV_PYTHON_DOWNLOADS: never
|
||||
@@ -40,19 +24,7 @@ jobs:
|
||||
real-recorded-crash:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image: ["ubuntu:24.04", "debian:trixie-slim"]
|
||||
container: ${{ matrix.image }}
|
||||
steps:
|
||||
- name: Install fixture and debugger prerequisites in the disposable container
|
||||
shell: bash
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install --yes --no-install-recommends \
|
||||
ca-certificates git python3-venv gcc libc6-dev strace gdb
|
||||
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
@@ -64,6 +36,11 @@ jobs:
|
||||
- uses: ./.github/actions/portable-build-cache
|
||||
with:
|
||||
build_cache_artifact: ${{ inputs.build_cache_artifact }}
|
||||
- name: Install GDB on the disposable CI runner
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install --yes --no-install-recommends gdb
|
||||
- name: Acquire exact unchanged upstreams in isolated runner storage
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -1,35 +1,36 @@
|
||||
name: Real signal recording
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
build_cache_artifact:
|
||||
description: Portable build cache artifact from the calling CI run.
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
remote_cache_write:
|
||||
description: Trusted release qualification may publish the remote cache.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
TURBO_TOKEN:
|
||||
description: Remote cache token. Only trusted callers pass it.
|
||||
required: false
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
# A called workflow sees the caller event, so workflow_call is not an event
|
||||
# name here. Writes when remote_cache_write is set, or on the default branch
|
||||
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
|
||||
# and use the local cache only.
|
||||
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "tests/boundary/signals/**"
|
||||
- "tests/boundary/cli/signalCleanup.test.ts"
|
||||
- "tests/boundary/mcp/signalRecordingMcp.test.ts"
|
||||
- "src/signals/**"
|
||||
- "src/domain/signals/**"
|
||||
- "src/application/signals/**"
|
||||
- "src/contracts/signals/**"
|
||||
- "src/composition/signals.ts"
|
||||
- "src/cli/signalCommands.ts"
|
||||
- "src/server/registerSignalTools.ts"
|
||||
- "src/server/createServer.ts"
|
||||
- "src/server/sessionAvailabilityPolicy.ts"
|
||||
- "src/application/CapabilityInventory.ts"
|
||||
- "src/contracts/toolContracts.ts"
|
||||
- "src/contracts/toolEffects.ts"
|
||||
- "src/cli.ts"
|
||||
- "src/cliCommandNames.ts"
|
||||
- "src/process/**"
|
||||
- "src/artifacts/readStableArtifact.ts"
|
||||
- "scripts/verify-signal-recording.mjs"
|
||||
- ".github/workflows/real-signal-recording.yml"
|
||||
- "package*.json"
|
||||
- ".nvmrc"
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: real-signal-recording-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
sigrok:
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -44,18 +45,8 @@ jobs:
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: sudo apt-get update && sudo apt-get install -y sigrok-cli util-linux python3
|
||||
- run: npm ci
|
||||
- uses: ./.github/actions/portable-build-cache
|
||||
with:
|
||||
build_cache_artifact: ${{ inputs.build_cache_artifact }}
|
||||
- run: npm run build:cached
|
||||
- name: Run focused tests and require Linux resource and cleanup cases
|
||||
run: |
|
||||
report=".cache/rea-signal-required-tests.json"
|
||||
mkdir -p .cache
|
||||
rm -f "$report"
|
||||
node scripts/run-exact-tests.mjs run --reporter=default --reporter=json --outputFile.json="$report" \
|
||||
tests/boundary/signals/decoderDiscovery.test.ts src/process/LinuxResourceLimits.test.ts src/domain/signals/annotationTextWindow.test.ts tests/boundary/signals/recordingProvider.test.ts tests/boundary/signals/recordingDescriptorCleanup.test.ts tests/boundary/signals/resourceLimits.test.ts tests/boundary/mcp/signalRecordingMcp.test.ts tests/boundary/cli/signalCleanup.test.ts
|
||||
node scripts/ci/require-provider-tests.mjs signal "$report"
|
||||
- run: npm run test:focused -- tests/boundary/signals/decoderDiscovery.test.ts src/process/LinuxResourceLimits.test.ts src/domain/signals/annotationTextWindow.test.ts tests/boundary/signals/recordingProvider.test.ts tests/boundary/signals/recordingDescriptorCleanup.test.ts tests/boundary/signals/resourceLimits.test.ts tests/boundary/mcp/signalRecordingMcp.test.ts tests/boundary/cli/signalCleanup.test.ts
|
||||
- run: node scripts/verify-signal-recording.mjs
|
||||
env:
|
||||
REA_SIGROK_COMMAND: /usr/bin/sigrok-cli
|
||||
@@ -12,11 +12,6 @@ permissions:
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
# Writes only on the default branch for push, schedule, manual dispatch,
|
||||
# and workflow_call. Pull requests get an empty token and remote read-only mode.
|
||||
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
|
||||
|
||||
jobs:
|
||||
real-termux:
|
||||
|
||||
@@ -9,28 +9,12 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
remote_cache_write:
|
||||
description: Trusted release qualification may publish the remote cache.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
TURBO_TOKEN:
|
||||
description: Remote cache token. Only trusted callers pass it.
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
# A called workflow sees the caller event, so workflow_call is not an event
|
||||
# name here. Writes when remote_cache_write is set, or on the default branch
|
||||
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
|
||||
# and use the local cache only.
|
||||
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
|
||||
NODE_OPTIONS: --max-old-space-size=768
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -7,27 +7,11 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
remote_cache_write:
|
||||
description: Trusted release qualification may publish the remote cache.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
TURBO_TOKEN:
|
||||
description: Remote cache token. Only trusted callers pass it.
|
||||
required: false
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
HUSKY: "0"
|
||||
# A called workflow sees the caller event, so workflow_call is not an event
|
||||
# name here. Writes when remote_cache_write is set, or on the default branch
|
||||
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
|
||||
# and use the local cache only.
|
||||
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
|
||||
jobs:
|
||||
real-web-runtime:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -44,8 +28,7 @@ jobs:
|
||||
- uses: ./.github/actions/portable-build-cache
|
||||
with:
|
||||
build_cache_artifact: ${{ inputs.build_cache_artifact }}
|
||||
# Own a fresh bus so Chromium never depends on an inherited session address.
|
||||
- run: dbus-run-session -- npm run verify:browser:runtime
|
||||
- run: env -u DBUS_SESSION_BUS_ADDRESS npm run verify:browser:runtime
|
||||
env:
|
||||
REA_BROWSER_EXECUTABLE: /usr/bin/google-chrome
|
||||
REA_BROWSER_NO_SANDBOX: "true"
|
||||
@@ -7,27 +7,11 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
remote_cache_write:
|
||||
description: Trusted release qualification may publish the remote cache.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
TURBO_TOKEN:
|
||||
description: Remote cache token. Only trusted callers pass it.
|
||||
required: false
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
HUSKY: "0"
|
||||
# A called workflow sees the caller event, so workflow_call is not an event
|
||||
# name here. Writes when remote_cache_write is set, or on the default branch
|
||||
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
|
||||
# and use the local cache only.
|
||||
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
|
||||
jobs:
|
||||
real-web-source-map:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -46,12 +30,8 @@ jobs:
|
||||
build_cache_artifact: ${{ inputs.build_cache_artifact }}
|
||||
- name: Install the pinned compiler into an isolated fixture directory
|
||||
run: npm install --prefix "$RUNNER_TEMP/rea-web-source-map-compiler" --no-audit --no-fund --package-lock=false [email protected]
|
||||
# Own a fresh bus so Chromium never depends on an inherited session address.
|
||||
- run: dbus-run-session -- npm run verify:browser:source-maps
|
||||
- run: npm run verify:browser:source-maps
|
||||
env:
|
||||
# Retain browser launch argv/PID/stderr in the verifier's captured
|
||||
# CLI failure diagnostics without changing its deadline or retrying.
|
||||
DEBUG: pw:browser
|
||||
REA_BROWSER_EXECUTABLE: /usr/bin/google-chrome
|
||||
REA_BROWSER_NO_SANDBOX: "true"
|
||||
REA_WEB_SOURCE_MAP_COMPILER: ${{ runner.temp }}/rea-web-source-map-compiler/node_modules/esbuild/lib/main.js
|
||||
+40
-127
@@ -21,12 +21,6 @@ on:
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
# Remote cache stays local when either value is empty.
|
||||
# Writes only on the default branch for push, schedule, manual dispatch,
|
||||
# and workflow_call. Pull requests get an empty token and remote read-only mode.
|
||||
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
|
||||
|
||||
concurrency:
|
||||
group: release-${{ inputs.release_branch || github.event.pull_request.merge_commit_sha || github.ref_name }}
|
||||
@@ -47,63 +41,19 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
# Filter at the repository connection, without a search-index dependency.
|
||||
pages=$(gh api graphql --paginate --slurp \
|
||||
-f owner="${GH_REPO%/*}" -f name="${GH_REPO#*/}" \
|
||||
-f query='query($owner:String!,$name:String!,$endCursor:String) {
|
||||
repository(owner:$owner,name:$name) {
|
||||
nameWithOwner
|
||||
pullRequests(first:100,after:$endCursor,states:OPEN,baseRefName:"main",labels:["rea:release-notes-finalized"]) {
|
||||
nodes { number headRefName headRepository { nameWithOwner } }
|
||||
pageInfo { hasNextPage endCursor }
|
||||
}
|
||||
}
|
||||
}')
|
||||
finalized=$(jq -er '
|
||||
if type != "array" or length == 0 then error("Missing GitHub pages") else . end |
|
||||
if .[-1].data.repository.pullRequests.pageInfo.hasNextPage != false
|
||||
then error("Incomplete GitHub pagination") else . end |
|
||||
map(
|
||||
if (.errors // [] | length) != 0 then error("GitHub query failed") else . end |
|
||||
.data.repository |
|
||||
if (.nameWithOwner | type) != "string" or (.pullRequests.nodes | type) != "array" or
|
||||
(.pullRequests.pageInfo.hasNextPage | type) != "boolean"
|
||||
then error("Invalid GitHub repository connection") else . end |
|
||||
.nameWithOwner as $repo | .pullRequests.nodes[] |
|
||||
if (.number | type) != "number" or (.headRefName | type) != "string" or (has("headRepository") | not) or
|
||||
(.headRepository != null and (.headRepository.nameWithOwner | type) != "string")
|
||||
then error("Invalid GitHub pull request") else . end |
|
||||
select(.headRepository.nameWithOwner == $repo) |
|
||||
select(.headRefName | startswith("release-please--branches--main--")) | .number
|
||||
) | map(tostring) | join(" ")' <<< "$pages")
|
||||
finalized=$(gh api --paginate "repos/${GH_REPO}/pulls?state=open&base=main&per_page=100" \
|
||||
--jq '.[] | select(.head.repo.full_name == .base.repo.full_name) | select(.head.ref | startswith("release-please--branches--main--")) | select(any(.labels[]; .name == "rea:release-notes-finalized")) | .number')
|
||||
if [[ -n "${finalized}" ]]; then
|
||||
echo "Finalized release notes on PR ${finalized}; automatic proposal refresh is paused." >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "frozen=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check out proposal controller and complete Git history
|
||||
if: steps.editorial.outputs.frozen != 'true'
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
- uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1
|
||||
if: steps.editorial.outputs.frozen != 'true'
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Install proposal controller dependencies
|
||||
if: steps.editorial.outputs.frozen != 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Update release proposal
|
||||
if: steps.editorial.outputs.frozen != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
run: node scripts/release-proposal.mjs --target-branch main --source-sha "$SOURCE_SHA"
|
||||
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
target-branch: main
|
||||
skip-github-release: true
|
||||
|
||||
merged-release:
|
||||
name: Release reviewed main merge
|
||||
@@ -276,25 +226,15 @@ jobs:
|
||||
node scripts/release-notes.mjs publication --repository "$GH_REPO" \
|
||||
--source "$SOURCE_SHA"
|
||||
|
||||
- name: Prepare checkpoint release proposal
|
||||
if: inputs.phase == 'prepare'
|
||||
id: proposal
|
||||
env:
|
||||
# A dedicated credential starts PR CI automatically; the built-in
|
||||
# token creates approval-required PR runs for maintainers to approve.
|
||||
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
RELEASE_BRANCH: ${{ inputs.release_branch }}
|
||||
SOURCE_SHA: ${{ steps.branch.outputs.sha }}
|
||||
run: node scripts/release-proposal.mjs --target-branch "$RELEASE_BRANCH" --source-sha "$SOURCE_SHA"
|
||||
|
||||
- uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1
|
||||
if: inputs.phase == 'publish'
|
||||
id: release
|
||||
with:
|
||||
# A dedicated credential starts PR CI automatically; the built-in
|
||||
# token creates approval-required PR runs for maintainers to approve.
|
||||
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
target-branch: ${{ inputs.release_branch }}
|
||||
skip-github-release: false
|
||||
skip-github-pull-request: true
|
||||
skip-github-release: ${{ inputs.phase == 'prepare' }}
|
||||
skip-github-pull-request: ${{ inputs.phase == 'publish' }}
|
||||
|
||||
- name: Require a merged release PR for publication
|
||||
if: inputs.phase == 'publish' && steps.release.outputs.release_created != 'true'
|
||||
@@ -314,26 +254,26 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Check out release pull request
|
||||
if: inputs.phase == 'prepare' && steps.proposal.outputs.prs_created == 'true'
|
||||
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
ref: ${{ fromJSON(steps.proposal.outputs.pr).headBranchName }}
|
||||
ref: ${{ fromJSON(steps.release.outputs.pr).headBranchName }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js for generated documentation
|
||||
if: inputs.phase == 'prepare' && steps.proposal.outputs.prs_created == 'true'
|
||||
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
if: inputs.phase == 'prepare' && steps.proposal.outputs.prs_created == 'true'
|
||||
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Validate prepared release version and migration notes
|
||||
if: inputs.phase == 'prepare' && steps.proposal.outputs.prs_created == 'true'
|
||||
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
|
||||
env:
|
||||
RELEASE_BRANCH: ${{ inputs.release_branch }}
|
||||
SOURCE_SHA: ${{ steps.branch.outputs.sha }}
|
||||
@@ -343,11 +283,11 @@ jobs:
|
||||
--source-sha "$SOURCE_SHA"
|
||||
|
||||
- name: Regenerate release documentation
|
||||
if: inputs.phase == 'prepare' && steps.proposal.outputs.prs_created == 'true'
|
||||
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
|
||||
run: npm run docs:generate
|
||||
|
||||
- name: Validate generated release documentation
|
||||
if: inputs.phase == 'prepare' && steps.proposal.outputs.prs_created == 'true'
|
||||
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
|
||||
run: npm run docs:check && git diff --exit-code
|
||||
|
||||
qualification:
|
||||
@@ -361,17 +301,11 @@ jobs:
|
||||
contents: read
|
||||
actions: read
|
||||
uses: ./.github/workflows/ci.yml
|
||||
secrets:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
with:
|
||||
source_sha: ${{ needs.merged-release.outputs.sha || needs.release-please.outputs.sha }}
|
||||
remote_cache_write: true
|
||||
|
||||
rehearsal:
|
||||
name: Rehearse qualified release on a clean runner
|
||||
env:
|
||||
TURBO_TOKEN: ""
|
||||
TURBO_CACHE: local:rw
|
||||
needs: [merged-release, release-please, qualification]
|
||||
if: >-
|
||||
always() && !cancelled() && needs.qualification.result == 'success'
|
||||
@@ -405,9 +339,6 @@ jobs:
|
||||
always() && !cancelled() &&
|
||||
needs.qualification.result == 'success' && needs.rehearsal.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TURBO_TOKEN: ""
|
||||
TURBO_CACHE: local:rw
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
@@ -416,56 +347,41 @@ jobs:
|
||||
with:
|
||||
ref: ${{ needs.merged-release.outputs.sha || needs.release-please.outputs.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
package-manager-cache: false
|
||||
registry-url: https://registry.npmjs.org
|
||||
- name: Download the original qualified package
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: verified-release-package
|
||||
path: .cache/verified-package/
|
||||
- name: Bind publication to qualified intent
|
||||
id: package
|
||||
run: node scripts/ci/prepare-npm-publication.mjs
|
||||
- name: Publish
|
||||
if: steps.package.outputs.published == 'false'
|
||||
env:
|
||||
NPM_RELEASE_TAG: ${{ steps.package.outputs.tag }}
|
||||
run: >-
|
||||
npm publish .cache/verified-package/package.tgz --ignore-scripts
|
||||
--registry=https://registry.npmjs.org --access=public --tag="$NPM_RELEASE_TAG"
|
||||
|
||||
verify-npm:
|
||||
name: Verify published npm package
|
||||
needs: [merged-release, release-please, publish]
|
||||
if: >-
|
||||
always() && !cancelled() && needs.publish.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TURBO_TOKEN: ""
|
||||
TURBO_CACHE: local:rw
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
ref: ${{ needs.merged-release.outputs.sha || needs.release-please.outputs.sha }}
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
package-manager-cache: false
|
||||
- run: npm ci
|
||||
|
||||
- uses: ./.github/actions/prepare-release-package
|
||||
id: package
|
||||
|
||||
- name: Inspect existing npm version
|
||||
id: registry
|
||||
env:
|
||||
VERIFIED_PACKAGE_TARBALL: ${{ steps.package.outputs.tarball }}
|
||||
VERIFIED_PACKAGE_VERSION: ${{ steps.package.outputs.version }}
|
||||
run: node scripts/ci/npm-release.mjs inspect "$VERIFIED_PACKAGE_TARBALL" "$VERIFIED_PACKAGE_VERSION"
|
||||
|
||||
- name: Publish
|
||||
if: steps.registry.outputs.published == 'false'
|
||||
shell: bash
|
||||
env:
|
||||
VERIFIED_PACKAGE_TARBALL: ${{ steps.package.outputs.tarball }}
|
||||
VERIFIED_PACKAGE_VERSION: ${{ steps.package.outputs.version }}
|
||||
run: |
|
||||
tag="$(node scripts/release-npm-tag.mjs "$VERIFIED_PACKAGE_VERSION")"
|
||||
npm publish "${VERIFIED_PACKAGE_TARBALL}" --access public --tag "${tag}"
|
||||
|
||||
- name: Verify published npm bytes and dist-tag
|
||||
env:
|
||||
VERIFIED_PACKAGE_TARBALL: ${{ steps.package.outputs.tarball }}
|
||||
VERIFIED_PACKAGE_VERSION: ${{ steps.package.outputs.version }}
|
||||
run: node scripts/ci/npm-release.mjs verify "$VERIFIED_PACKAGE_TARBALL" "$VERIFIED_PACKAGE_VERSION"
|
||||
|
||||
- name: Verify published CLI from npm
|
||||
env:
|
||||
VERIFIED_PACKAGE_VERSION: ${{ steps.package.outputs.version }}
|
||||
@@ -473,12 +389,9 @@ jobs:
|
||||
|
||||
publish-mcp:
|
||||
name: Publish to MCP Registry
|
||||
env:
|
||||
TURBO_TOKEN: ""
|
||||
TURBO_CACHE: local:rw
|
||||
needs: [merged-release, release-please, verify-npm]
|
||||
needs: [merged-release, release-please, publish]
|
||||
if: >-
|
||||
always() && !cancelled() && needs.verify-npm.result == 'success' &&
|
||||
always() && !cancelled() && needs.publish.result == 'success' &&
|
||||
((needs.merged-release.result == 'success' && needs.merged-release.outputs.release_created == 'true') ||
|
||||
(needs.release-please.result == 'success' && inputs.phase == 'publish' && needs.release-please.outputs.release_created == 'true'))
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -17,10 +17,10 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Exercise unavailable-runtime diagnostics and rolling supported releases.
|
||||
# Security floors prevent fallback to an older hosted-toolcache patch.
|
||||
# Exercise unavailable-runtime diagnostics, the first supported release,
|
||||
# and new patches on both supported lines without hand-editing versions.
|
||||
# The repository-pinned Node 24 version also runs the complete test suite.
|
||||
node: ["^22.23.4", "24.22.1", "^24.22.1", "^26.12.1"]
|
||||
node: ["22.x", "24.15.0", "24.x", "26.x"]
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
|
||||
@@ -21,10 +21,6 @@ jobs:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: website/requirements.txt
|
||||
- name: Set up Node.js for the website Worker checks
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Install sharing-image dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
|
||||
@@ -54,10 +54,6 @@ jobs:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: website/requirements.txt
|
||||
- name: Set up Node.js for the website Worker checks
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Install sharing-image dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -79,21 +75,10 @@ jobs:
|
||||
PY
|
||||
- name: Configure Pages
|
||||
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0
|
||||
# upload-pages-artifact excludes dot directories, including .well-known.
|
||||
- name: Package the website including agent discovery
|
||||
run: |
|
||||
tar --dereference --hard-dereference --directory website/_site \
|
||||
--exclude=.git --exclude=.github -cf "$RUNNER_TEMP/artifact.tar" .
|
||||
tar -tf "$RUNNER_TEMP/artifact.tar" \
|
||||
./.well-known/ard.json ./.well-known/ai-catalog.json \
|
||||
./.well-known/agent-skills/index.json > /dev/null
|
||||
- name: Upload the website
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0
|
||||
with:
|
||||
name: github-pages
|
||||
path: ${{ runner.temp }}/artifact.tar
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
path: website/_site
|
||||
|
||||
deploy:
|
||||
needs: prepare
|
||||
@@ -145,24 +130,14 @@ jobs:
|
||||
PUBLISH_TARGET: ${{ inputs.target }}
|
||||
run: |
|
||||
python3 -u - <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from urllib.error import URLError
|
||||
from urllib.parse import urljoin, urlsplit
|
||||
from urllib.parse import urljoin
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
def open_public(url, accept=None):
|
||||
headers = {
|
||||
"Cache-Control": "no-cache",
|
||||
"User-Agent": "REA-Website-Verifier/1.0 (+https://github.com/morluto/rea)",
|
||||
}
|
||||
if accept:
|
||||
headers["Accept"] = accept
|
||||
return urlopen(Request(url, headers=headers), timeout=10)
|
||||
|
||||
expected = os.environ["GITHUB_SHA"]
|
||||
pages_url = os.environ["PAGES_URL"]
|
||||
if not pages_url:
|
||||
@@ -178,7 +153,11 @@ jobs:
|
||||
for name, base in list(pending.items()):
|
||||
url = urljoin(base, "deployment.json") + "?commit=" + expected
|
||||
try:
|
||||
with open_public(url) as response:
|
||||
request = Request(url, headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"User-Agent": "REA-Website-Verifier/1.0 (+https://github.com/morluto/rea)",
|
||||
})
|
||||
with urlopen(request, timeout=10) as response:
|
||||
actual = json.load(response).get("commit")
|
||||
if actual == expected:
|
||||
print(f"{name} serves {expected}: {base}")
|
||||
@@ -193,27 +172,6 @@ jobs:
|
||||
time.sleep(5)
|
||||
if pending:
|
||||
sys.exit("Could not verify published revision on: " + ", ".join(pending))
|
||||
for name, base in hosts.items():
|
||||
with open_public(urljoin(base, ".well-known/agent-skills/index.json")) as response:
|
||||
skill, = json.load(response)["skills"]
|
||||
# Fetch from each selected host, including the /rea/ mirror.
|
||||
artifact_url = urljoin(base, urlsplit(skill["url"]).path.lstrip("/"))
|
||||
with open_public(artifact_url) as response:
|
||||
digest = "sha256:" + hashlib.sha256(response.read()).hexdigest()
|
||||
if digest != skill["digest"]:
|
||||
sys.exit(name + " serves a skill archive with the wrong digest")
|
||||
for manifest in ("ard.json", "ai-catalog.json"):
|
||||
with open_public(urljoin(base, ".well-known/" + manifest)) as response:
|
||||
if not json.load(response).get("entries"):
|
||||
sys.exit(name + " serves an empty discovery manifest")
|
||||
if name == "Cloudflare":
|
||||
with open_public(base, accept="text/markdown") as response:
|
||||
vary = {value.strip().lower() for value in response.headers.get("Vary", "").split(",")}
|
||||
if response.headers.get_content_type() != "text/markdown" or "accept" not in vary:
|
||||
sys.exit("Cloudflare did not negotiate Markdown with Vary: Accept")
|
||||
if 'rel="ard"' not in response.headers.get("Link", ""):
|
||||
sys.exit("Cloudflare is missing its discovery Link header")
|
||||
print(name + " serves the discovery manifests and verified skill archive")
|
||||
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary:
|
||||
links = " and ".join(f"[{name}]({url})" for name, url in hosts.items())
|
||||
summary.write(f"Published commit `{expected}` to {links}.\n")
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ Thumbs.db
|
||||
|
||||
# Local reference sources and analysis artifacts
|
||||
/_reference/
|
||||
/experiments/analysis-efficiency/
|
||||
/experiments/analysis-efficiency/results/
|
||||
|
||||
# Generated website download
|
||||
/website/public/examples/notes-example.zip
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"threshold": 4,
|
||||
"threshold": 3.5,
|
||||
"reporters": ["consoleFull"],
|
||||
"ignore": [
|
||||
"node_modules/**",
|
||||
|
||||
+1
-4
@@ -10,10 +10,7 @@
|
||||
"node_modules/**",
|
||||
"CHANGELOG.md",
|
||||
"package-lock.json",
|
||||
".claude-plugin/plugin.json",
|
||||
".codex-plugin/plugin.json",
|
||||
"_reference/**",
|
||||
"third_party/**",
|
||||
".codex-plugin/mcp.json"
|
||||
"third_party/**"
|
||||
]
|
||||
}
|
||||
+3
-95
@@ -1,13 +1,7 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript", "import", "unicorn", "oxc", "vitest"],
|
||||
"plugins": ["typescript", "import", "unicorn", "oxc"],
|
||||
"rules": {
|
||||
"rea/no-buffer-deep-equality": "error",
|
||||
"rea/cancellation-cause": "error",
|
||||
"no-async-promise-executor": "error",
|
||||
"no-unreachable": "error",
|
||||
"no-unsafe-optional-chaining": "error",
|
||||
"no-useless-escape": ["warn", { "allowRegexCharacters": ["-"] }],
|
||||
"complexity": ["warn", 20],
|
||||
"max-depth": ["warn", 4],
|
||||
"max-lines": [
|
||||
@@ -29,38 +23,7 @@
|
||||
"max-params": ["warn", 4],
|
||||
"no-var": "warn",
|
||||
"no-var-requires": "error",
|
||||
"typescript/no-import-type-side-effects": "error",
|
||||
"vitest/no-standalone-expect": "off",
|
||||
"vitest/no-conditional-expect": "off",
|
||||
"vitest/expect-expect": "off",
|
||||
"vitest/no-disabled-tests": "off",
|
||||
"vitest/require-mock-type-parameters": "off",
|
||||
"vitest/require-to-throw-message": "off",
|
||||
"import/no-duplicates": [
|
||||
"warn",
|
||||
{
|
||||
"preferInline": false
|
||||
}
|
||||
],
|
||||
"vitest/valid-expect": [
|
||||
"warn",
|
||||
{
|
||||
"maxArgs": 2
|
||||
}
|
||||
],
|
||||
"rea/no-evidence-json-preadmission": "error",
|
||||
"rea/no-positional-tool-contract": "error",
|
||||
"rea/no-fixture-root-cleanup": "error",
|
||||
"typescript/no-floating-promises": "error",
|
||||
"typescript/no-misused-promises": "warn",
|
||||
"typescript/return-await": ["error", "error-handling-correctness-only"],
|
||||
"typescript/switch-exhaustiveness-check": [
|
||||
"error",
|
||||
{
|
||||
"considerDefaultExhaustiveForUnions": true
|
||||
}
|
||||
],
|
||||
"rea/no-shared-module-mock": "error"
|
||||
"typescript/no-import-type-side-effects": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
@@ -73,12 +36,6 @@
|
||||
"no-var-requires": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["src/**/*.ts"],
|
||||
"rules": {
|
||||
"typescript/no-misused-promises": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["tests/**/*.ts", "src/**/*.test.ts", "src/**/*.fixture.ts"],
|
||||
"rules": {
|
||||
@@ -90,8 +47,7 @@
|
||||
"skipBlankLines": true,
|
||||
"skipComments": true
|
||||
}
|
||||
],
|
||||
"typescript/no-misused-promises": "warn"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -135,45 +91,6 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["tests/**/*.test.ts", "src/**/*.test.ts"],
|
||||
"rules": {
|
||||
"vitest/valid-expect": [
|
||||
"warn",
|
||||
{
|
||||
"maxArgs": 2
|
||||
}
|
||||
],
|
||||
"vitest/valid-expect-in-promise": "error",
|
||||
"vitest/no-focused-tests": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"scripts/lib/http3-capture-assertions.test.mjs",
|
||||
"scripts/lib/http3-verifier-lifecycle.test.mjs"
|
||||
],
|
||||
"rules": {
|
||||
"typescript/no-floating-promises": [
|
||||
"error",
|
||||
{
|
||||
"allowForKnownSafeCalls": [
|
||||
{
|
||||
"from": "package",
|
||||
"package": "@types/node",
|
||||
"name": "test"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["scripts/fixtures/javascript-recovery/code-split/entry.mjs"],
|
||||
"rules": {
|
||||
"typescript/no-floating-promises": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"env": {
|
||||
@@ -192,14 +109,5 @@
|
||||
".devin",
|
||||
".factory",
|
||||
"third_party"
|
||||
],
|
||||
"options": {
|
||||
"reportUnusedDisableDirectives": "error"
|
||||
},
|
||||
"jsPlugins": [
|
||||
{
|
||||
"name": "rea",
|
||||
"specifier": "./scripts/lint/index.mjs"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "6.8.0"
|
||||
".": "6.5.0"
|
||||
}
|
||||
@@ -22,16 +22,10 @@ Keep proof of ownership separate from validity, editability, and readiness. Malf
|
||||
|
||||
For lifecycle changes, follow [ownership and completion invariants](docs/tool-design.md#own-the-lifecycle) and select the applicable [lifecycle regressions](docs/testing.md#lifecycle-regressions). Include late handshakes, cancellation during final cleanup, mixed child outcomes, and cleanup retry where those stages exist; active-execution cancellation alone does not cover finalization.
|
||||
|
||||
Before adding complexity for a speculative or vanishingly unlikely race or security edge case, explain the concrete failure mode, likelihood, and complexity cost to the user and get their buy-in. Do not silently expand scope for theoretical robustness.
|
||||
|
||||
Assess brittleness in touched code during implementation, debugging, review, and maintenance. Fix the producing representation or ownership rule before adding downstream repairs. Normalize once at the owning boundary using format-aware parsers. Remove superseded representations, aliases, wrappers, and redundant tests after verifying affected consumers. Introduce abstractions only when they remove observed complexity.
|
||||
|
||||
When fixing a bug, inspect the producing boundary and affected consumers for duplicated interpretation or unclear ownership. Complete any directly related refactor in the same change and remove superseded code. Passing regression tests alone does not establish that the brittleness is resolved. If related cleanup remains, explain what remains and why.
|
||||
|
||||
After confirming a bug, search sibling adapters, consumers and similar workflows
|
||||
for the same failure. Verify suspected matches, record working comparisons, and
|
||||
group findings by root cause instead of filing duplicate reports.
|
||||
|
||||
When addressing a review thread, inspect the producing boundary and affected consumers for brittleness. Complete directly related refactoring in the same change. Before resolving the thread, explain the root cause, structural fix, validation, and any remaining cleanup.
|
||||
|
||||
Parse unknown boundary values and model expected failures with the tagged error algebra and `Result`. Preserve partial facts and meaningful failure reasons. Keep identity and lookup values separate from display formatting; preserve source values and explicit unknowns when normalization loses information. Absolute paths, file URLs, and HTTP paths have different semantics.
|
||||
@@ -57,18 +51,15 @@ Optimize in this order:
|
||||
3. CPU and memory. Avoid unnecessary work and copies; bound concurrency,
|
||||
retained state and serialization growth.
|
||||
|
||||
Reserve performance measurements for changes that target performance or have a
|
||||
concrete regression concern. Routine cleanup uses correctness checks; changes
|
||||
to helper calls, imports, or representations alone do not require synthetic
|
||||
benchmarks. When measurement is warranted, measure representative completed
|
||||
tasks and compare equivalent workflows before and after where available. Report
|
||||
token usage, elapsed time, CPU time and peak process-family memory. Separate cold
|
||||
For changes affecting these costs, measure representative completed tasks and
|
||||
compare equivalent workflows before and after where available. Report token
|
||||
usage, elapsed time, CPU time and peak process-family memory. Separate cold
|
||||
and warm runs, label estimates, and state unmeasured costs. Fewer bytes alone
|
||||
do not establish fewer tokens or faster analysis.
|
||||
|
||||
## Development
|
||||
|
||||
Use [.nvmrc](.nvmrc) and `package.json#packageManager` for the pinned toolchain. Cursor Cloud may place older Node.js first on `PATH`; environment setup installs the pinned toolchain under `/usr/local/bin`. Prefix `PATH="/usr/local/bin:$PATH"` before `node` and `npm`.
|
||||
Use [.nvmrc](.nvmrc) and `package.json#packageManager` for the pinned toolchain. Cursor Cloud may place older Node.js first on `PATH`; environment setup installs the pinned toolchain under `/usr/local/bin`.
|
||||
|
||||
- `npm ci`: install locked dependencies.
|
||||
- `npm run build:cached`: build the runtime and packaged skill.
|
||||
@@ -79,24 +70,13 @@ Use [.nvmrc](.nvmrc) and `package.json#packageManager` for the pinned toolchain.
|
||||
|
||||
Use relevant checks for the change. The complete local gate, `npm run check:pr`, is optional for broad changes or CI diagnosis. See [CONTRIBUTING.md](CONTRIBUTING.md) for development, generated-file ownership, and release conventions.
|
||||
|
||||
When changing installers or setup recovery, follow
|
||||
[Checking installers and publisher downloads](docs/testing.md#checking-installers-and-publisher-downloads).
|
||||
|
||||
Coordinate expensive checks across agents. If a test unexpectedly consumes heavy CPU, stop its owned process family and profile a bounded representative case before retrying. Inspect production work, fixture generation, subprocesses, and repeated setup; preserve regression coverage when optimizing. Follow the [test resource guidance](docs/testing.md#developer-commands).
|
||||
|
||||
`docs/public/product-catalog.json`, `docs/verification/managed-conformance-*.json`, and `skills/` are ignored outputs. Edit source contracts and `.agents/skills/reverse-engineer-anything/`, then regenerate as needed. `.cache/mcp-tool-catalog.json` is test metadata; runtime builds and source checking do not consume it. Never commit binaries, provider project documents, credentials, `dist/`, `node_modules/`, or local planning artifacts.
|
||||
|
||||
## Tool and Test Changes
|
||||
|
||||
For tool design, follow [docs/tool-design.md](docs/tool-design.md): start from the analyst question, reuse existing contracts, preserve caller choices, and return useful evidence inline. Limits must follow real format, protocol, authority, or resource constraints. Account for representation expansion before allocation; derive completeness from examined coverage.
|
||||
|
||||
For user- or agent-facing tool problems, prioritize fixing the owning behavior,
|
||||
contract, and actionable result or recovery at the point of use. Document the
|
||||
result in product docs when explanation is needed. Add skill guidance only for
|
||||
workflow judgment or context that the tool cannot reasonably convey; do not use
|
||||
a skill as the primary repair for a discoverability or contract defect.
|
||||
|
||||
For test changes or provider claims, use [docs/testing.md](docs/testing.md). Select applicable [boundary regressions](docs/testing.md#boundary-regressions) for parser, inventory, snapshot, and projection fixes. Prefer real public CLI/MCP workflows, then production-boundary integration, then producer goldens. Keep module tests for distinct semantics stronger workflows cannot reliably reproduce. Preserve malformed-input, cancellation, cleanup, permission, capacity, and target-identity coverage when pruning. Report unverified coverage explicitly; simulated providers do not establish real-provider behavior.
|
||||
For test changes or provider claims, use [docs/testing.md](docs/testing.md). Prefer real public CLI/MCP workflows, then production-boundary integration, then producer goldens. Keep module tests for distinct semantics stronger workflows cannot reliably reproduce. Preserve malformed-input, cancellation, cleanup, permission, capacity, and target-identity coverage when pruning. Report unverified coverage explicitly; simulated providers do not establish real-provider behavior.
|
||||
|
||||
Use Conventional Commit subjects and PR titles. Mark breaking changes with `!` or a `BREAKING CHANGE:` footer. Describe behavior changes, relevant validation, and real-provider verification scope.
|
||||
|
||||
@@ -124,12 +104,11 @@ distinguish source-derived conclusions, proposed checks, and unavailable coverag
|
||||
Address actionable automated review findings and explain disagreements.
|
||||
|
||||
Leave the template's **Author readiness** declarations unchecked for the human
|
||||
owner to complete. Never attest to human review on their behalf. Draft status
|
||||
is optional; preserve the PR's current draft status unless the user asks to change
|
||||
it. Later pushes do not invalidate the readiness declaration or require another
|
||||
human sign-off. Keep the PR body and affected validation current as work changes.
|
||||
The readiness check validates the filled declaration or the author exemption
|
||||
listed in CONTRIBUTING.md; CI and automated review results do not establish that
|
||||
owner to complete. Never attest to human review on their behalf. Keep the PR in
|
||||
draft until the human declares the PR initially ready. Later pushes do not
|
||||
invalidate that declaration or require another human sign-off. Keep the PR body
|
||||
and affected validation current as work changes. The readiness check validates
|
||||
the filled declaration; CI and automated review results do not establish that
|
||||
human review occurred, maintainer scope approval, or merge permission.
|
||||
|
||||
<!-- BEGIN:turborepo-agent-rules -->
|
||||
|
||||
+2
-192
@@ -1,195 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [6.8.0](https://github.com/morluto/rea/compare/rea-agents-6.7.0...rea-agents-6.8.0) (2026-10-25)
|
||||
|
||||
### Highlights
|
||||
|
||||
- Build local agent environments from network captures, browser scenarios, or a prompt, then serve derived reads and evidence-gated writes and compare them with fresh application captures.
|
||||
- Inspect more shipped formats, including GPU code objects, BEAM, Python bytecode, Unity IL2CPP metadata, Godot PCK, PyInstaller, Node.js SEA, devicetree, and additional PE/ELF tables.
|
||||
- Keep large investigations focused with retained summaries, selected fact queries, configurable recovery limits, and row-oriented CLI exports. Complete Evidence remains available.
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
- **Function analysis:** `analyze_function` now defaults to a retained summary. Set `detail: "complete"` for the previous inline dossier; the CLI retains its complete default and requires `--evidence_output` when selecting a summary. [#5660](https://github.com/morluto/rea/pull/5660). Thanks @CryptoJones, @gensokyo-dev, @RishTheFish21.
|
||||
- **Accessibility:** Accept JSON numbers and booleans in node `value`, alongside strings and null. Update consumers that previously assumed text-only values. [cc90771](https://github.com/morluto/rea/commit/cc90771af14f045538651615147d8bf86b576549), [#6759](https://github.com/morluto/rea/pull/6759), [#6467](https://github.com/morluto/rea/issues/6467). Thanks @coygeek, @gensokyo-dev, @mvanhorn.
|
||||
- **JSON Lines:** `--format jsonl` now emits object-collection rows followed by a `$rea: "jsonl/v1"`, `type: "metadata"` record; scalar selections and unsupported reports are refused. Parse control records separately, select a single supported collection when necessary, and use `--format json` for the previous whole-document output. See [JSON Lines framing](https://github.com/morluto/rea/blob/rea-agents-6.8.0/docs/cli.md#json-lines-record-output). [#5629](https://github.com/morluto/rea/pull/5629), [#5487](https://github.com/morluto/rea/issues/5487). Thanks @gensokyo-dev, @jayykernel, @vijaykiran06.
|
||||
- **Interface Builder:** Keyed-archive graph identities use decimal table UIDs instead of authored `objectID`/`object-id`. Treat authored IDs as attributes and update persisted graph lookups; ibtool and native NIBArchive identities keep their source semantics. [6fbe67d](https://github.com/morluto/rea/commit/6fbe67d66e3d716db69929503a3a232a7ada0860).
|
||||
- **Frida:** `instrument-with-frida` JSON returns the canonical Evidence directly. Read `normalized_result.cleanup_error` instead of the former top-level wrapper field. [48f700e](https://github.com/morluto/rea/commit/48f700ee35e0988ed973e5486dc9db33f2a5d944).
|
||||
- **Reconstruction proof:** ID-array proof records no longer establish verified or ready ledgers. Supply implementation/revision bindings and individual claim outcomes; handle inapplicable, unavailable, and failed proof diagnostics. [f8b65e7](https://github.com/morluto/rea/commit/f8b65e78723b1e2169f94dd1226198ea3139707e).
|
||||
- **Mach-O imports:** Handle a null import `name` when identity is ambiguous; use `raw_name` and `name_status` to distinguish observed expressions from exact symbols. [5d0d243](https://github.com/morluto/rea/commit/5d0d243a45c9132e1aff75d454ab53d86a74cc31), [169512a](https://github.com/morluto/rea/commit/169512a35bb015ff209168fb8bc1c9b31a2c678b).
|
||||
- **Node.js:** Use a runtime satisfying `^22.23.4 || ^24.22.1 || >=26.12.1`; Node 23 and 25 are unsupported. Upgrade Node before installing or updating REA; the installer does not upgrade it. [#6315](https://github.com/morluto/rea/pull/6315), [#6283](https://github.com/morluto/rea/issues/6283). Thanks @GamerDubz, @gensokyo-dev, @ShikamaruNaaraa.
|
||||
- **Browser storage:** Historical hashed storage remains readable, but comparisons stay unknown until both captures are recaptured in the new representation. [#6341](https://github.com/morluto/rea/pull/6341), [#6289](https://github.com/morluto/rea/issues/6289). Thanks @coygeek, @gongfpp.
|
||||
- **Environment serving:** `environment-serve` serves derived state by default, including write tools whose effects are confirmed by recorded evidence. Use `--replay-only` for stateless replay of manifest tools. Read stop-summary `state` instead of `state_evidence_id`. [#5351](https://github.com/morluto/rea/pull/5351), [#6158](https://github.com/morluto/rea/pull/6158). Thanks @earakely-scale, @gensokyo-dev, @kaoru0822-kitauji.
|
||||
- **MCP schemas:** Input advertisements default to compact. Set `REA_MCP_INPUT_SCHEMA_PROFILE=full` for complete advertised guidance and nested constraints. [#5975](https://github.com/morluto/rea/pull/5975). Thanks @gensokyo-dev.
|
||||
- **Browser console arguments:** Accept null `argument_types` and `text_capture.status: "unavailable"` when the protocol argument inventory is omitted or malformed; preserve that uncertainty. [#6016](https://github.com/morluto/rea/pull/6016), [#5934](https://github.com/morluto/rea/issues/5934). Thanks @coygeek, @kaoru0822-kitauji.
|
||||
- **Reconstruction summaries:** `summary.by_evidence_authority` is keyed by Evidence authority; read the former source-state counts from required `summary.by_source_state`. Regenerate ledgers whose closure digests depended on the old projection. [#5875](https://github.com/morluto/rea/pull/5875), [#5838](https://github.com/morluto/rea/issues/5838). Thanks @coygeek, @kaoru0822-kitauji.
|
||||
- **Swift bindings:** Conformance `type_name` and `protocol_name` may be null; consume typed `type_binding`/`protocol_binding` evidence. Replace the former Objective-C-specific import-binding type/schema with `MachoImportBinding`. [d0067ba](https://github.com/morluto/rea/commit/d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f).
|
||||
- **WebSocket frames:** Accept null opcodes and preserve unknown frame facts; numeric opcodes are restricted to the four-bit protocol range. [0bc2b42](https://github.com/morluto/rea/commit/0bc2b42001f684beffcf47b70e507443b1a5d320).
|
||||
- **Resource-only inspection:** Evidence subjects and executable digests may be null. Use `resource_root` and each resource's reported provenance instead of requiring an executable target. [6a6adf7](https://github.com/morluto/rea/commit/6a6adf703fa44100643947d20fbc670f86260b1b).
|
||||
- **Browser timestamps:** Console and session-timeline timestamps may be null when the protocol supplies no finite/comparable time. Keep unknown time explicit in sorting and comparisons. [690fc51](https://github.com/morluto/rea/commit/690fc518540bae982038a74f47ef7781201785aa), [2a0410f](https://github.com/morluto/rea/commit/2a0410f1717b3126e8a95063d77dea66a597010a).
|
||||
- **Reference sources:** Read failed root enumeration from `rootFailure` rather than a synthetic failed child at `"."`; preserve the top-level source-graph limitation. [6296fdf](https://github.com/morluto/rea/commit/6296fdf560f1ac4aa55985558edaa98300aacdbc).
|
||||
- **Filesystem capture:** Accept `entry_unavailable` and `identity_changed` enumeration omission reasons in addition to the previous reason literals. [c85b44e](https://github.com/morluto/rea/commit/c85b44e1b14851c54e65327a4807f79ad5cd5a61). Thanks @rudrakshagarwal4200-ai.
|
||||
- **APK locales:** Replace numeric legacy selectors such as `es-r419` with `es-419` or `b+es+419`. Two-letter regions remain supported. [29910d1](https://github.com/morluto/rea/commit/29910d16b98fe4b607a7aa15928789a73614b0b9). Thanks @mertkayacs.
|
||||
- **XML plist scalars:** Recognize `$plist_type: "unknown"` and read `element`, `literal`, and `reason`; unresolved XML scalars use this wrapper instead of the former real/null unknown marker or malformed scalar prefixes. Supported extended UTC years preserve their timestamp. [3537e58](https://github.com/morluto/rea/commit/3537e58c1ab29309f06eabf50bc52adba9817a0c).
|
||||
|
||||
### Changes
|
||||
|
||||
- **Binary layouts:** Inspect ELF32 i386, little-endian ARM, and little- or big-endian MIPS `EXEC`/`DYN`/`REL` layouts on Linux x64 using a caller-supplied pwntools environment. [#7052](https://github.com/morluto/rea/pull/7052), [#6934](https://github.com/morluto/rea/issues/6934). Thanks @roblennon, @salmanalmutairi1.
|
||||
- **Agent environments:** Project network captures and Evidence files into manifests, preserve template path pairs and positions, export AgentEnv bundles, and serve manifests as replay environments. [#4954](https://github.com/morluto/rea/pull/4954), [#5293](https://github.com/morluto/rea/pull/5293), [#5664](https://github.com/morluto/rea/pull/5664), [#4999](https://github.com/morluto/rea/pull/4999), [#4975](https://github.com/morluto/rea/pull/4975). Thanks @earakely-scale, @gensokyo-dev, @kaoru0822-kitauji.
|
||||
- **Capture to environment:** Build environments from browser scenarios or a prompt, extract the initial observed state, and compare the environment with application probe captures. [#5182](https://github.com/morluto/rea/pull/5182), [#6946](https://github.com/morluto/rea/pull/6946), [#5319](https://github.com/morluto/rea/pull/5319), [#5699](https://github.com/morluto/rea/pull/5699). Thanks @earakely-scale, @gensokyo-dev, @kaoru0822-kitauji.
|
||||
- **State-backed tools:** Render read filters, search, sorting and paging from state; apply confirmed write effects with broader state targets and recording-based write admission. [#5352](https://github.com/morluto/rea/pull/5352), [293832b](https://github.com/morluto/rea/commit/293832bafc9a054914ec514fe906dd4936c7a372), [#5800](https://github.com/morluto/rea/pull/5800), [#5787](https://github.com/morluto/rea/pull/5787), [#5600](https://github.com/morluto/rea/pull/5600), [5375ff0](https://github.com/morluto/rea/commit/5375ff0f9aadb4fd0d46333815aa0431e3df1010), [#6169](https://github.com/morluto/rea/pull/6169), [#6385](https://github.com/morluto/rea/pull/6385), [#6897](https://github.com/morluto/rea/pull/6897). Thanks @earakely-scale, @gensokyo-dev, @kaoru0822-kitauji.
|
||||
- **State learning:** Model application-assigned values, preserve view-specific clock spellings, learn write clocks from scenario timing, derive fields, and learn creates across sorted arrays and referenced entities. [7ec9b6e](https://github.com/morluto/rea/commit/7ec9b6e56e1b3762285dca881db6d900a389ab9c), [#6136](https://github.com/morluto/rea/pull/6136), [957bcfa](https://github.com/morluto/rea/commit/957bcfa149c1a0593bb0aa2466cc56d1d4f62e7d), [#6138](https://github.com/morluto/rea/pull/6138), [#6967](https://github.com/morluto/rea/pull/6967), [#7025](https://github.com/morluto/rea/pull/7025), [#7034](https://github.com/morluto/rea/pull/7034). Thanks @earakely-scale.
|
||||
- **Recorded writes:** Serve creates whose compatible read-backs add omitted fields or express timestamps at a different precision, and whole-object updates that regenerate observed slugs or modification times. Conflicting observations still withhold writes. State views and bundles carrying the new read-back data need REA 6.8.0 or later to serve. [#7080](https://github.com/morluto/rea/pull/7080). Thanks @earakely-scale.
|
||||
- **Native investigations:** Page native inventory summaries, match functions across builds, export pseudocode to resumable directories, and select objdump disassembly by symbol. [#5378](https://github.com/morluto/rea/pull/5378), [#5390](https://github.com/morluto/rea/pull/5390), [#6170](https://github.com/morluto/rea/pull/6170), [#5171](https://github.com/morluto/rea/pull/5171), [#4249](https://github.com/morluto/rea/issues/4249), [#5343](https://github.com/morluto/rea/issues/5343), [#5159](https://github.com/morluto/rea/issues/5159). Thanks @coygeek, @CryptoJones, @gensokyo-dev, @kaoru0822-kitauji.
|
||||
- **Engine workflows:** Bound a single Ghidra decompilation without closing its session, analyze GameCube/Wii DOL executables, and inspect static CUDA/HIP code objects. [#5662](https://github.com/morluto/rea/pull/5662), [#6240](https://github.com/morluto/rea/pull/6240), [#5813](https://github.com/morluto/rea/pull/5813), [#5340](https://github.com/morluto/rea/issues/5340), [#5345](https://github.com/morluto/rea/issues/5345). Thanks @arielfikru, @coygeek, @gensokyo-dev, @RishTheFish21.
|
||||
- **PE/ELF:** Inspect export tables without an engine, PE imports/delay-loads, Authenticode certificates, TLS callbacks/load configuration, ELF notes, Linux kernel module metadata, and Zstandard section-name tables. [#6278](https://github.com/morluto/rea/pull/6278), [#6509](https://github.com/morluto/rea/pull/6509), [#4830](https://github.com/morluto/rea/pull/4830), [#7008](https://github.com/morluto/rea/pull/7008), [#4837](https://github.com/morluto/rea/pull/4837), [#7010](https://github.com/morluto/rea/pull/7010), [#6987](https://github.com/morluto/rea/pull/6987), [#4057](https://github.com/morluto/rea/issues/4057). Thanks @gensokyo-dev, @kaoru0822-kitauji, @mvanhorn, @Rinkia, @ShuaixinHuang.
|
||||
- **Apple/native metadata:** Inspect IPS crash reports, decode Swift nominal declarations, and select universal Mach-O slices in Go/Rust metadata readers. [#6511](https://github.com/morluto/rea/pull/6511), [#6257](https://github.com/morluto/rea/pull/6257), [#6219](https://github.com/morluto/rea/pull/6219), [#5341](https://github.com/morluto/rea/issues/5341), [#4102](https://github.com/morluto/rea/issues/4102). Thanks @coygeek, @gensokyo-dev, @kaoru0822-kitauji, @mvanhorn, @Rinkia.
|
||||
- **Packaged applications:** Triage desktop installation directories and inspect/extract Godot PCK, PyInstaller archives, and Node.js single-executable application blobs. [#5207](https://github.com/morluto/rea/pull/5207), [#3634](https://github.com/morluto/rea/pull/3634), [afa5da7](https://github.com/morluto/rea/commit/afa5da745e0b139e68029a96b776ae30e48ea2b2), [#2433](https://github.com/morluto/rea/pull/2433), [#7000](https://github.com/morluto/rea/pull/7000), [#4399](https://github.com/morluto/rea/issues/4399). Thanks @0xthearchitect, @coygeek, @gensokyo-dev, @mvanhorn.
|
||||
- **Unity and managed code:** Inventory IL2CPP images/types/methods and metadata versions 24/27; read member names and literals, link native methods and generic instances, and export Ghidra seeds. Project managed call/field-access edges and page managed member summaries. [#3807](https://github.com/morluto/rea/pull/3807), [#5326](https://github.com/morluto/rea/pull/5326), [52d1a72](https://github.com/morluto/rea/commit/52d1a72af047a73b787b8935b032786a34966036), [#5327](https://github.com/morluto/rea/pull/5327), [#5785](https://github.com/morluto/rea/pull/5785), [#6260](https://github.com/morluto/rea/pull/6260), [#5167](https://github.com/morluto/rea/pull/5167), [#3784](https://github.com/morluto/rea/issues/3784), [#5112](https://github.com/morluto/rea/issues/5112), [#4056](https://github.com/morluto/rea/issues/4056), [#5148](https://github.com/morluto/rea/issues/5148). Thanks @8rulerstar, @alexlanghart, @elricfd, @gensokyo-dev, @jaelliot, @kaoru0822-kitauji, @Rinkia.
|
||||
- **Android:** Inspect APK signing declarations and structured manifest components; report static-final DEX field values. [e93e3b1](https://github.com/morluto/rea/commit/e93e3b1e7d6e7e4f49c2de24b92452ec29e853f8), [#4158](https://github.com/morluto/rea/pull/4158), [#5025](https://github.com/morluto/rea/pull/5025), [#6981](https://github.com/morluto/rea/pull/6981), [#4537](https://github.com/morluto/rea/issues/4537), [#6397](https://github.com/morluto/rea/issues/6397). Thanks @Abelo9996, @BaaRai, @FerroLx, @gensokyo-dev, @sserrgo12.
|
||||
- **Bytecode and firmware:** Inspect and query CPython pyc and BEAM facts, inspect flattened devicetrees, and attribute MMIO peripheral regions in native disassembly. [#4826](https://github.com/morluto/rea/pull/4826), [#6928](https://github.com/morluto/rea/pull/6928), [#4833](https://github.com/morluto/rea/pull/4833), [#6943](https://github.com/morluto/rea/pull/6943), [#7009](https://github.com/morluto/rea/pull/7009), [#5348](https://github.com/morluto/rea/pull/5348), [#6922](https://github.com/morluto/rea/issues/6922), [#6938](https://github.com/morluto/rea/issues/6938). Thanks @gensokyo-dev, @liziye627-design, @mvanhorn.
|
||||
- **JavaScript:** Retain queryable static route inventories, link assignment values to writes, and accept source selections as a JSON array or file. [#6019](https://github.com/morluto/rea/pull/6019), [#6206](https://github.com/morluto/rea/pull/6206), [#5029](https://github.com/morluto/rea/pull/5029), [#4772](https://github.com/morluto/rea/issues/4772), [#6193](https://github.com/morluto/rea/issues/6193). Thanks @gensokyo-dev, @IvenKooLab, @jacobmalibiran, @JoshOsullivan-au, @kaoru0822-kitauji, @KJLavender.
|
||||
- **Data inspection:** Decode selected protobuf/gRPC bodies, inspect GGUF/safetensors declarations, and produce byte-entropy profiles. [#5031](https://github.com/morluto/rea/pull/5031), [#5603](https://github.com/morluto/rea/pull/5603), [#2331](https://github.com/morluto/rea/pull/2331), [#4986](https://github.com/morluto/rea/issues/4986), [#3323](https://github.com/morluto/rea/issues/3323). Thanks @AayushJB03, @gensokyo-dev, @kaoru0822-kitauji, @KJLavender, @michaelkillgta, @mvanhorn.
|
||||
- **CLI input/output:** Accept structured request documents on stdin and expose version/runtime output formats. Row-oriented JSONL exports use the migration above. [#7040](https://github.com/morluto/rea/pull/7040), [#5486](https://github.com/morluto/rea/issues/5486), [#6530](https://github.com/morluto/rea/pull/6530), [169512a](https://github.com/morluto/rea/commit/169512a35bb015ff209168fb8bc1c9b31a2c678b), [#5483](https://github.com/morluto/rea/issues/5483). Thanks @Abelo9996, @ahmed-husssain, @CassieFox23, @DeryFerd, @eco-null, @ezrabrilliant, @GamerDubz, @gensokyo-dev, @gongfpp, @hifzabuildsai, @jayykernel, @mhamzasakhi, @pablodiazjorge, @Plastic-time, @raidnouredden-ux, @SandstromPL, @ShuaixinHuang, @Sriram-PR, @XiaoBinGan.
|
||||
- **MCP and setup:** Add opt-in Streamable HTTP and FreeBuff setup registration. FreeBuff support first appears in 6.8.0; installed native FreeBuff loading and skill discovery remain unverified. [#4955](https://github.com/morluto/rea/pull/4955), [#2736](https://github.com/morluto/rea/pull/2736), [#3243](https://github.com/morluto/rea/issues/3243). Thanks @0xLLVEL, @codebuff-team, @earakely-scale, @gensokyo-dev, @kaoru0822-kitauji.
|
||||
- **Recovery controls:** Adjust per-call WASM/SQLite/firmware deadlines, MCP startup budgets, regex matching deadlines/worker heap, capture decoder heap, ASAR header admission, Apple dispatch input, and the bridge request ceiling. MCP runtime limit recovery reports actionable settings. [#5798](https://github.com/morluto/rea/pull/5798), [#5818](https://github.com/morluto/rea/pull/5818), [aa53c06](https://github.com/morluto/rea/commit/aa53c065387c58b4189d82c47f1f3445fc505e30), [#5863](https://github.com/morluto/rea/pull/5863), [#6304](https://github.com/morluto/rea/pull/6304), [#6267](https://github.com/morluto/rea/pull/6267), [#6301](https://github.com/morluto/rea/pull/6301), [#6299](https://github.com/morluto/rea/pull/6299), [#6209](https://github.com/morluto/rea/pull/6209), [#6233](https://github.com/morluto/rea/pull/6233), [#6376](https://github.com/morluto/rea/pull/6376), [#7049](https://github.com/morluto/rea/pull/7049), [#5797](https://github.com/morluto/rea/issues/5797), [#5812](https://github.com/morluto/rea/issues/5812), [#5851](https://github.com/morluto/rea/issues/5851), [#6176](https://github.com/morluto/rea/issues/6176), [#3917](https://github.com/morluto/rea/issues/3917). Thanks @gensokyo-dev, @jaychou813, @kaoru0822-kitauji.
|
||||
- **APK output:** Set `string_tables_only` or CLI `--string-tables-only` to omit duplicate primary strings and avoid their output-capacity charge. [7c458f0](https://github.com/morluto/rea/commit/7c458f04b9155707c8ba406f8b455b10016f1b99), [#4990](https://github.com/morluto/rea/pull/4990), [#4982](https://github.com/morluto/rea/issues/4982). Thanks @gensokyo-dev, @kaoru0822-kitauji, @N0zoM1z0.
|
||||
- **Retained analysis:** Reuse successful validation of authenticated immutable native/imported-JavaScript parents and captures. Query retained Go build identity, Rust dependencies, JavaScript source-to-bundle mappings, and native export facts without repeating full output. [8f7d12e](https://github.com/morluto/rea/commit/8f7d12e30d63f3d3b4cfe2a90336362841438f56), [#4962](https://github.com/morluto/rea/pull/4962), [#5349](https://github.com/morluto/rea/pull/5349), [#5524](https://github.com/morluto/rea/pull/5524), [#5815](https://github.com/morluto/rea/pull/5815), [#6128](https://github.com/morluto/rea/pull/6128), [#6907](https://github.com/morluto/rea/pull/6907), [#4810](https://github.com/morluto/rea/issues/4810), [#5499](https://github.com/morluto/rea/issues/5499), [#5802](https://github.com/morluto/rea/issues/5802), [#6112](https://github.com/morluto/rea/issues/6112), [#6900](https://github.com/morluto/rea/issues/6900). Thanks @gensokyo-dev, @kaoru0822-kitauji, @N0zoM1z0.
|
||||
- **Compact results:** Use retained Frida/GDB/objdump summaries, omit redundant Ghidra raw results, and defer the analysis runtime during MCP catalog discovery. [#5661](https://github.com/morluto/rea/pull/5661), [#5614](https://github.com/morluto/rea/pull/5614), [#5355](https://github.com/morluto/rea/pull/5355), [#4424](https://github.com/morluto/rea/pull/4424), [#5640](https://github.com/morluto/rea/issues/5640), [#5584](https://github.com/morluto/rea/issues/5584), [#5337](https://github.com/morluto/rea/issues/5337). Thanks @coygeek, @gensokyo-dev, @kaoru0822-kitauji, @leeguooooo.
|
||||
- **Analysis work:** Reuse Hopper literal-string records, JavaScript workers and captured prefix selections, Interface Builder validation and plist scalar schemas; bound stable reads to one snapshot buffer and avoid decoding omitted GGUF strings. [#5017](https://github.com/morluto/rea/pull/5017), [#5819](https://github.com/morluto/rea/pull/5819), [31de177](https://github.com/morluto/rea/commit/31de177404461d122761d8243abe4fd24564f4da), [#5885](https://github.com/morluto/rea/pull/5885), [85e38ac](https://github.com/morluto/rea/commit/85e38ac199dff15e8c43343d9170db7e0e46d284), [#6966](https://github.com/morluto/rea/pull/6966), [#5781](https://github.com/morluto/rea/pull/5781), [#5010](https://github.com/morluto/rea/issues/5010), [#5855](https://github.com/morluto/rea/issues/5855), [#6954](https://github.com/morluto/rea/issues/6954), [#5774](https://github.com/morluto/rea/issues/5774). Thanks @gensokyo-dev, @kaoru0822-kitauji.
|
||||
- **Native scheduling:** Batch modern IDA callee lookups, schedule decompilation reads on demand, index NativeAOT type closure, and retain call-path ancestry once per visited node. [0c6fed9](https://github.com/morluto/rea/commit/0c6fed90b74c75a9fd23d1ec1b510a7274f85887), [06fe7ee](https://github.com/morluto/rea/commit/06fe7eecbc8c387414b4c1aa0d88ad6314f54f0c), [ce61680](https://github.com/morluto/rea/commit/ce61680c04c95a9cdfad5335632a99542af26d37), [5e4ec0f](https://github.com/morluto/rea/commit/5e4ec0fa1b678aa3bf20704ec9110224ba8c1676).
|
||||
- **Selected processing:** Prepare CLI positional schemas only for the selected command, separate entropy reads from report resolution, prune typed-array reference escapes, index state-write cutoffs, decode only selected replay query values, and skip payload text during WAT framing. [315b234](https://github.com/morluto/rea/commit/315b23403f0819967878cee418c3f9cb4b1a69d9), [#6950](https://github.com/morluto/rea/pull/6950), [#5139](https://github.com/morluto/rea/pull/5139), [#5350](https://github.com/morluto/rea/pull/5350), [96ea32b](https://github.com/morluto/rea/commit/96ea32b7e4e28d3c9cd79a1de4b8e1ba3b4244e9), [70835d3](https://github.com/morluto/rea/commit/70835d3d5f030deadf606e1c79c70a60883e50c9), [#5766](https://github.com/morluto/rea/pull/5766), [#6947](https://github.com/morluto/rea/issues/6947), [#5760](https://github.com/morluto/rea/issues/5760). Thanks @galleonli, @gensokyo-dev, @kaoru0822-kitauji.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **IPS report fields:** Preserve the recorded terminating process and PID, termination flags, parent PID, and build/platform metadata. [#7079](https://github.com/morluto/rea/pull/7079), [#7078](https://github.com/morluto/rea/issues/7078). Thanks @rodricksz4h5.
|
||||
- **Ghidra language selection:** Accept stock language IDs containing `LEBE` or slash-separated processor names, and report conflicting DOL language overrides as configuration errors naming `REA_GHIDRA_LANGUAGE_ID`. [#7070](https://github.com/morluto/rea/pull/7070), [#7072](https://github.com/morluto/rea/issues/7072), [#7081](https://github.com/morluto/rea/pull/7081), [#7082](https://github.com/morluto/rea/issues/7082). Thanks @Adolanium.
|
||||
- **Setup and Hopper:** Preserve the Linux embedding runtime, supported x86_64 host restriction, readiness-based provider choices, orderly demo-helper shutdown, and installed apt dependency versions. Keep cleanup and skill publication ownership through retries. [#5041](https://github.com/morluto/rea/pull/5041), [#3422](https://github.com/morluto/rea/pull/3422), [f5d1012](https://github.com/morluto/rea/commit/f5d1012fabfe99af82523c3078ce9d93eb3780c1), [#4966](https://github.com/morluto/rea/pull/4966), [#6974](https://github.com/morluto/rea/pull/6974), [211e353](https://github.com/morluto/rea/commit/211e3533b34f81ae0910ad4f53f2a3d3c8587d3e), [#6920](https://github.com/morluto/rea/pull/6920), [#5015](https://github.com/morluto/rea/issues/5015), [#3419](https://github.com/morluto/rea/issues/3419), [#4921](https://github.com/morluto/rea/issues/4921), [#6975](https://github.com/morluto/rea/issues/6975). Thanks @Abel-I-Akhigbe, @Adolanium, @DavidFB-creator, @DeryFerd, @gensokyo-dev, @kaoru0822-kitauji, @sx4im.
|
||||
- **Client setup:** Treat empty APPDATA as unset, distinguish MSIX Claude Desktop process views, preserve aliased Hermes server tables, reuse client dialect/alias bindings, and materialize the pinned package-runner cache before the first start. [#5126](https://github.com/morluto/rea/pull/5126), [#4903](https://github.com/morluto/rea/pull/4903), [#3785](https://github.com/morluto/rea/pull/3785), [b7eeded](https://github.com/morluto/rea/commit/b7eededf5af5061a834f93f57f2c888acf870344), [#7032](https://github.com/morluto/rea/pull/7032), [#5104](https://github.com/morluto/rea/issues/5104), [#4278](https://github.com/morluto/rea/issues/4278), [#3320](https://github.com/morluto/rea/issues/3320), [#7027](https://github.com/morluto/rea/issues/7027). Thanks @CmsChase, @coygeek, @myso-kr, @pablodiazjorge, @vaidehi-snk, @wxj006007.
|
||||
- **Target admission:** Preserve thin arm64_32 Mach-O support, distinguish JVM identity before FAT admission, validate FAT format/CPU identity, preserve original DOS header padding, and reject NULs in local objdump/rizin paths. [#5788](https://github.com/morluto/rea/pull/5788), [6843dbe](https://github.com/morluto/rea/commit/6843dbea9bce4709ee2e8bf46c38f0bbf640716b), [497037e](https://github.com/morluto/rea/commit/497037e6e3975cbb37cfd89ea0ad300faa2a4769), [#7013](https://github.com/morluto/rea/pull/7013), [#3600](https://github.com/morluto/rea/pull/3600), [#5713](https://github.com/morluto/rea/issues/5713), [#6510](https://github.com/morluto/rea/issues/6510). Thanks @coygeek, @dhruvac29, @kaoru0822-kitauji, @Lew1sWong, @qorvr.
|
||||
- **Mach-O and dylibs:** Preserve container framing, facts when dyld rejects a missing UUID, verified-signature snapshot identity, unsigned diagnostics, absolute paths inside the selected root, legacy platform rules, lazy-load payloads, and reasons for rejecting present dylibs. [cf3288d](https://github.com/morluto/rea/commit/cf3288d8cf7a09685acf89103b36c32359be56fa), [2b263a7](https://github.com/morluto/rea/commit/2b263a7fe9e9d4c185c7d6af6b4e67b21a73af30), [eb7acc7](https://github.com/morluto/rea/commit/eb7acc70bf06f119e8639e30a78081d5ba82b0bf), [1502cfb](https://github.com/morluto/rea/commit/1502cfbc8ca0d1d1d86eebf334018dfb418bbac0), [90062c0](https://github.com/morluto/rea/commit/90062c092228dea9d33fadc0d7e1b968105b8b2c), [#5650](https://github.com/morluto/rea/pull/5650), [3b67b36](https://github.com/morluto/rea/commit/3b67b362aa2cd79eac5af57d0965e81a7d0b8c2b), [#5551](https://github.com/morluto/rea/issues/5551). Thanks @coygeek.
|
||||
- **Native facts:** Admit direct dispatch-metadata providers; preserve record-local Objective-C omissions, snapshot profiles, class-import binding identity, stripped class labels, version-like names, and qualified Swift protocol identities. [50b6975](https://github.com/morluto/rea/commit/50b6975d861069e2c6cfa202780621449f863802), [9eec57b](https://github.com/morluto/rea/commit/9eec57b469087fe9bff3d29dcc02f47074159e91), [0ba4d6f](https://github.com/morluto/rea/commit/0ba4d6f43ad610dacc861a1548be1a5c4712cd61), [4d7de00](https://github.com/morluto/rea/commit/4d7de00d4cc9a95ec2b7a3123b04e01db2daafa0), [22c84ce](https://github.com/morluto/rea/commit/22c84ce64aa6c7601aa81af86d4117b5a4c2feac), [4852c83](https://github.com/morluto/rea/commit/4852c830af1e3b9df437235b7a8fa8970a6d1c59), [aa9043f](https://github.com/morluto/rea/commit/aa9043f6b61c453e4735795cfe6a559bf6b6cc16).
|
||||
- **Apple resources:** Preserve independent plist scalars, processing instructions, nonstandard table widths, unsupported NIB connector coverage, and strict IPS JSON surrogate validation; classify Interface Builder objects by their declared superclass chains. [fd728f2](https://github.com/morluto/rea/commit/fd728f271e7de7bcfa63890b573715720d5f948c), [#5806](https://github.com/morluto/rea/pull/5806), [4526862](https://github.com/morluto/rea/commit/4526862728af01f1135714079c0d8a8c754eb364), [14cd78c](https://github.com/morluto/rea/commit/14cd78c3224edcf79fca152915077112f547b9ed), [#6990](https://github.com/morluto/rea/pull/6990), [#5805](https://github.com/morluto/rea/issues/5805), [#5228](https://github.com/morluto/rea/pull/5228), [#5218](https://github.com/morluto/rea/issues/5218). Thanks @coygeek, @DeryFerd, @kaoru0822-kitauji, @Mangolycheematcha, @mhamzasakhi, @Shreyash021104.
|
||||
- **Archives and files:** Complete positional ZIP reads, preserve bounded ASAR signatures, report stable symlink loops as resolution failures, retain target applicability, and report unknown traversal coverage. Retain reported integrity contradictions for members present in only one compared artifact. [#4414](https://github.com/morluto/rea/pull/4414), [e8da7e1](https://github.com/morluto/rea/commit/e8da7e1edd075feec0e6beedec93d5645fed8125), [#5571](https://github.com/morluto/rea/pull/5571), [#5763](https://github.com/morluto/rea/pull/5763), [#5867](https://github.com/morluto/rea/pull/5867), [#5521](https://github.com/morluto/rea/issues/5521), [#5731](https://github.com/morluto/rea/issues/5731), [#5837](https://github.com/morluto/rea/issues/5837), [#7076](https://github.com/morluto/rea/pull/7076). Thanks @Afloat16, @coygeek, @jdotc1, @kaoru0822-kitauji, @WenyuChiou.
|
||||
- **Extraction and file ownership:** Restrict destination-case recovery to owned collisions, retain physical-read failures through late cancellation/cleanup, preserve cleanup capabilities for retry, and retain independent firmware extraction failures. [932fbb7](https://github.com/morluto/rea/commit/932fbb7d84a9a995b9aee3c1be7da05a8c4d7bbb), [9499667](https://github.com/morluto/rea/commit/949966712e68d8857a50a165ecddf04c2d7e6d3b), [56679dd](https://github.com/morluto/rea/commit/56679dd2ae31dda58e181f8bf99775014f7bb82b), [b3c912a](https://github.com/morluto/rea/commit/b3c912ab79ad147a37b6cfbe1552a1a5eecec629), [7298bd0](https://github.com/morluto/rea/commit/7298bd08d134860b84b4073f6703021916a5a4c5).
|
||||
- **Processes and cancellation:** Retain partial samples and shutdown gaps, unresolved descendants and caller abort reasons; bind cleanup errors to their owners, propagate comparison contradictions, and report no normal exit code for signalled targets. [ba866a2](https://github.com/morluto/rea/commit/ba866a26ecbe179a717e746cbc0d1605ae616ad5), [#5270](https://github.com/morluto/rea/pull/5270), [#6144](https://github.com/morluto/rea/pull/6144), [a721c42](https://github.com/morluto/rea/commit/a721c42c07dc674c32ab939627355b8a1bee97a5), [#6298](https://github.com/morluto/rea/pull/6298), [#6486](https://github.com/morluto/rea/pull/6486), [#5935](https://github.com/morluto/rea/issues/5935), [#5944](https://github.com/morluto/rea/issues/5944), [#6100](https://github.com/morluto/rea/issues/6100), [#6101](https://github.com/morluto/rea/issues/6101), [#6110](https://github.com/morluto/rea/issues/6110), [#6113](https://github.com/morluto/rea/issues/6113), [#6121](https://github.com/morluto/rea/issues/6121), [#6131](https://github.com/morluto/rea/issues/6131), [#6132](https://github.com/morluto/rea/issues/6132), [#6218](https://github.com/morluto/rea/issues/6218), [#6445](https://github.com/morluto/rea/issues/6445). Thanks @agorevski, @coygeek, @gongfpp, @kwakhyun, @mhamzasakhi.
|
||||
- **Provider cleanup:** Preserve Windows native handles until close is confirmed, native/Inspector/Electron cleanup ownership, Kuna probe identity and cleanup, IDA registration configuration, and Hopper preparation ownership. [#5772](https://github.com/morluto/rea/pull/5772), [87ab054](https://github.com/morluto/rea/commit/87ab054ea384b3fcbae2998b1f94685edcd5f624), [ff78165](https://github.com/morluto/rea/commit/ff78165978de9dbcf9fa16c2acf74bf85a69dc44), [04ae07a](https://github.com/morluto/rea/commit/04ae07ab8c9ae1a6697b6ad15489bbde515da8b5), [dd0015a](https://github.com/morluto/rea/commit/dd0015aaf156c5701ef9598bcc6c885e6b8da803), [b231c28](https://github.com/morluto/rea/commit/b231c28cf64c2f799ada99343f8f21e0e6acb15e), [#4505](https://github.com/morluto/rea/issues/4505), [#5616](https://github.com/morluto/rea/issues/5616).
|
||||
- **Provider diagnostics:** Give Ghidra restart recovery, stable Homebrew aliases, unsupported-host results for Linux-only integrations, correct IDA regex/readiness handling, and clear diagnostics when Windows refuses a verified native addon. [#5356](https://github.com/morluto/rea/pull/5356), [7c6142e](https://github.com/morluto/rea/commit/7c6142efdc19a42c148d0e8c96b69d18e63c7f87), [#5793](https://github.com/morluto/rea/pull/5793), [#4862](https://github.com/morluto/rea/pull/4862), [99ccfb6](https://github.com/morluto/rea/commit/99ccfb6e709ac4bc0afe176315280abde9ebcb9f), [#6832](https://github.com/morluto/rea/pull/6832), [#5730](https://github.com/morluto/rea/issues/5730), [#4860](https://github.com/morluto/rea/issues/4860), [#5723](https://github.com/morluto/rea/issues/5723). Thanks @coygeek, @dark13b, @gensokyo-dev, @jgt87, @jpmorris41488, @Lew1sWong.
|
||||
- **MCP results:** Preserve protocol envelopes on oversized errors, enforce and advertise absolute Evidence paths, retain typed output-filter errors and selected JSON paths, and give client/provider timeout recovery. Compact advertisements now normalize reported enum and union shapes for strict gateway translation. [562dab0](https://github.com/morluto/rea/commit/562dab00c5cbf76a9b2695bad93792eccdc9a49d), [3cf6206](https://github.com/morluto/rea/commit/3cf6206614e65b5ebb6284a5709090dfb8f9aaee), [0e83d92](https://github.com/morluto/rea/commit/0e83d92c583ae36afe00b2c23dd2a0f1d025cbca), [4714c7e](https://github.com/morluto/rea/commit/4714c7eaa3d1d8b6a5df0aff586e844b9d9ab06d), [1bfa60a](https://github.com/morluto/rea/commit/1bfa60ac20e1a344bcf3132c08d15b6860b05c61), [#5608](https://github.com/morluto/rea/pull/5608), [#3806](https://github.com/morluto/rea/pull/3806), [#5598](https://github.com/morluto/rea/issues/5598), [#3800](https://github.com/morluto/rea/issues/3800). Thanks @gensokyo-dev, @Hahaa909, @wang1xiang.
|
||||
- **Browser lifecycles:** Retain historical outcomes through cleanup, stop late navigation before session release, stop expired work, observe aborted fetch settlement, and mark later scenario actions skipped after failure or cancellation. [2e5171e](https://github.com/morluto/rea/commit/2e5171e391b83f8a467d0861fd452c1f78621fd8), [599fdac](https://github.com/morluto/rea/commit/599fdac029912f96c511f859660483405d2de26f), [b2fad36](https://github.com/morluto/rea/commit/b2fad362f2b37f81dc9a6f784f4521e4ca24d41e), [13a8640](https://github.com/morluto/rea/commit/13a864020414577006dce58d7fdaa877ddd0cb52), [#6023](https://github.com/morluto/rea/pull/6023), [#5943](https://github.com/morluto/rea/issues/5943). Thanks @coygeek, @kaoru0822-kitauji.
|
||||
- **Browser comparisons:** Preserve frame identity, default origin scope, unknown body/shape coverage, correspondence and uncertainty, and independent retained-observation budgets. [#4040](https://github.com/morluto/rea/pull/4040), [#4413](https://github.com/morluto/rea/pull/4413), [#4476](https://github.com/morluto/rea/pull/4476), [a0c22ff](https://github.com/morluto/rea/commit/a0c22ff0125b8af7612db54d3708413e2bfecb25), [a0fcc60](https://github.com/morluto/rea/commit/a0fcc60d89eb0cc09b08181eb9f961acfd07d8e9), [#3878](https://github.com/morluto/rea/issues/3878), [#3910](https://github.com/morluto/rea/issues/3910), [#3941](https://github.com/morluto/rea/issues/3941), [#4385](https://github.com/morluto/rea/issues/4385), [#4477](https://github.com/morluto/rea/issues/4477). Thanks @Adolanium, @coygeek, @eco-null, @gensokyo-dev, @kaoru0822-kitauji, @XY-code1.
|
||||
- **Electron observations:** Preserve listener callback ownership and count registrations by source callsite; block trash calls in the active hook, retain window handles through cancelled selection, and preserve cancellation during finalization. [dfe8378](https://github.com/morluto/rea/commit/dfe8378cf5a727e3d93d47a5986233175c6ef898), [5064007](https://github.com/morluto/rea/commit/506400748eb1d6c674f0cf51330073929c45f939), [816a9dc](https://github.com/morluto/rea/commit/816a9dc98886e6d824cd8ae88b68731b161d5dda), [1707d17](https://github.com/morluto/rea/commit/1707d171d750955bf6387e028e378183b360ffe3), [4e2a566](https://github.com/morluto/rea/commit/4e2a566d6c7e053ecbbee1b684311ed31ea163bf).
|
||||
- **JavaScript source maps:** Keep unmapped and missing mappings unknown, preserve partial indexed-map facts and section paths, propagate preparation capability errors, and retain observed worker failures through cancellation. [a4dbb36](https://github.com/morluto/rea/commit/a4dbb36d8ea83e9f94dd289ac1a2d220d2c72c9b), [2b2e362](https://github.com/morluto/rea/commit/2b2e362a00fc5ba2f93fa6c62e2d8780072d1b4b), [498a667](https://github.com/morluto/rea/commit/498a667234b0140c1cfa43c819b35454ab11d14e), [#6149](https://github.com/morluto/rea/pull/6149), [#6214](https://github.com/morluto/rea/pull/6214), [f01dcca](https://github.com/morluto/rea/commit/f01dccab77f712fb4ac936d0a6e92a70d12f0789), [#6082](https://github.com/morluto/rea/issues/6082), [#6061](https://github.com/morluto/rea/issues/6061). Thanks @coygeek, @diyasharma05, @kaoru0822-kitauji.
|
||||
- **JavaScript semantics:** Preserve overflowing timer uncertainty, unavailable-scope comparisons, directory-form URL/ESM path intent, and artifact confinement. Replaced globals lose built-in effects; erased imports no longer become runtime bindings. Treat a known truthy `fork()` `silent` option as piped stdio unless a string/array `stdio` override takes precedence; unresolved options stay dynamic. [#3456](https://github.com/morluto/rea/pull/3456), [8d198d6](https://github.com/morluto/rea/commit/8d198d6ec707402d75276f516e0090d71767b7e4), [#4646](https://github.com/morluto/rea/pull/4646), [#2700](https://github.com/morluto/rea/pull/2700), [#4565](https://github.com/morluto/rea/pull/4565), [#6994](https://github.com/morluto/rea/pull/6994), [#3400](https://github.com/morluto/rea/issues/3400), [#3882](https://github.com/morluto/rea/issues/3882), [#3367](https://github.com/morluto/rea/issues/3367), [#7083](https://github.com/morluto/rea/pull/7083), [#7084](https://github.com/morluto/rea/issues/7084). Thanks @Adolanium, @amyanger, @coygeek, @gensokyo-dev, @HawkForge, @rohitg00, @viniciusdsandrade.
|
||||
- **JavaScript routes and imports:** Treat path data as route candidates, preserve route flag and lexical RPC identities, package-main trailing-slash selection, catch aliases, and factory parameter ownership. [#6360](https://github.com/morluto/rea/pull/6360), [38ecc02](https://github.com/morluto/rea/commit/38ecc0248e1993d25a2f7dd350024484cce797bb), [#4219](https://github.com/morluto/rea/pull/4219), [21097b1](https://github.com/morluto/rea/commit/21097b192b1ab94647e3446b1a2a4dd00e924d5e), [f774dc4](https://github.com/morluto/rea/commit/f774dc420b3befe2b8e68971b4116da2d62fc800), [#6349](https://github.com/morluto/rea/issues/6349), [#4189](https://github.com/morluto/rea/issues/4189). Thanks @coygeek, @hemv-857, @kaoru0822-kitauji, @Nikhi00718.
|
||||
- **Managed/IL2CPP:** Preserve extension-only scope uncertainty and type discovery after member limits, nested names, ReadyToRun/declaration facts, selected type scope, valid shared-suffix strings, and truncated IL2CPP metadata prefixes. [#5674](https://github.com/morluto/rea/pull/5674), [#5312](https://github.com/morluto/rea/pull/5312), [87fa1b6](https://github.com/morluto/rea/commit/87fa1b613d2b6bda1722855a00f1390f27731b64), [bc2d494](https://github.com/morluto/rea/commit/bc2d494030efd1ccabaeaa0bd49226aa87c378a2), [1f0eb22](https://github.com/morluto/rea/commit/1f0eb228e50908ee2a2aaa72d329178ae1a2cdbd), [4c5fae5](https://github.com/morluto/rea/commit/4c5fae5179dd54c4a0d427cd2a115b9c887e3841), [b3aadaf](https://github.com/morluto/rea/commit/b3aadaf474e59b33e5ff465236008a67eccefb87), [#5467](https://github.com/morluto/rea/issues/5467), [#5146](https://github.com/morluto/rea/issues/5146). Thanks @coygeek, @eco-null, @elricfd, @umi008.
|
||||
- **Format boundaries:** Preserve independent facts across malformed inputs in model, GPU, WASM, Rust, Android, and SQLite inspection; report optional metadata warnings without discarding core results. [eb41f35](https://github.com/morluto/rea/commit/eb41f358cf83d5626f09de68332f9718f9aa6482), [#5789](https://github.com/morluto/rea/pull/5789), [00a9e95](https://github.com/morluto/rea/commit/00a9e95de91a4bd4d2a41f8aee704a53fa575750), [fc63fe8](https://github.com/morluto/rea/commit/fc63fe8682654086261540dcf3c59afc36c7d58f), [39a91d4](https://github.com/morluto/rea/commit/39a91d4db96ca13224c25936330ea84fe78970e5), [6c6e2ad](https://github.com/morluto/rea/commit/6c6e2ad62b20f810b54f5a567e66fc55241514b2), [929a0a7](https://github.com/morluto/rea/commit/929a0a76c870d9991e015dde58308fd9df0280b8), [d137a4c](https://github.com/morluto/rea/commit/d137a4cdde0049de967f1d5cd429ed9e1f7d0a10), [ef475fc](https://github.com/morluto/rea/commit/ef475fcf8b0601c61052b7091568ba574546417d), [#5749](https://github.com/morluto/rea/issues/5749). Thanks @coygeek, @gensokyo-dev.
|
||||
- **Capture decoding:** Preserve malformed protobuf trailer facts and wire/schema identity, HTTP/2 URI and UTF-8 failure facts, WebMCP property coverage, and offline capture availability without a browser provider. Report invalid record ordinals as selection errors with actual bounds where available, and trim surrounding SP/HTAB from gRPC-web trailer values while preserving internal whitespace. [9cf7a7c](https://github.com/morluto/rea/commit/9cf7a7cb0d3c72a1121fde280cb11bf911af8055), [b3c3b21](https://github.com/morluto/rea/commit/b3c3b21e43cab4e71b460d08829d8c6e4e9b9c21), [4cb1f1d](https://github.com/morluto/rea/commit/4cb1f1d820df4498ee464bcf72a5f47d40cf8dab), [51e5938](https://github.com/morluto/rea/commit/51e593877642ab42fb5bf25812f4a4110ee7ac26), [d23be5c](https://github.com/morluto/rea/commit/d23be5c0898c39c9f0c54e2e0b43ee746d92d316), [#6725](https://github.com/morluto/rea/pull/6725), [#3947](https://github.com/morluto/rea/issues/3947), [#7067](https://github.com/morluto/rea/pull/7067), [#7068](https://github.com/morluto/rea/issues/7068), [#7069](https://github.com/morluto/rea/pull/7069), [#7071](https://github.com/morluto/rea/issues/7071). Thanks @Adolanium, @coygeek, @Sriram-PR.
|
||||
- **Evidence and errors:** Authenticate ledger Evidence before committing; preserve parent-schema diagnostics, canonical correction details and owned cleanup outcomes; identify the rejected function-Evidence side/kind. Fixed allowances now name their actual limits. [0407bbf](https://github.com/morluto/rea/commit/0407bbfbe65e8fa475e8bb0c78c30170e2c2f55e), [#6361](https://github.com/morluto/rea/pull/6361), [1629266](https://github.com/morluto/rea/commit/1629266e3c08c90fed6bba5383528af7986a12b7), [#6969](https://github.com/morluto/rea/pull/6969), [#5860](https://github.com/morluto/rea/pull/5860), [#6034](https://github.com/morluto/rea/issues/6034), [#5932](https://github.com/morluto/rea/issues/5932), [#5847](https://github.com/morluto/rea/issues/5847). Thanks @AyoubElleuch, @coygeek, @dhruvac29, @kaoru0822-kitauji.
|
||||
- **ADB and firmware recovery:** Report an unparsed push checksum as unverified, normalize absent installer-package sentinels, describe extraction-relative Unblob links, and avoid recommending a budget increase at its fixed ceiling. [#6010](https://github.com/morluto/rea/pull/6010), [#4073](https://github.com/morluto/rea/pull/4073), [#4573](https://github.com/morluto/rea/pull/4573), [#6971](https://github.com/morluto/rea/pull/6971), [#5924](https://github.com/morluto/rea/issues/5924), [#4175](https://github.com/morluto/rea/issues/4175). Thanks @coygeek, @Ganesh2325, @kaoru0822-kitauji, @Rinkia, @XiaoBinGan, @yankikucuk.
|
||||
- **CLI formatting:** Share Markdown report formatting, exclude inherited row values, and preserve arrays of empty objects as JSON. Render Markdown table headers and cells literally while keeping intraword underscores readable. [b784912](https://github.com/morluto/rea/commit/b784912811fd10ac9dc425696dc0e9af632afa34), [#6984](https://github.com/morluto/rea/pull/6984), [#7058](https://github.com/morluto/rea/pull/7058), [#7057](https://github.com/morluto/rea/issues/7057), [#7073](https://github.com/morluto/rea/pull/7073). Thanks @Plastic-time, @ShuaixinHuang, @viniciusdsandrade.
|
||||
- **Format identity:** Preserve Python marshal scalar/decode limits, empty optional BEAM chunks, PE resource branch identities and certificate alignment, unsupported compressed ELF notes, and codec-framed GPU bundle extents. [0603223](https://github.com/morluto/rea/commit/06032237c6755e3dd5f9a8db732c89958b238cc7), [c46064c](https://github.com/morluto/rea/commit/c46064c3f6eaba121c5de7b17390bfdecdd132ef), [66671e8](https://github.com/morluto/rea/commit/66671e874888e98f7e2b27e68a7c6fb320bbb827), [03077d5](https://github.com/morluto/rea/commit/03077d54afc49613db4e38f70c506bc60f5223aa), [#6721](https://github.com/morluto/rea/pull/6721), [9fb8c11](https://github.com/morluto/rea/commit/9fb8c114a03ad60e57c11586967c6df845a3f97e), [f6a1e2c](https://github.com/morluto/rea/commit/f6a1e2c892dc23c7583c3d03af9edb01632ff5a5), [#6694](https://github.com/morluto/rea/issues/6694). Thanks @coygeek, @Deepak-Kambala.
|
||||
- **WASM and APK metadata:** Preserve ambiguous WASM identity and partial facts, glue URL query/fragment text across line terminators, APK signer identity, and raw issuer identity when comparison rules are unknown. [af8f0ba](https://github.com/morluto/rea/commit/af8f0badf64fd3546392b6aa4625777904b2f051), [#6802](https://github.com/morluto/rea/pull/6802), [f783f0f](https://github.com/morluto/rea/commit/f783f0f1e82472cdddd4de2156d974f729e332a6), [115a78f](https://github.com/morluto/rea/commit/115a78fe241276c34917b496621954c5f29bfc15), [#6639](https://github.com/morluto/rea/issues/6639). Thanks @coygeek, @gensokyo-dev, @gongfpp.
|
||||
- **Source identity:** Preserve Next route spelling, module grammar and loader request identity, directory package-main behavior, accessor effects, exact source targets, and resolver-discovery versus module-loading distinctions. [ddd7ac7](https://github.com/morluto/rea/commit/ddd7ac7b846b48191ead1f7dcd0e3bd735468fa8), [9974512](https://github.com/morluto/rea/commit/99745126009b15060d71c38809418858a07d5afa), [f2e5fd1](https://github.com/morluto/rea/commit/f2e5fd148a85fa737302dd194a33172c4561093e), [baf1678](https://github.com/morluto/rea/commit/baf16784af3592534687052431c145daef55cfd1), [6e324b2](https://github.com/morluto/rea/commit/6e324b224405f2befd953234f6dc210cf72ad50a), [75daf0c](https://github.com/morluto/rea/commit/75daf0c7df108f04ab41f34aec1a31d4a7ceb322), [f91b7f8](https://github.com/morluto/rea/commit/f91b7f8009c9af27a5e0148e3b07d20bfe3b7a2a).
|
||||
- **Graphs and selected facts:** Preserve JavaScript graph omissions/version facts, Electron native-loader facts, web JSON identity/coverage, module-path selection, feature-trace qualifications, selected native paths, endpoint context, and mixed Swift demangler outcomes. [30db499](https://github.com/morluto/rea/commit/30db499db310f073d9bf3d3acfd77985e1cb7e2e), [5a7bddb](https://github.com/morluto/rea/commit/5a7bddb3aeb0345854e76b79d529aed1c40e2054), [79805f2](https://github.com/morluto/rea/commit/79805f244a1ec0850f3109929168a9c8ebb1b908), [f719dc4](https://github.com/morluto/rea/commit/f719dc46c51a55b3926eec9659d06e7dfb95132b), [7784fdb](https://github.com/morluto/rea/commit/7784fdbad78f8b384d513f0e7dae8e983cd48ef8), [76500f1](https://github.com/morluto/rea/commit/76500f12b54e07b1ea700a403b2593890d63b42a), [676d29f](https://github.com/morluto/rea/commit/676d29f9eef794cea668f5f09f0ff510c848a513), [edf7b3d](https://github.com/morluto/rea/commit/edf7b3d32c3248f221e27f7ce5a3d34e8590285b), [93de7bc](https://github.com/morluto/rea/commit/93de7bc7e324a761162a31625b838ba0d4e3824d).
|
||||
- **Managed boundaries:** Honor native platform symbol lookup, consume resolved IL2CPP ELF declarations, and attribute malformed comparison inputs to the supplied argument. [64fa1db](https://github.com/morluto/rea/commit/64fa1db1309218283df19f3ee4dba61f5fbd2a67), [87745af](https://github.com/morluto/rea/commit/87745af52ab0ccd3a172c2534c516d41b0095035), [96ad94f](https://github.com/morluto/rea/commit/96ad94f69eda5d68c7335ff775b6927b1376fd2b).
|
||||
- **Provider and worker recovery:** Preserve output loss after successful command exit, physical refusal diagnostics, bounded worker control framing, JavaScript worker-phase/budget attribution, admitted ambiguity choices, and selected EVM limiter availability. Scope objdump debug-link limitations to DWARF requests and keep process-capture preparation nonexecuting. [e639edf](https://github.com/morluto/rea/commit/e639edf6204c05285d96840478926592daf95876), [171b33b](https://github.com/morluto/rea/commit/171b33b2c4defc6cbea49545bedf4272b72d1ea2), [34563fc](https://github.com/morluto/rea/commit/34563fcc9bd92af30668820dcac5635e27181094), [3f9357b](https://github.com/morluto/rea/commit/3f9357b5990d166a38440f56f439452266650093), [cec8f1e](https://github.com/morluto/rea/commit/cec8f1e8f305d6332272457a6ee419d3c5fb6359), [a74d859](https://github.com/morluto/rea/commit/a74d859a19122fb8f1282b3632d320997b1caae0), [c284168](https://github.com/morluto/rea/commit/c284168c4b1cd214ce7f504d43da28528d82d603), [4fb936f](https://github.com/morluto/rea/commit/4fb936fe86bb7a07f94f819ae8510301f2ef69c9).
|
||||
- **Setup and limits:** Retain unknown environment-budget facts, selected-skill/maintenance uncertainty, Hopper URL admission and bookmark verification, and listener failure recovery that matches the system cause. [a718037](https://github.com/morluto/rea/commit/a7180370332350bc0f998fbd643888221759570e), [d1fbebb](https://github.com/morluto/rea/commit/d1fbebb578538dc94fd5e376e6e1959218efa835), [60accad](https://github.com/morluto/rea/commit/60accad2de8f2862fe91901502fe26cc5390fb36), [fd7da77](https://github.com/morluto/rea/commit/fd7da77fc02dcfaf7d1bc9f2fb240d71cdd27c59), [0c440bf](https://github.com/morluto/rea/commit/0c440bfc07f3cb343e2e836d27cccaed539cfe9c).
|
||||
- **State and Evidence:** Retain export tarball ownership and SQLite primary outcomes through cleanup retry, count emitted SQLite schema names across encodings, scope display refresh to matching snapshot bindings, and report retained-result validation failures consistently. Reconstruction uses one effective completion scope and kind-based labels when names are blank. [ab58edc](https://github.com/morluto/rea/commit/ab58edcdf50b4bd687900ee2dc362ecc5c3dce15), [1006714](https://github.com/morluto/rea/commit/100671427105791a5c5913a918b63a394f6bc83e), [a47788a](https://github.com/morluto/rea/commit/a47788a6a4abb067273f954275a9d0a269adf1e7), [8aa40e9](https://github.com/morluto/rea/commit/8aa40e9a61155cff8b6f3c138c601bf805b20653), [b8c1b45](https://github.com/morluto/rea/commit/b8c1b45316882beeab4a9de49381d94e131fab0f), [a35b933](https://github.com/morluto/rea/commit/a35b9335a0124674ee5e3abe8c075f1fff6b4fa6), [#6137](https://github.com/morluto/rea/pull/6137), [#6126](https://github.com/morluto/rea/issues/6126). Thanks @carlogiovannineri-rgb, @mohithhhh.
|
||||
- **Capture coverage:** Name browser bundle budgets and deadline recovery, separate domain readiness from worker attribution, attribute rendered-frame omissions to their producing stage, share offline-capture host admission with discovery, preserve selected signal-file semantics, and retain short Flutter GNU build IDs. [#6036](https://github.com/morluto/rea/pull/6036), [ba78133](https://github.com/morluto/rea/commit/ba781330d6d28b22d15498e63c144fb40e086156), [5934387](https://github.com/morluto/rea/commit/5934387f4185129efe9dc8b7338d8bc3fa5b25bd), [ad629b2](https://github.com/morluto/rea/commit/ad629b2e31c3a9495b27179360b508fa021fe86a), [928ba76](https://github.com/morluto/rea/commit/928ba76882bfd216dde8ed79118789b2c3aad4f9), [678c4cf](https://github.com/morluto/rea/commit/678c4cfa68e2fee12f620075e12dc87d9b1195b0).
|
||||
- **Additional boundary facts:** Retain parser format/query occurrence identity and independent malformed-input facts, unresolved nested Swift identifiers, and completed signing-capture coverage; describe crash documents without assuming a provider. [5f702dd](https://github.com/morluto/rea/commit/5f702dd6ccd9e8e505e224854b91678439c3b41f), [99d2d7a](https://github.com/morluto/rea/commit/99d2d7ad22bcbd780919760008aba6a88e7d1077), [90e433d](https://github.com/morluto/rea/commit/90e433df06b3f83417176b8db1b0a905f9df1d3a), [#5548](https://github.com/morluto/rea/pull/5548), [ee12d51](https://github.com/morluto/rea/commit/ee12d517a25d34853d40e31fc039c7d1576c60e1), [#5538](https://github.com/morluto/rea/issues/5538). Thanks @coygeek, @ShuaixinHuang.
|
||||
- **Documentation:** Add the Romanian README. [#7026](https://github.com/morluto/rea/pull/7026). Thanks @An3treiu.
|
||||
|
||||
Full contribution record: [1740 commits and 497 merged PRs](https://github.com/morluto/rea/blob/rea-agents-6.8.0/docs/releases/6.8.0.contributions.json).
|
||||
|
||||
## [6.7.0](https://github.com/morluto/rea/compare/rea-agents-6.6.0...rea-agents-6.7.0) (2026-10-18)
|
||||
|
||||
### Highlights
|
||||
|
||||
- Client configuration edits follow the duplicate-member semantics used by MCP clients.
|
||||
|
||||
### Changes
|
||||
|
||||
- Includes the [6.6.0 changes and migration notes](https://github.com/morluto/rea/releases/tag/rea-agents-6.6.0). That version was tagged but was not published to npm.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Client configuration:** Setup edits the last repeated JSON/JSONC member, matching the value clients actually read. Setup and uninstall refuse a removal that would reactivate a shadowed duplicate, preserving the original configuration. ([#4957](https://github.com/morluto/rea/pull/4957), [morluto/rea#4952](https://github.com/morluto/rea/issues/4952)). Thanks @kaoru0822-kitauji, @xu91102.
|
||||
|
||||
Full contribution record: [4 commits and 1 merged PR](https://github.com/morluto/rea/blob/rea-agents-6.7.0/docs/releases/6.7.0.contributions.json).
|
||||
|
||||
## [6.6.0](https://github.com/morluto/rea/compare/rea-agents-6.5.0...rea-agents-6.6.0) (2026-10-18)
|
||||
|
||||
### Highlights
|
||||
|
||||
- Add Kuna as a caller-supplied static-analysis provider.
|
||||
- Query selected JavaScript facts and multiple APK locale tables without repeating the underlying analysis.
|
||||
- Extend Ghidra analysis to Windows ARM PE targets and explicit raw m68k images.
|
||||
- Preserve completed Evidence, cancellation reasons and retryable cleanup across provider and CLI finalization.
|
||||
|
||||
### Changes
|
||||
|
||||
- **Kuna analysis:** Use a caller-installed Kuna for procedure listings, pseudocode, assembly, call relationships, exact-address xrefs and cached string searches. Project exports retain source spans and digests; repeated requests reuse completed work. Recorded real-provider coverage uses Kuna 1.833 on Linux; Windows, PE and universal Mach-O are unsupported. See [the provider guide](docs/kuna-provider.md). ([#3832](https://github.com/morluto/rea/pull/3832), [#3854](https://github.com/morluto/rea/pull/3854), [#4071](https://github.com/morluto/rea/pull/4071), [#4095](https://github.com/morluto/rea/pull/4095)). Thanks @gensokyo-dev, @kaoru0822-kitauji, @N0zoM1z0.
|
||||
- **Retained JavaScript facts:** Query exact application paths, selected properties and filtered module references through `inspect_analysis_view`, with paging and complete row Evidence. These views reuse completed analysis and preserve source selection and unknown coverage. ([#4265](https://github.com/morluto/rea/pull/4265), [morluto/rea#4190](https://github.com/morluto/rea/issues/4190), [#4097](https://github.com/morluto/rea/pull/4097), [morluto/rea#4092](https://github.com/morluto/rea/issues/4092)). Thanks @kaoru0822-kitauji, @N0zoM1z0.
|
||||
- **APK locale tables:** Request several resource locales in one decode with repeatable CLI `--locales` or MCP `locales`. Each table reports its status and coverage; existing single-locale fields remain available. ([#4804](https://github.com/morluto/rea/pull/4804), [morluto/rea#4777](https://github.com/morluto/rea/issues/4777)). Thanks @abhay-2108, @N0zoM1z0.
|
||||
- **Windows ARM binaries:** Ghidra admits ARMNT/Thumb-2 and ARM64 PE applications and DLLs on the Windows x64 host lane. Recorded Ghidra 12.1.4 verification uses `WindowsResourceReference` disabled and raised startup budgets; default-analyzer completion, ARM64EC/ARM64X and Windows-on-Arm hosts remain unverified or unsupported. See [the exact scope](docs/windows-ghidra-p0.md#native-authority-and-target-identity). ([#3965](https://github.com/morluto/rea/pull/3965), [morluto/rea#2186](https://github.com/morluto/rea/issues/2186)). Thanks @N0zoM1z0, @WestLabsAI.
|
||||
- **Raw m68k images:** Analyze explicitly selected big-endian m68k raw binaries using Ghidra’s fixed 68000 profile and a caller-supplied aligned entry. Real CLI/MCP verification used Ghidra 12.1.4 on macOS arm64; Linux is unverified and Windows P0 does not admit m68k. See [mapping requirements](docs/raw-binary.md). ([#4018](https://github.com/morluto/rea/pull/4018)). Thanks @icedac.
|
||||
- **JADX deadlines:** Set `REA_JADX_OPERATION_TIMEOUT_MS` to extend APK loading and upstream operation deadlines together. Doctor reports the effective value and setup forwards an explicit selection. ([#4466](https://github.com/morluto/rea/pull/4466), [morluto/rea#3918](https://github.com/morluto/rea/issues/3918)). Thanks @jaychou813, @SandstromPL.
|
||||
- **Electron window options:** Resolve stable local constant `BrowserWindow` options while retaining source provenance. Relevant writes and escapes keep the result unknown. ([#4400](https://github.com/morluto/rea/pull/4400), [morluto/rea#4366](https://github.com/morluto/rea/issues/4366)). Thanks @kaoru0822-kitauji, @N0zoM1z0.
|
||||
- **Language editions:** Add the Greek website edition and Malayalam documentation, with Korean typography and translation corrections across the site. ([#3520](https://github.com/morluto/rea/pull/3520), [#4792](https://github.com/morluto/rea/pull/4792), [#3821](https://github.com/morluto/rea/pull/3821), [morluto/rea#3820](https://github.com/morluto/rea/issues/3820), [#3799](https://github.com/morluto/rea/pull/3799), [morluto/rea#3798](https://github.com/morluto/rea/issues/3798), [#4344](https://github.com/morluto/rea/pull/4344), [#3856](https://github.com/morluto/rea/pull/3856), [#4166](https://github.com/morluto/rea/pull/4166)). Thanks @gensokyo-dev, @hi2242, @kalayciburak, @kaoru0822-kitauji, @kraken-503, @kukinghan, @N0zoM1z0, @yeseong0412.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Cancellation and cleanup:** Keep completed observations, exact cancellation reasons and the original physical failure through finalization. Failed cleanup retains its process or file owner for retry; recovered resources disappear from pending-cleanup reports without replacing the original error. ([#4238](https://github.com/morluto/rea/pull/4238), [morluto/rea#4232](https://github.com/morluto/rea/issues/4232), [#4274](https://github.com/morluto/rea/pull/4274), [morluto/rea#3923](https://github.com/morluto/rea/issues/3923), [3e82366](https://github.com/morluto/rea/commit/3e8236639abffa39deb5d5e939ec8baf6aaf3ca3), [16924e2](https://github.com/morluto/rea/commit/16924e275e939d0543ba01493324959a9e1d731c), [bbfe6ed](https://github.com/morluto/rea/commit/bbfe6edd60290f9bfbec68c8a0bcf605d8bbf5b3), [1f23e97](https://github.com/morluto/rea/commit/1f23e977b50301e91f02a4a17c9f8d48bb7b60f3), [08fbeb8](https://github.com/morluto/rea/commit/08fbeb8bdab27118b6c57039aef7c0fa8b6463bc)). Thanks @coygeek.
|
||||
- **Provider startup:** Join prerequisite probes and in-flight readiness work before shutdown, preserve typed timeout and JVM-memory failures, and retain startup diagnostics through cleanup. Ghidra recognizes keg-only Homebrew JDKs and names the setting responsible for a startup timeout. ([376eeca](https://github.com/morluto/rea/commit/376eeca770820b31f7f8d4c5fcc397315621643e), [ceeff5a](https://github.com/morluto/rea/commit/ceeff5a41788e47e5aee96a4baf191a6ce7fde4b), [3ad1032](https://github.com/morluto/rea/commit/3ad1032b11edea2dcb054e78f951cc43159ae83d), [964decd](https://github.com/morluto/rea/commit/964decd989acfa84c4bfc3295362c4c46877fc20), [3fa972a](https://github.com/morluto/rea/commit/3fa972af7c7cb6cf014ffc426bdf7130dc6f2fd2), [#4557](https://github.com/morluto/rea/pull/4557), [morluto/rea#4556](https://github.com/morluto/rea/issues/4556), [#4590](https://github.com/morluto/rea/pull/4590), [morluto/rea#4241](https://github.com/morluto/rea/issues/4241)). Thanks @DanielCvz1, @J0UH, @Rinkia, @RishTheFish21, @vadlachary2002.
|
||||
- **Client configuration:** Preserve the selected launcher, destination file, caller timeout choices and unresolved configuration evidence. Setup/uninstall recovery stays bound to the exact published file, keeps current verified backups, and transfers failed cleanup ownership for retry. ([d0253c9](https://github.com/morluto/rea/commit/d0253c99d65afbbd12732a2fdaf711640574e17f), [7e4eed9](https://github.com/morluto/rea/commit/7e4eed9dd48485b6a7100300aa98401811c2deb9), [2fb5e20](https://github.com/morluto/rea/commit/2fb5e204a35723f6f843d56ca47b0db94d8fcc44), [8235210](https://github.com/morluto/rea/commit/823521085a28f825e7fee1929d852d420042d1f7), [d818c3d](https://github.com/morluto/rea/commit/d818c3d2639d3115edb8a5269a754101aea6c3bd), [19329e4](https://github.com/morluto/rea/commit/19329e43bb38ca9d64512d801b25abcb188d2512), [dadf909](https://github.com/morluto/rea/commit/dadf909dce2e8b3d055541de69b4118d21276316), [333e0ab](https://github.com/morluto/rea/commit/333e0ab0dfc126a9915907d5079ed6195190e38e), [c15c206](https://github.com/morluto/rea/commit/c15c2062ad815cc9a3222ccfaeb7ef53cacd8b72)). Thanks @Adolanium.
|
||||
- **Configuration formats:** Read signed 64-bit TOML integers without rounding, handle large TOML documents without argument-spread failures, preserve JSONC trivia and Hermes YAML scalar text, and reject NUL bytes in registration environment values. ([#4937](https://github.com/morluto/rea/pull/4937), [morluto/rea#4917](https://github.com/morluto/rea/issues/4917), [c43bc0b](https://github.com/morluto/rea/commit/c43bc0bf66a4e245a0cafa40def0d0207bcbd73c), [4fd28fc](https://github.com/morluto/rea/commit/4fd28fc9e0c4f8358147af38c72ab9e814f52d61), [#3568](https://github.com/morluto/rea/pull/3568), [#4335](https://github.com/morluto/rea/pull/4335)). Thanks @alfredo1491, @gensokyo-dev, @kaoru0822-kitauji, @N0zoM1z0, @ShuaixinHuang, @xu91102.
|
||||
- **Claude Code configuration:** Setup, doctor and uninstall now accept only the strict JSON format Claude Code reads, including a leading UTF-8 BOM. Comments and trailing commas are refused before backup or writing; clients that support JSONC retain their existing behavior. ([#4944](https://github.com/morluto/rea/pull/4944), [morluto/rea#4940](https://github.com/morluto/rea/issues/4940)). Thanks @kaoru0822-kitauji, @xu91102.
|
||||
- **CLI output and dispatch:** Accept `--flag=value` and spaced global output values, keep array positions in filtered JSON, validate integer tokens before coercion, and exit quietly when the output reader closes. Cancellation remains effective through command finalization; app bundles and static native inspections select the intended workflow. ([#3811](https://github.com/morluto/rea/pull/3811), [#4823](https://github.com/morluto/rea/pull/4823), [2ae4e16](https://github.com/morluto/rea/commit/2ae4e1608f290c95ff7cff28c4d16a79b301d03d), [8d82579](https://github.com/morluto/rea/commit/8d8257928bbe8d43fedffc4db5fca26c8108820f), [#3781](https://github.com/morluto/rea/pull/3781), [279c590](https://github.com/morluto/rea/commit/279c590ef6bb737d4a48ea696a0ae5eb30010b3d), [#3511](https://github.com/morluto/rea/pull/3511), [morluto/rea#3321](https://github.com/morluto/rea/issues/3321), [f31b231](https://github.com/morluto/rea/commit/f31b231a92a61be6bd1800b16be0b3210ae82944)). Thanks @arjunprabhulal, @coygeek, @kaoru0822-kitauji, @ShuaixinHuang, @Sriram-PR, @Tapraj-Pathak.
|
||||
- **Updates:** Route `--update` through the verified updater and bound retained package-manager diagnostics. ([4f7b5fb](https://github.com/morluto/rea/commit/4f7b5fb6023ce2ec449561782d09e3c7e5ac7c7b), [01cdbbd](https://github.com/morluto/rea/commit/01cdbbdc556262c47d04a5b3391dd9c17946d3fc)).
|
||||
- **MCP contracts:** Advertise self-contained tool inputs while preserving tuple, array, Boolean and nested-JSON constraints across schema profiles. Schema-reference handling no longer rewrites literal metadata; catalog identity includes the output-schema profile. ([5e30969](https://github.com/morluto/rea/commit/5e30969cc5f1255d44ec7a1f1ca8dbac0af5e7c4), [10aa74d](https://github.com/morluto/rea/commit/10aa74dfedc3ae776f50d1fa1eb83b64ae82d3d6), [204b9f3](https://github.com/morluto/rea/commit/204b9f3ac140a993c3e458bb0e117de31818ab29), [b3389a5](https://github.com/morluto/rea/commit/b3389a5b0834265fe7a40391be315d810a53a97e), [a2e3016](https://github.com/morluto/rea/commit/a2e30168a046f3b79319eb7fbaf807b802104199), [ba98130](https://github.com/morluto/rea/commit/ba98130049fa40ade66b03611ff70c7ddc011a1d), [c5fb86d](https://github.com/morluto/rea/commit/c5fb86d1948af0a287a581b29b2b1d554743c607)). Thanks @akushonkamen, @HarsH-1877, @kaoru0822-kitauji, @xingzihai.
|
||||
- **Session ordering:** Keep snapshot admission ahead of later calls on the same connection, retain shutdown signals until cleanup succeeds, and preserve inspection authority through investigation publication. ([#3661](https://github.com/morluto/rea/pull/3661), [morluto/rea#3334](https://github.com/morluto/rea/issues/3334), [8873b39](https://github.com/morluto/rea/commit/8873b39770e89eae22b7693bc492d7e423b22bea), [663d07f](https://github.com/morluto/rea/commit/663d07f02cd62c99d81c59391d2c6d745c875335)). Thanks @coygeek, @rudycelekli, @Swatantra-66.
|
||||
- **JavaScript exports and resolution:** Retain CommonJS exports created with `Object.defineProperty` and track export receiver lifetimes. Resolve scoped packages, package `main`, JSON requires and `.mts`/`.cts` imports using the owning loader; overwritten `require` bindings and erased type-only imports no longer create false runtime links. ([#4725](https://github.com/morluto/rea/pull/4725), [morluto/rea#4724](https://github.com/morluto/rea/issues/4724), [#3567](https://github.com/morluto/rea/pull/3567), [morluto/rea#3541](https://github.com/morluto/rea/issues/3541), [#4838](https://github.com/morluto/rea/pull/4838), [morluto/rea#4632](https://github.com/morluto/rea/issues/4632), [#3844](https://github.com/morluto/rea/pull/3844), [morluto/rea#3383](https://github.com/morluto/rea/issues/3383), [#4164](https://github.com/morluto/rea/pull/4164), [morluto/rea#3915](https://github.com/morluto/rea/issues/3915), [#4223](https://github.com/morluto/rea/pull/4223), [#3801](https://github.com/morluto/rea/pull/3801), [morluto/rea#3393](https://github.com/morluto/rea/issues/3393), [#4165](https://github.com/morluto/rea/pull/4165), [morluto/rea#3932](https://github.com/morluto/rea/issues/3932)). Thanks @Abishek0070, @coygeek, @JNTMTMTM, @kaoru0822-kitauji, @prasad2512, @rodricksz4h5, @xu91102.
|
||||
- **JavaScript value flow:** Preserve values captured before later assignments, guarded factory fallthrough and nested destructuring. Track exact property keys and setter writes; direct `eval`, compound assignments and incoming callbacks preserve unresolved effects instead of inventing certainty. ([#4196](https://github.com/morluto/rea/pull/4196), [morluto/rea#3881](https://github.com/morluto/rea/issues/3881), [#4341](https://github.com/morluto/rea/pull/4341), [morluto/rea#3963](https://github.com/morluto/rea/issues/3963), [bfb7ed0](https://github.com/morluto/rea/commit/bfb7ed0bd43edc605e00fc52c6e21e735b285904), [#3480](https://github.com/morluto/rea/pull/3480), [#3831](https://github.com/morluto/rea/pull/3831), [morluto/rea#3312](https://github.com/morluto/rea/issues/3312), [9ac76d3](https://github.com/morluto/rea/commit/9ac76d3d9486856d25395b413d7ac5ffa744cad0), [#4342](https://github.com/morluto/rea/pull/4342), [morluto/rea#4206](https://github.com/morluto/rea/issues/4206), [#4243](https://github.com/morluto/rea/pull/4243), [morluto/rea#3880](https://github.com/morluto/rea/issues/3880)). Thanks @coygeek, @kaoru0822-kitauji, @maxazure, @N0zoM1z0, @ShuaixinHuang.
|
||||
- **JavaScript relationships:** Distinguish `Map` reads from network endpoints, associate response endpoints only with writes that reach the read, and preserve child-process receiver ownership and effective stdio choices. Version comparisons keep relocation and unknown relationships distinct from application changes. ([fbfdb6a](https://github.com/morluto/rea/commit/fbfdb6abe9d663be0be146a15a489e33ccba0f68), [#4759](https://github.com/morluto/rea/pull/4759), [morluto/rea#4606](https://github.com/morluto/rea/issues/4606), [#3819](https://github.com/morluto/rea/pull/3819), [morluto/rea#3362](https://github.com/morluto/rea/issues/3362), [#4336](https://github.com/morluto/rea/pull/4336), [morluto/rea#3355](https://github.com/morluto/rea/issues/3355), [556504e](https://github.com/morluto/rea/commit/556504edcbb0f8cd9eaf81a91b18ff891b8f5457), [4d980d0](https://github.com/morluto/rea/commit/4d980d017622d3175d86aaf3b0dcfbdf4ca3d091)). Thanks @Abdooo2235, @coygeek, @kaoru0822-kitauji, @Xinglan233.
|
||||
- **JavaScript source admission:** Parse JavaScript with its own grammar, refuse FIFO recovery inputs without blocking, and report oversized or resource-skipped files with their actual capacity and coverage state. Malformed runtime events stay excluded rather than becoming policy-filtered facts. ([#3782](https://github.com/morluto/rea/pull/3782), [morluto/rea#3780](https://github.com/morluto/rea/issues/3780), [#3788](https://github.com/morluto/rea/pull/3788), [morluto/rea#3332](https://github.com/morluto/rea/issues/3332), [#4757](https://github.com/morluto/rea/pull/4757), [morluto/rea#4629](https://github.com/morluto/rea/issues/4629), [#3852](https://github.com/morluto/rea/pull/3852), [morluto/rea#3670](https://github.com/morluto/rea/issues/3670), [#4755](https://github.com/morluto/rea/pull/4755), [morluto/rea#4685](https://github.com/morluto/rea/issues/4685)). Thanks @andresaragon, @coygeek, @kaoru0822-kitauji, @sx4im.
|
||||
- **Browser capture:** Bound CDP handshakes and command responses, preserve malformed inventory uncertainty and partial cleanup diagnostics, and report cancellation after content reads and final cleanup. Accessibility comparisons require observed coverage; attached emulation teardown limits remain explicit. ([#3555](https://github.com/morluto/rea/pull/3555), [morluto/rea#3553](https://github.com/morluto/rea/issues/3553), [#3901](https://github.com/morluto/rea/pull/3901), [morluto/rea#3379](https://github.com/morluto/rea/issues/3379), [4cc044b](https://github.com/morluto/rea/commit/4cc044be68f813e96907e6cedcf422d0a3170de3), [#4406](https://github.com/morluto/rea/pull/4406), [morluto/rea#4403](https://github.com/morluto/rea/issues/4403), [#2893](https://github.com/morluto/rea/pull/2893), [0123d59](https://github.com/morluto/rea/commit/0123d5976828bfe621bd51daa4cf39fee48a1349), [2ff0867](https://github.com/morluto/rea/commit/2ff086796ad7b8f60397956750584e170dbe2910)). Thanks @coygeek, @dakjdakd, @kaoru0822-kitauji, @maxazure, @N0zoM1z0, @YunzhouLi-hub.
|
||||
- **Browser page facts:** Preserve URL path bytes when removing credentials, repeated document `Link` headers and complete COOP/COEP structured items. WebMCP discovery reads the actual schema properties and exact receiver path. ([#4731](https://github.com/morluto/rea/pull/4731), [df2fc25](https://github.com/morluto/rea/commit/df2fc251e318a1255309dba232f9f73c700777b6), [c360cfc](https://github.com/morluto/rea/commit/c360cfcd6d7901514b16dd6113eba2e0b0fdf422), [ebd5a82](https://github.com/morluto/rea/commit/ebd5a82c439f6deb3a06062225ce009eb03257cf), [38f92d1](https://github.com/morluto/rea/commit/38f92d108e3148cdf47e4ffbfe3de8a76e795d51)). Thanks @Abdooo2235, @geekwolf, @ShuaixinHuang.
|
||||
- **Electron runtime capture:** Keep native IPC listener and one-shot dispatch behavior, block synchronous shell effects during capture, refuse active scenarios when the inspect fuse is disabled, and preserve fuse/transport cleanup ownership. ([#4389](https://github.com/morluto/rea/pull/4389), [morluto/rea#4390](https://github.com/morluto/rea/issues/4390), [#4605](https://github.com/morluto/rea/pull/4605), [morluto/rea#4593](https://github.com/morluto/rea/issues/4593), [#4562](https://github.com/morluto/rea/pull/4562), [morluto/rea#4393](https://github.com/morluto/rea/issues/4393), [#3605](https://github.com/morluto/rea/pull/3605), [morluto/rea#3453](https://github.com/morluto/rea/issues/3453), [f9de413](https://github.com/morluto/rea/commit/f9de41371f7536e2e22ce6e43c9314fa2f238a30), [e7df00e](https://github.com/morluto/rea/commit/e7df00e80777757b329bbe8aba6d421d8892b9d5)). Thanks @Adolanium, @gensokyo-dev, @gkobayashi0, @HarsH-1877, @kaoru0822-kitauji, @keeponlight, @N0zoM1z0, @Nitin13-git, @xu91102.
|
||||
- **APK resource accuracy:** Retain completed tables and decode facts after interrupted or failed reads. Derive locale limits from resource configuration fields, disclose unselectable qualified tables, distinguish document failures from omitted entries, and bind Evidence to the decoded snapshot. ([34a88d6](https://github.com/morluto/rea/commit/34a88d6c1674710953e2c94bd0a17332cd69ca6f), [f5fcf52](https://github.com/morluto/rea/commit/f5fcf5216ffb3a266b8c00f6d9a0943051084ab5), [6385ef4](https://github.com/morluto/rea/commit/6385ef4355d7a794b3154ee38529cbddee583336), [98886ad](https://github.com/morluto/rea/commit/98886adddfcbd7b4f4236b3a9825f91878c3cb11), [dc2e811](https://github.com/morluto/rea/commit/dc2e8117b60c4481372ed89b077e6c1cf81ee653), [#4771](https://github.com/morluto/rea/pull/4771), [morluto/rea#4768](https://github.com/morluto/rea/issues/4768)). Thanks @abhay-2108, @gensokyo-dev, @Iyanuoluwa007, @kaoru0822-kitauji, @N0zoM1z0.
|
||||
- **Apktool guidance:** Retain the observed version and disclose the matching platform/release advisory windows without treating a version string as proof of decoder safety. Missing prerequisites and decode refusals identify the selected APK or launcher. ([d38f099](https://github.com/morluto/rea/commit/d38f099692394ec90c582f9f0a14b036b421b160), [63696d2](https://github.com/morluto/rea/commit/63696d277328f556c971d1fc9f81ae83128d468b), [cc038da](https://github.com/morluto/rea/commit/cc038da218cada68c87700477aba3db4739791ff)). Thanks @GovinduSasanka, @peter-research.
|
||||
- **Android method bodies:** Recover original bytecode bodies when Java source is omitted while preserving genuine native/abstract body absence and primary failures during retryable cleanup. ([63c65a2](https://github.com/morluto/rea/commit/63c65a22430a4bbc0c1807a0298edb867775001f), [742b6f5](https://github.com/morluto/rea/commit/742b6f587f3c4ef1b8212e7376d6f3a305b194dc), [9fa6d2b](https://github.com/morluto/rea/commit/9fa6d2b254cc390665974efb52463386a029e9fc)). Thanks @balkan16, @Chessing234.
|
||||
- **ADB observations and transfers:** Preserve display-scoped focus, distinguish missing declarations from absent focus, retain numeric physical density and mark unknown permission coverage partial. Transfers use consumed snapshot identity and refuse unknown destination existence; uninstall requests explicitly select user zero. ([7086768](https://github.com/morluto/rea/commit/7086768a27242a15daa93486aefbaacc3c891e78), [3d028db](https://github.com/morluto/rea/commit/3d028dbc74a465f6f8df1813a931fbb5362e7dc3), [#4074](https://github.com/morluto/rea/pull/4074), [morluto/rea#3942](https://github.com/morluto/rea/issues/3942), [#4663](https://github.com/morluto/rea/pull/4663), [#4665](https://github.com/morluto/rea/pull/4665), [1342127](https://github.com/morluto/rea/commit/1342127e91d4db8b4113a7a4e85be909bc96e1db), [61e6880](https://github.com/morluto/rea/commit/61e688070f48016b6d178cd04c02fcc2f33e19ad), [be0b2d1](https://github.com/morluto/rea/commit/be0b2d138c46bb951fc3c8ee57ef7b0d57fed312)). Thanks @Copilot, @coygeek, @gongfpp, @Tapraj-Pathak, @yankikucuk.
|
||||
- **Apple metadata:** Preserve exact plist/keyed-archive integers, negative zero, non-finite real identity and dictionary keys. Reject malformed XML before value decoding, retain undecodable dates as unknown and report unsupported OpenStep entries explicitly. ([#4324](https://github.com/morluto/rea/pull/4324), [morluto/rea#4292](https://github.com/morluto/rea/issues/4292), [2cf9c28](https://github.com/morluto/rea/commit/2cf9c28f27261b7e64af2ba7ecb4e23ea7714aee), [#4839](https://github.com/morluto/rea/pull/4839), [morluto/rea#4834](https://github.com/morluto/rea/issues/4834), [f2b7bbb](https://github.com/morluto/rea/commit/f2b7bbb31279a820f4cf32d439570f8e5381d010), [6c98708](https://github.com/morluto/rea/commit/6c98708793c23615de8053b2cd14d9f164b960c5), [78cf610](https://github.com/morluto/rea/commit/78cf6100f33ad2412c0a3d60b83f8a44a43fbbb5), [c7e9e09](https://github.com/morluto/rea/commit/c7e9e096d3e2444e4c251123b8e914e9c0941f4e), [#3793](https://github.com/morluto/rea/pull/3793), [#4105](https://github.com/morluto/rea/pull/4105), [morluto/rea#3892](https://github.com/morluto/rea/issues/3892)). Thanks @coygeek, @kaoru0822-kitauji, @mertkayacs, @pablodiazjorge, @pollychen-lab, @xu91102.
|
||||
- **Apple application structure:** Derive bundle components and launchd domains from their owning directories, retain unavailable components, and select only exact `Assets.car` names as asset catalogs. Interface Builder keeps unresolved classes and counts hierarchy records beyond the returned object page. ([149a0fc](https://github.com/morluto/rea/commit/149a0fc06c3c67ee45624e6da14dfb0754473758), [5d6029e](https://github.com/morluto/rea/commit/5d6029ea62a65ec34fd739ee40223c77c6fdb63b), [c192c58](https://github.com/morluto/rea/commit/c192c58815ddeaf5cf30159e7082c742c0bc36b7), [#4094](https://github.com/morluto/rea/pull/4094), [morluto/rea#3914](https://github.com/morluto/rea/issues/3914), [#4118](https://github.com/morluto/rea/pull/4118), [morluto/rea#3958](https://github.com/morluto/rea/issues/3958), [#4162](https://github.com/morluto/rea/pull/4162), [morluto/rea#3950](https://github.com/morluto/rea/issues/3950)). Thanks @coygeek, @kaoru0822-kitauji.
|
||||
- **Native names and layout:** Keep valid Objective-C records beside malformed neighbors, preserve leading U+FEFF and repeated load-command fields, and select exact arm64e.x1 slices. PE resource ownership and NativeAOT reads stay within declared image/header ranges. ([#3833](https://github.com/morluto/rea/pull/3833), [morluto/rea#3336](https://github.com/morluto/rea/issues/3336), [#4549](https://github.com/morluto/rea/pull/4549), [morluto/rea#4540](https://github.com/morluto/rea/issues/4540), [morluto/rea#4541](https://github.com/morluto/rea/issues/4541), [#4730](https://github.com/morluto/rea/pull/4730), [#4048](https://github.com/morluto/rea/pull/4048), [morluto/rea#4008](https://github.com/morluto/rea/issues/4008), [#3500](https://github.com/morluto/rea/pull/3500), [morluto/rea#3389](https://github.com/morluto/rea/issues/3389), [#4463](https://github.com/morluto/rea/pull/4463), [morluto/rea#4373](https://github.com/morluto/rea/issues/4373), [morluto/rea#4455](https://github.com/morluto/rea/issues/4455), [3fc0075](https://github.com/morluto/rea/commit/3fc007534bb2104d662b43cc712281f9d1d6a7d3), [4a076f3](https://github.com/morluto/rea/commit/4a076f368c62bdfb19415accad342e03a38d1005)). Thanks @coygeek, @Iyanuoluwa007, @jayinvers, @kaoru0822-kitauji, @kevinjosh10, @maxazure, @ShuaixinHuang.
|
||||
- **Native analysis providers:** Keep Ghidra byte matches within examined coverage, retain PE PDB/function-boundary limitations and preserve configured MIPS choices. Attached IDA refuses universal Mach-O rather than silently choosing a slice; request timeout identity and captured output survive adapter handling. ([#4531](https://github.com/morluto/rea/pull/4531), [#2933](https://github.com/morluto/rea/pull/2933), [morluto/rea#2259](https://github.com/morluto/rea/issues/2259), [01a1b3f](https://github.com/morluto/rea/commit/01a1b3fda6a7dacd24a0b10619d41d12153080d4), [50dd9fc](https://github.com/morluto/rea/commit/50dd9fcad1efb47cb404192c30bdf190dcdaca58), [#4765](https://github.com/morluto/rea/pull/4765), [morluto/rea#3871](https://github.com/morluto/rea/issues/3871), [c0cfccb](https://github.com/morluto/rea/commit/c0cfccb45fe43e7eeff18e56c64caa5b121988f9)). Thanks @BeLit-Development-2, @coygeek, @kaoru0822-kitauji, @N0zoM1z0, @ShuaixinHuang.
|
||||
- **Recorded crashes:** Prevent GDB from discovering an executable named by an untrusted core, reject core mapping paths that escape the isolated sysroot, and preserve pwntools worker ownership across launcher exec. Completed decode facts and cancellation reasons remain available through failed final cleanup. ([5bf4fff](https://github.com/morluto/rea/commit/5bf4fffc1b835130ddb16f96db9ce13b639591db), [b33c0df](https://github.com/morluto/rea/commit/b33c0df02e93fabca00a44f2de625b1a63637ee9), [#4435](https://github.com/morluto/rea/pull/4435), [morluto/rea#4011](https://github.com/morluto/rea/issues/4011)). Thanks @coygeek, @N0zoM1z0.
|
||||
- **Cutter and Rizin:** Read Cutter’s active document from core configuration and attribute its host version correctly. Keep interrupted Rizin frames and output whitespace before completion markers, with exact command and cleanup failure attribution. ([#4507](https://github.com/morluto/rea/pull/4507), [morluto/rea#4497](https://github.com/morluto/rea/issues/4497), [#4514](https://github.com/morluto/rea/pull/4514), [morluto/rea#4498](https://github.com/morluto/rea/issues/4498), [#4459](https://github.com/morluto/rea/pull/4459), [morluto/rea#4015](https://github.com/morluto/rea/issues/4015), [d5fc7e6](https://github.com/morluto/rea/commit/d5fc7e6ebf9af050081fc4c6e18b9079cfaa5cdc), [199396f](https://github.com/morluto/rea/commit/199396f61c8f61da326d059606a992304a760147)). Thanks @coygeek, @N0zoM1z0, @Xander-AJ.
|
||||
- **Windows cleanup:** Retain unpublished runtimes after failed removal, handle read-only attributes and sharing conflicts, preserve primary snapshot failures and honor caller environment overrides. ([f78d137](https://github.com/morluto/rea/commit/f78d137b42741a0ed5fb8ac161286f9a50220c4d), [d69acc3](https://github.com/morluto/rea/commit/d69acc3455c33f90a2fc4381db43d38b6af50622), [0679a79](https://github.com/morluto/rea/commit/0679a7972b4f2b02eee6a86a44f50f4c2967d3cb), [5f9ac2c](https://github.com/morluto/rea/commit/5f9ac2c98a6fe11988c41afb737df9b6d5c3a1b3), [1bbf4bd](https://github.com/morluto/rea/commit/1bbf4bd16ca134cdb2187eaf29291cae90d14a57)). Thanks @DeryFerd.
|
||||
- **Managed metadata:** Compare nominal signatures and declaring identities across builds, preserve empty names without display substitutions, verify ABI-prefixed Mach-O exports exactly, and charge cached metadata once. Metadata errors report artifact file offsets; mixed-mode assemblies retain their actual boundary classification. ([#3059](https://github.com/morluto/rea/pull/3059), [#3079](https://github.com/morluto/rea/pull/3079), [#3853](https://github.com/morluto/rea/pull/3853), [morluto/rea#3352](https://github.com/morluto/rea/issues/3352), [#3974](https://github.com/morluto/rea/pull/3974), [morluto/rea#3961](https://github.com/morluto/rea/issues/3961), [#3491](https://github.com/morluto/rea/pull/3491), [morluto/rea#3449](https://github.com/morluto/rea/issues/3449), [#4418](https://github.com/morluto/rea/pull/4418), [morluto/rea#4372](https://github.com/morluto/rea/issues/4372), [#3516](https://github.com/morluto/rea/pull/3516), [morluto/rea#3366](https://github.com/morluto/rea/issues/3366)). Thanks @8rulerstar, @coygeek, @duckhunt113, @Hahaa909, @JayOfTheKeyboard, @kaoru0822-kitauji, @keeponlight, @N0zoM1z0, @tomyrms.
|
||||
- **Archive and filesystem identity:** Reject colliding extraction paths before writing, classify ZIP offsets beyond EOF as malformed, normalize ZIP current-directory prefixes once, and preserve symlink and unreadable-path provenance. A directory named `*.map` is no longer mistaken for a source-map file. ([862a0b2](https://github.com/morluto/rea/commit/862a0b2631785492f8dc20c8acd396646c545fd1), [c209dcb](https://github.com/morluto/rea/commit/c209dcb209118069f123e789949eeba1ef78853e), [6b1c36e](https://github.com/morluto/rea/commit/6b1c36e336eeacbb7159b211e687b4b84f99b9f5), [25c2222](https://github.com/morluto/rea/commit/25c22222abe7a681491bdb4d7316250fdcf32973), [d6e4fe4](https://github.com/morluto/rea/commit/d6e4fe4a4dd8914ec3db50d2a7fc3d0802649262), [#4131](https://github.com/morluto/rea/pull/4131), [morluto/rea#3909](https://github.com/morluto/rea/issues/3909)). Thanks @Adkid-Zephyr, @coygeek, @jackie-cqz, @kaoru0822-kitauji, @maxazure, @XY-code1.
|
||||
- **Evidence and reconstruction:** Authenticate all retained inventory substeps, preserve source-attributed caveats in composed workflows, and require comparable cited Evidence before declaring reconstruction coverage or readiness. Missing Evidence and residual replay uncertainty stay explicit. ([9ac91e0](https://github.com/morluto/rea/commit/9ac91e0c1e13e91cd21341635ad5be89b0e8642d), [c4608a2](https://github.com/morluto/rea/commit/c4608a29620ea3e7bb8259b47357b942fe4e4709), [#4075](https://github.com/morluto/rea/pull/4075), [morluto/rea#3357](https://github.com/morluto/rea/issues/3357), [#3526](https://github.com/morluto/rea/pull/3526), [morluto/rea#3363](https://github.com/morluto/rea/issues/3363), [153017d](https://github.com/morluto/rea/commit/153017d750322188c1a112b2284c1b8065c67320)). Thanks @coygeek, @gensokyo-dev, @kaoru0822-kitauji, @N0zoM1z0, @rudycelekli.
|
||||
- **Offline data:** Preserve exact UTC packet-capture instants, SQLite column identity and snapshot admission failures, and cooked WASM directive literals. Cleanup cancellation retains completed EVM/WASM observations rather than losing them. ([796faa0](https://github.com/morluto/rea/commit/796faa0e4e6bf745bcdc33acef65af8ccfc3622d), [748ac7f](https://github.com/morluto/rea/commit/748ac7f0df340f0d0569eca2e803a59cd51ffdbd), [bc51d0e](https://github.com/morluto/rea/commit/bc51d0ea5df7dcee1fcc42dfae3ab0c62c9cca5e), [4f87f02](https://github.com/morluto/rea/commit/4f87f02bb19bf77349a683a5bcb2da2d1da427d0), [d694aef](https://github.com/morluto/rea/commit/d694aef227e90164e6654ea9e5ce6a854d099404), [321cacb](https://github.com/morluto/rea/commit/321cacb84a020f447e1110df5c58fa71d99b7bf1)). Thanks @himamaheswarreddy, @mertkayacs, @rollroyces, @viniciusdsandrade.
|
||||
- **Kuna recovery:** Share concurrent single-function requests and cache repeats, validate warm exports once, stream string inventories beyond the former fixed buffer ceiling, and keep unproven processes in their run slots until close confirms release. Failure messages distinguish final buffer refusals from configurable deadlines. ([#4045](https://github.com/morluto/rea/pull/4045), [#4169](https://github.com/morluto/rea/pull/4169), [#4049](https://github.com/morluto/rea/pull/4049), [#4318](https://github.com/morluto/rea/pull/4318), [#4052](https://github.com/morluto/rea/pull/4052), [#4098](https://github.com/morluto/rea/pull/4098), [#4343](https://github.com/morluto/rea/pull/4343)). Thanks @kaoru0822-kitauji.
|
||||
- **Flutter snapshots:** Check symbol-table ranges against loadable segments and retain completed inspection facts when cancellation occurs during snapshot cleanup. ([#3859](https://github.com/morluto/rea/pull/3859), [morluto/rea#3346](https://github.com/morluto/rea/issues/3346), [#4766](https://github.com/morluto/rea/pull/4766), [morluto/rea#4717](https://github.com/morluto/rea/issues/4717), [#4762](https://github.com/morluto/rea/pull/4762), [morluto/rea#4640](https://github.com/morluto/rea/issues/4640)). Thanks @coygeek, @kaoru0822-kitauji.
|
||||
- **Native metadata recovery:** Keep unknown Objective-C category classes, empty protocol names, undecoded Swift aliases and unfinished property types explicit instead of discarding neighboring valid records. Preserve unnamed segment identity and exclude a dylib’s own install name from dependencies. ([#4093](https://github.com/morluto/rea/pull/4093), [morluto/rea#4012](https://github.com/morluto/rea/issues/4012), [#4078](https://github.com/morluto/rea/pull/4078), [morluto/rea#3873](https://github.com/morluto/rea/issues/3873), [#4085](https://github.com/morluto/rea/pull/4085), [morluto/rea#3934](https://github.com/morluto/rea/issues/3934), [#3825](https://github.com/morluto/rea/pull/3825), [morluto/rea#3345](https://github.com/morluto/rea/issues/3345), [#4051](https://github.com/morluto/rea/pull/4051), [morluto/rea#3875](https://github.com/morluto/rea/issues/3875), [#4050](https://github.com/morluto/rea/pull/4050), [morluto/rea#3872](https://github.com/morluto/rea/issues/3872)). Thanks @coygeek, @kaoru0822-kitauji.
|
||||
- **Document selection:** Preserve explicitly selected plist paths and bundle defaults, and only use document fallback after confirming its applicability. Finalization keeps completed selection facts and cancellation outcomes. ([e6c4877](https://github.com/morluto/rea/commit/e6c487799bb64f559e66b69be1863e6ec5187548), [#3774](https://github.com/morluto/rea/pull/3774), [9df559b](https://github.com/morluto/rea/commit/9df559b7e712298a761ff26e9caae8d659b7e54b)). Thanks @kaoru0822-kitauji.
|
||||
- **Capture and comparison accuracy:** Admit schema-valid HAR numeric values while retaining exact-publication limits, preserve empty URL path segments, and keep unrelated process traces and unnamed reference endpoints distinct or unknown. ([#3159](https://github.com/morluto/rea/pull/3159), [morluto/rea#3145](https://github.com/morluto/rea/issues/3145), [#3792](https://github.com/morluto/rea/pull/3792), [morluto/rea#3307](https://github.com/morluto/rea/issues/3307), [#4178](https://github.com/morluto/rea/pull/4178), [morluto/rea#3931](https://github.com/morluto/rea/issues/3931), [#3809](https://github.com/morluto/rea/pull/3809), [morluto/rea#3388](https://github.com/morluto/rea/issues/3388)). Thanks @coygeek, @gensokyo-dev, @kaoru0822-kitauji, @N0zoM1z0, @pablodiazjorge, @xu91102.
|
||||
- **Evidence publication:** Recheck staged output identity after its final write, classify missing review Evidence and duplicate ledger entries as caller input, and preserve the selected freshness requirement in comparison parameters. ([#4764](https://github.com/morluto/rea/pull/4764), [morluto/rea#4613](https://github.com/morluto/rea/issues/4613), [#4452](https://github.com/morluto/rea/pull/4452), [morluto/rea#4444](https://github.com/morluto/rea/issues/4444), [#4065](https://github.com/morluto/rea/pull/4065), [morluto/rea#4000](https://github.com/morluto/rea/issues/4000)). Thanks @amyanger, @coygeek, @kaoru0822-kitauji, @Rinkia, @Swatantra-66.
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
- **Saved Evidence:** Evidence and analysis snapshots now require explicit nullable `relationship_coverage` in their identity commitments. Earlier records whose digest omitted null coverage are incompatible even when the serialized field is explicitly null. Artifact inventories require scoped coverage and explicit `integrity_contradictions`; obsolete `coverage_status` and prose-based coverage inference are removed. Regenerate old Evidence/snapshots and inventory observations with this release: saved files are not migrated and omitted facts are not filled with guessed defaults. ([db8128d](https://github.com/morluto/rea/commit/db8128dc0d1c4f731f3d19877fa00d25cf17f657), [0405c9d](https://github.com/morluto/rea/commit/0405c9d361de6d904dfa16cc0b6d8350407f2c9b), [a9588be](https://github.com/morluto/rea/commit/a9588be4ebdd20eeda975cb63ffcdb1cd868b340)).
|
||||
- **Browser capture results:** Consumers must retain resolved capture limits, event families and omission facts. Set desired budgets through the supported capture options; omitted caller inputs still select defaults, but stored observations must report the resolved values. Admitted content reads queue behind bounded concurrency. ([57a23f0](https://github.com/morluto/rea/commit/57a23f0a2da0fdc3835e203718ded8acea5a34e6)).
|
||||
- **Android version codes:** `inspect_adb_package.version_code` is a decimal string rather than a JSON number. Read it as a string, or use `BigInt` for numeric comparison. ([#3485](https://github.com/morluto/rea/pull/3485), [morluto/rea#3368](https://github.com/morluto/rea/issues/3368), [7666a48](https://github.com/morluto/rea/commit/7666a482b69dc7cabdbac3bbaf89a43e32300192)). Thanks @coygeek, @kaoru0822-kitauji.
|
||||
- **Native UI failures:** Failed or cancelled native UI scenarios now return typed errors with the collected report in `partial_observation`. Handle the error path and read that report instead of expecting a successful result containing failed steps. ([d909919](https://github.com/morluto/rea/commit/d9099190ab3a819bd3a9fd62a8714cef398016d5)).
|
||||
- **Provider status:** `binary_session` candidate availability can be unknown during owner finalization. Preserve that state rather than treating it as unavailable; internal consumers must await the asynchronous session status API. ([150e996](https://github.com/morluto/rea/commit/150e996a86f2bec4982ba87f37bf502a8d5e8214)).
|
||||
- **Swift symbol locations:** `swift_symbols` entries may have a null `address` when the provider supplies no location. Handle unknown addresses; optional `evidence.reported_address` retains the original provider value. ([8116dc0](https://github.com/morluto/rea/commit/8116dc06512b4609d8db63cebc9f2ff8cb964151)).
|
||||
- **Linux Hopper admission:** Older builds previously admitted by a fixed allowlist need retained publisher-signed package evidence or an approved update to the current public demo. Update explicit `HOPPER_LAUNCHER_PATH` selections when migrating; the live feed offers only its current release. The official RPM metadata mismatch remains unresolved and is refused. ([#3590](https://github.com/morluto/rea/pull/3590), [morluto/rea#3577](https://github.com/morluto/rea/issues/3577), [70cd4cb](https://github.com/morluto/rea/commit/70cd4cbfb849b61f05d97d2690372a9782831426)). Thanks @gensokyo-dev, @kaoru0822-kitauji, @N0zoM1z0.
|
||||
|
||||
Full contribution record: [571 commits and 236 merged PRs](https://github.com/morluto/rea/blob/rea-agents-6.6.0/docs/releases/6.6.0.contributions.json).
|
||||
|
||||
## [6.5.0](https://github.com/morluto/rea/compare/rea-agents-6.4.0...rea-agents-6.5.0) (2026-10-13)
|
||||
|
||||
Provider prerequisites and recorded verification scope are listed in [the testing guide](docs/testing.md).
|
||||
@@ -2683,7 +2493,7 @@ Read the [6.1 to 6.2 migration guide](docs/migration-6.2.md) before upgrading. R
|
||||
* enforce conventional pull request titles ([3a21f16](https://github.com/morluto/rea/commit/3a21f16c1832430765083636511a8c12dbccf2dd))
|
||||
* verify installed package with Linux Hopper ([4f97f69](https://github.com/morluto/rea/commit/4f97f699e4d5471557d1ce0ff91e144065d06a61))
|
||||
|
||||
## [0.3.0](https://github.com/morluto/rea/compare/rea-0.2.1...rea-agents-0.3.0) (2026-07-12)
|
||||
## [0.3.0](https://github.com/morluto/rea/compare/rea-agents-0.2.1...rea-agents-0.3.0) (2026-07-12)
|
||||
|
||||
|
||||
### Features
|
||||
@@ -2736,7 +2546,7 @@ Read the [6.1 to 6.2 migration guide](docs/migration-6.2.md) before upgrading. R
|
||||
* redesign and localize README ([051b6db](https://github.com/morluto/rea/commit/051b6db65687fd14826b6462cd623feb2504f123))
|
||||
* redesign and localize README ([910eda6](https://github.com/morluto/rea/commit/910eda6f323ba1151b0771fe5de6c60a91e3d46d))
|
||||
|
||||
## [0.2.0](https://github.com/morluto/rea/releases/tag/rea-0.2.0) (2026-07-12)
|
||||
## [0.2.0](https://github.com/morluto/rea/compare/rea-0.1.0...rea-0.2.0) (2026-07-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
+12
-101
@@ -10,45 +10,19 @@ and maintenance cost. Keep cosmetic polish with useful fixes; confusing or
|
||||
incorrect user instructions warrant standalone corrections. Collect other
|
||||
cosmetic-only suggestions in an issue rather than separate PRs.
|
||||
|
||||
For selected architectural owners and dependency paths, use the
|
||||
[architecture map](docs/architecture.mermaid). It is not a complete directory or
|
||||
import inventory. For current provider wiring, start with
|
||||
[`src/composition/binary.ts`](src/composition/binary.ts) for deep candidates and
|
||||
the other `src/composition/` factories for tool services, including
|
||||
[`gpu.ts`](src/composition/gpu.ts) and [`model.ts`](src/composition/model.ts).
|
||||
Live Frida, Cutter, GDB and Rizin construction also uses shared composition
|
||||
factories. For service ownership, availability and catalog declarations, follow
|
||||
the [capability composition guide](docs/capability-composition.md).
|
||||
Production factories construct fresh providers without starting engines or
|
||||
acquiring targets; optional adapter failures must preserve successful peers. Run
|
||||
For capability organization and provider composition, use the
|
||||
[architecture map](docs/architecture.mermaid). Production factories in
|
||||
`src/composition/` construct fresh providers without starting engines or acquiring
|
||||
targets; optional adapter failures must preserve successful peers. Run
|
||||
`npm run verify:test-discovery` after adding or moving tests. Resolved import
|
||||
boundaries are checked by `npm run verify:module-boundaries`, included in lint.
|
||||
|
||||
Before retiring an alias or facade, trace runtime registration, dynamic imports,
|
||||
JavaScript verifiers, subprocess scripts, and packaged entrypoints as well as
|
||||
TypeScript references. Tests may be the last consumers of a superseded internal
|
||||
path; migrate their distinct assertions to the current production path before
|
||||
removing it. Preserve validation, lazy loading, caller policy, and resource
|
||||
acquisition and cleanup supplied by the old boundary. Published APIs, persisted
|
||||
formats, and independently versioned bridges require an explicit compatibility
|
||||
decision; an internal representation alone does not establish that obligation.
|
||||
See [interpreting boundary facts once](docs/tool-design.md#interpret-boundary-facts-once)
|
||||
for admission ownership and the distinction between repeated validation and
|
||||
independent checks.
|
||||
|
||||
For analysis workflows and agent interfaces, follow the
|
||||
[performance priorities](AGENTS.md#performance). Explain the selected output
|
||||
scope, reuse/cache decisions, and relevant before/after measurements when these
|
||||
costs change.
|
||||
Use the [analysis optimization skill](.agents/skills/rea-analysis-optimization/SKILL.md)
|
||||
to locate provider and shared-workflow bottlenecks, preserve Evidence, and
|
||||
compare equivalent real tasks before claiming an improvement.
|
||||
|
||||
When adding or changing an MCP tool, follow the [tool design guide](docs/tool-design.md) and preserve the canonical contracts and generated catalog.
|
||||
When adding or changing an admission ceiling, budget, or deadline, follow
|
||||
[analysis settings, limits, and recovery](docs/design/limits-and-recovery.md)
|
||||
for ownership, operation snapshots, provider integration, and the steps for
|
||||
adding a runtime setting.
|
||||
|
||||
## Reporting issues
|
||||
|
||||
@@ -93,8 +67,7 @@ Before requesting maintainer review, the human owner must:
|
||||
passing. Follow the [verification requirements](#development-feedback-and-pr-verification)
|
||||
for required checks and real-provider coverage.
|
||||
- Complete the **Author readiness** declaration in the PR template, explicitly
|
||||
stating that the PR is ready for initial review, unless the PR author is
|
||||
exempt from the declaration as listed below.
|
||||
stating that the PR is ready for initial review.
|
||||
|
||||
Keep the PR description concise and current. Explain the problem, observable
|
||||
outcome, approach, and useful evidence; link long logs or put them in optional
|
||||
@@ -103,11 +76,7 @@ Human ownership and review remain required when CI or an automated reviewer
|
||||
reports success. Readiness requests maintainer review; it does not grant scope
|
||||
approval or permission to merge.
|
||||
|
||||
Draft status is optional. Draft PRs may request early feedback with open questions
|
||||
and incomplete checks. Agents preserve the current draft status unless asked to
|
||||
change it; missing readiness declarations or later updates do not require a PR
|
||||
to become a draft.
|
||||
|
||||
Draft PRs may request early feedback with open questions and incomplete checks.
|
||||
Leave the readiness declaration unchecked until the human owner has completed
|
||||
it. The declaration records initial readiness; later pushes do not invalidate
|
||||
it or require another human sign-off. Keep the PR description and affected
|
||||
@@ -115,12 +84,9 @@ validation current as work changes. AI agents must leave human declarations to
|
||||
the human owner and must not attest to a human review on their behalf.
|
||||
|
||||
The `PR readiness` check reports pending for drafts and requires a named human
|
||||
owner and all three template declarations for non-drafts. PRs authored by
|
||||
@morluto, @kaoru0822-kitauji, @N0zoM1z0, or @gensokyo-dev are exempt from
|
||||
the declaration; their human ownership, review, and evidence responsibilities
|
||||
above still apply. The check identifies authors by GitHub account ID. It checks
|
||||
that the declaration is filled, not whether the code or evidence is correct.
|
||||
Existing PRs can copy the **Author readiness** section from the current template.
|
||||
owner and all three template declarations for non-drafts. It checks that the
|
||||
declaration is filled, not whether the code or evidence is correct. Existing PRs
|
||||
can copy the **Author readiness** section from the current template.
|
||||
Open PRs must have distinct head commits because GitHub shares commit statuses.
|
||||
|
||||
Maintainers enable merge enforcement after the workflow is merged: run
|
||||
@@ -235,8 +201,6 @@ Use the development toolchain pinned in `.nvmrc` and
|
||||
`package.json#packageManager`. Run `nvm use` before installing dependencies;
|
||||
installed-package runtime support is documented in
|
||||
[installation](docs/installation.md#start-setup).
|
||||
Use Node.js and npm for source builds and verifier scripts; the installed
|
||||
package's Bun runtime support does not extend to those development scripts.
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
@@ -251,15 +215,6 @@ catalogs, documentation projections, or managed verification evidence. Turbo
|
||||
caches deterministic builds and static checks across Git worktrees. After a package, lockfile, or managed-skill version change, run
|
||||
`npm run metadata:generate` before building.
|
||||
|
||||
Dependabot proposes updates for the npm lockfiles, website Python requirements,
|
||||
and GitHub Actions. Review dependency changes and run the checks relevant to
|
||||
their consumers. For dependencies maintained outside Dependabot, including
|
||||
`third_party/` and the HTTP/3 lab's pinned requirements, the maintainer should
|
||||
review upstream security advisories when updating or using them. Preserve source
|
||||
and version provenance, update license notices when needed, and report which
|
||||
verification was actually repeated. The lab requirements describe a recorded
|
||||
environment; a dependency update does not verify the revised environment.
|
||||
|
||||
Repository invariants live in [AGENTS.md](AGENTS.md). Keep provider-specific
|
||||
protocols in adapters and shared CLI/MCP workflows in the application layer.
|
||||
|
||||
@@ -270,10 +225,7 @@ 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;
|
||||
this includes `run-exclusive` commands, which use the native Job Object to keep
|
||||
the artifact lock until every command descendant has exited. Without the
|
||||
artifacts, those controls are unavailable even after a successful runtime
|
||||
build.
|
||||
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,
|
||||
@@ -281,22 +233,6 @@ 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.
|
||||
|
||||
The general source suite (`npm run test:fast`, `npm run test:changed`, and
|
||||
`npm run test:local`) runs on Linux in CI, and Windows CI runs a curated lane.
|
||||
On a Windows checkout, some source tests fail because fixtures use POSIX paths,
|
||||
`#!/bin/sh` tool stubs, or POSIX rename semantics. Compare failures against the
|
||||
base revision, or run the suite on Linux or WSL, before attributing them to a
|
||||
change. The embedded-artifact parent-swap tests exercise the selected-path
|
||||
replacement when the rename succeeds; if Windows refuses it while the admitted
|
||||
file is open, they instead verify the refusal and cleanup. See
|
||||
[testing](docs/testing.md#developer-commands) for command scopes
|
||||
and lane selection.
|
||||
|
||||
`git submodule update --init --recursive` fails at the pinned Unblob submodule
|
||||
on Windows and can leave later submodules unchecked out. Apply the
|
||||
[Unblob checkout workaround](docs/firmware-analysis.md#windows-submodule-checkout),
|
||||
then rerun the command.
|
||||
|
||||
## Documentation website
|
||||
|
||||
The VitePress site uses the Markdown files in `docs/`. Run `npm run docs:dev`
|
||||
@@ -319,23 +255,6 @@ The separate GitHub Pages host uses only the manual
|
||||
**Settings → Pages → Build and deployment → Source** must be set to
|
||||
**GitHub Actions** before deploying there.
|
||||
|
||||
### Translation corrections
|
||||
|
||||
README editions and website editions have separate translation sources:
|
||||
|
||||
- Find the language's `readme` path in the
|
||||
[language inventory](website/locales/languages.json) and edit that file.
|
||||
Locale codes and README suffixes can differ. Use the
|
||||
[English README](README.md) to check commands, requirements, and analysis claims.
|
||||
- Edit `msgstr` values in `website/locales/<language>/messages.po` for website
|
||||
text. Follow the [website translation guide](website/locales/README.md) for
|
||||
the Python environment, catalog validation, and local preview. Preserve source
|
||||
strings, contexts, placeholders, commands, paths, and recorded evidence.
|
||||
|
||||
Before submitting a website translation, run the catalog and website checks in
|
||||
that guide. Translated HTML in `website/_site/` is generated; edit the PO catalog
|
||||
rather than the generated pages.
|
||||
|
||||
## Development feedback and PR verification
|
||||
|
||||
For routine merges, wait for the required checks to pass for the current PR
|
||||
@@ -403,17 +322,9 @@ Real-provider execution is uncached.
|
||||
### Temporary files and diagnostics
|
||||
|
||||
Tests use `createTestTempDirectory` from `tests/fixtures/temporaryDirectory.ts`
|
||||
for exact-path, awaited cleanup, including failures and timeouts. It registers
|
||||
deletion with Vitest's `onTestFinished`; do not also add its result to a roots
|
||||
array or an `afterEach` removal. `createTestWorkspace` from
|
||||
`tests/support/workspace/workspaceFixture.ts` leaves cleanup with its caller,
|
||||
which must await `removeTestWorkspace`. Keep explicit resource teardown before
|
||||
directory deletion, using the case's `finally` or `afterEach`; Vitest runs
|
||||
`afterEach` before `onTestFinished` and awaits finish hooks in reverse
|
||||
registration order.
|
||||
for exact-path, awaited cleanup, including failures and timeouts.
|
||||
`npm run verify:test-temp-hygiene` runs the suite under a fresh `TMPDIR` and
|
||||
rejects remaining REA-owned paths. It cannot detect duplicate deletion owners or
|
||||
prove resource-close ordering. Never glob-clean shared `/tmp/rea-*` content.
|
||||
rejects remaining REA-owned paths. Never glob-clean shared `/tmp/rea-*` content.
|
||||
|
||||
`REA_LOG_LEVEL` selects `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or
|
||||
`silent`. Logs go to stderr: MCP defaults to `info`, and one-shot CLI logging
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div align="center">
|
||||
|
||||
**English** · [简体中文](docs/readme/README_zh.md) · [繁體中文](docs/readme/README_zh-TW.md) · [日本語](docs/readme/README_ja.md) · [한국어](docs/readme/README_ko.md) · [Türkçe](docs/readme/README_tr.md) · [Русский](docs/readme/README_ru.md) · [Tiếng Việt](docs/readme/README_vi.md) · [ไทย](docs/readme/README_th.md) · [Deutsch](docs/readme/README_de.md) · [Español](docs/readme/README_es.md) · [Français](docs/readme/README_fr.md) · [Українська](docs/readme/README_uk.md) · [Polski](docs/readme/README_pl.md) · [Português (Brasil)](docs/readme/README_pt-BR.md) · [العربية](docs/readme/README_ar.md) · [فارسی](docs/readme/README_fa.md) · [Bahasa Indonesia](docs/readme/README_id.md) · [Italiano](docs/readme/README_it.md)
|
||||
· [Ελληνικά](docs/readme/README_el.md) · [മലയാളം](docs/readme/README_ml.md) · [Roman Urdu](docs/readme/README_ur-Latn.md) · [Română](docs/readme/README_ro.md)
|
||||
· [Ελληνικά](docs/readme/README_el.md)
|
||||
|
||||
# REA: Reverse Engineer Anything
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
|
||||
**[Website](https://rea.tools/) · [Guides](https://rea.tools/guides/) · [Showcases](https://rea.tools/showcase/)**
|
||||
|
||||
<a href="https://discord.com/invite/GkcryMnJDM"><img alt="Join the REA community on Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
|
||||
|
||||
[Quick start](#quick-start) · [How REA works](#how-rea-works) · [What you can analyze](#what-you-can-analyze) · [Showcases](#showcases) · [Build RL environments](#build-rl-environments) · [FAQ](#faq) · [Documentation](#documentation)
|
||||
[Quick start](#quick-start) · [How REA works](#how-rea-works) · [What you can analyze](#what-you-can-analyze) · [Showcases](#showcases) · [FAQ](#faq) · [Documentation](#documentation)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -64,7 +62,7 @@ See a feature in an app that you want in your own product? Ask your agent to inv
|
||||
|
||||
REA connects your agent to tools for inspecting native binaries, JavaScript and Electron apps, .NET assemblies, and websites. You can also use the same tools from your terminal. Analysis runs locally, and results include the evidence and limitations behind each conclusion.
|
||||
|
||||
Setup registers REA with your agent and installs matching workflow instructions. Native analysis can use an existing Hopper, Ghidra, IDA or Kuna installation; setup can optionally install Hopper with approval. Static JavaScript analysis needs no native analysis engine.
|
||||
Setup registers REA with your agent and installs matching workflow instructions. Native analysis can use an existing Hopper, Ghidra, or IDA installation; setup can optionally install Hopper with approval. Static JavaScript analysis needs no native analysis engine.
|
||||
|
||||
> **[Visit the REA website](https://rea.tools/)** for setup instructions, illustrated guides, and real case studies.
|
||||
|
||||
@@ -85,27 +83,12 @@ npx rea-agents setup
|
||||
|
||||
Choose your agents, review the proposed changes, and approve them. Setup adds
|
||||
REA's MCP server and matching workflow instructions, with backups of existing
|
||||
configuration. Restart your agent afterward. Setup records the selected runtime and REA
|
||||
entry script directly. If acquired through a package runner, keep its cache entry
|
||||
available. Run package acquisition from a trusted directory outside the target
|
||||
project. Installation and recovery details are in the setup guide below.
|
||||
configuration. Restart your agent afterward.
|
||||
|
||||
Setup supports Claude Code, Codex, Cursor, Gemini CLI, Grok Build and
|
||||
[other agents](docs/installation.md#supported-agents). See
|
||||
[installation and setup](docs/installation.md) for provider configuration and
|
||||
manual MCP registration and Claude Code/Codex plugin installation.
|
||||
|
||||
MCP uses compact input schemas by default to reduce context overhead. Set
|
||||
`REA_MCP_INPUT_SCHEMA_PROFILE=full` in your MCP registration when you need the
|
||||
complete advertised field guidance and nested validation details. Both profiles
|
||||
keep the full tool set and canonical server validation. See
|
||||
"Compact input schema profile" in the MCP contracts guide.
|
||||
|
||||
Complete output schemas are advertised by default. If your client caps the
|
||||
complete `tools/list` frame, set `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` to
|
||||
bound each advertised output schema, or `off` to omit output schemas entirely.
|
||||
Both settings keep canonical server-side validation and complete structured
|
||||
results. See "Output schemas" in the MCP contracts guide.
|
||||
manual MCP registration.
|
||||
|
||||
### Ask your agent
|
||||
|
||||
@@ -149,14 +132,6 @@ For a CLI installed with `npm install --global` or `bun add --global`:
|
||||
rea update
|
||||
```
|
||||
|
||||
`rea --update` is an alias for this verified workflow and accepts the same global
|
||||
output flags. Use `rea update` when combining it with other command controls.
|
||||
|
||||
Updater commands retain at most 4 MiB of stdout and stderr combined. Oversized
|
||||
metadata replies are rejected after the command exits; verbose installs continue
|
||||
with bounded diagnostics. Human progress output respects terminal backpressure
|
||||
and waits for submitted writes to finish.
|
||||
|
||||
Bun global updates require Bun 1.2.17 or later. With an older Bun release,
|
||||
upgrade Bun manually before running `rea update`.
|
||||
|
||||
@@ -188,47 +163,34 @@ CLI commands use the same workflows.
|
||||
|
||||
## What you can analyze
|
||||
|
||||
REA requires Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+, plus npm.
|
||||
REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+, plus npm.
|
||||
Additional tools and host support depend on the target:
|
||||
|
||||
| Target | What REA returns | Requirements and guide |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Desktop installations | Runtime candidates, supporting files, component markers and next probes | Built-in bounded directory/header inspection; [desktop applications](docs/desktop-applications.md) |
|
||||
| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra, IDA or Kuna; [native analysis](docs/native-investigation.md) |
|
||||
| Offline ELF layout | Sections, segments, original symbols/relocations and static mitigation candidates | Caller-supplied pwntools on Linux x64; [binary diagnostics](docs/binary-diagnostics.md) |
|
||||
| Go binaries | Embedded compiler version, modules, replacements and build settings | Built-in static ELF/PE/thin Mach-O inspection; no Go installation required |
|
||||
| GPU code objects | CUDA/HIP targets, selected kernel parameters and resources, retained PTX text | Built-in static ELF/PE/fatbin/CUBIN/PTX inspection; [GPU code objects](docs/gpu-code-objects.md) |
|
||||
| Model artifacts | GGUF / safetensors metadata, tensor declarations and derived totals | Built-in header inspection; optional full-file hash; [model files](docs/model-artifacts.md) |
|
||||
| IL2CPP metadata | Unity global-metadata.dat assembly images, type and method names and index ranges | Built-in static decoding of versions 24, 27, 29, 31 and 39; no Unity required |
|
||||
| Godot PCK | Archive inventory and extraction, original header/directory facts and selected content identities | Built-in static formats 2, 3 and 4; no Godot required |
|
||||
| Rust binaries | Embedded cargo-auditable crate names, versions, sources and dependency edges | Built-in static ELF/PE/thin Mach-O inspection; [Rust binaries](docs/rust-binaries.md) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](docs/pe-certificates.md) |
|
||||
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](docs/pe-imports.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](docs/elf-notes.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and documented crash classes | Built-in static JSON inspection; [Apple IPS](docs/apple-ips.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](docs/kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, imports, lambdas and the Code header | Built-in static BEAM inspection; [Erlang BEAM](docs/erlang-beam.md) |
|
||||
| WASM artifact | Validation, sections, exact import/export WAT forms and decoded WAT | Caller-supplied WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier on Linux x64; [offline EVM guide](docs/evm-bytecode.md) |
|
||||
| SQLite databases | Tables, columns, indexes, views, triggers and selected ordinary-table rows | Local database/WAL snapshot; Node.js 24.x >=24.15 or 26+ with native SQLite limits |
|
||||
| Recorded Linux crashes | Raw notes, every recorded thread's registers/signals and optional mapping candidates | Caller-supplied pwntools on Linux x64; optional GDB/pwndbg; [recorded crashes](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Modules, imports, source maps, routes, IPC and native add-on relationships | Node.js and npm; Linux: procps-compatible `ps`; [application analysis](docs/javascript-application-workflows.md) |
|
||||
| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](docs/browser-observation.md) |
|
||||
| Saved network captures | Requests, responses, exposed payloads and source locations | HAR or Fiddler Everywhere SAZ; mitmdump on Linux for native mitmproxy; [capture guide](docs/web-network-captures.md) |
|
||||
| .NET assemblies | Metadata, CIL instructions, declared native dependencies and build comparisons | None (static only); [managed-code guide](docs/managed-code-analysis.md) |
|
||||
| Android APKs | Manifest declarations, classes, decompiled methods and references | Headless JADX and a full JDK on Linux/macOS/Windows x64; [Android guide](docs/android-analysis.md) |
|
||||
| APK signatures | v1/v2/v3/v3.1 signers, certificate fingerprints, key rotation and digest self-consistency | None (static only); `inspect_apk_signature` |
|
||||
| Android devices | Devices, packages, processes, logs, dumps, screen captures, and APK/file transfers | Caller-supplied adb (emulator or hardware); [ADB guide](docs/adb-device-analysis.md) |
|
||||
| JEB projects | Engine-backed project units, artifact digests and type/method pseudo-code | A caller-started JEB client serving MCP, selected with REA_JEB_MCP_URL; [JEB guide](docs/jeb-analysis.md) |
|
||||
| Android resources | Decoded manifest, string tables, locales and version facts | Caller-supplied Apktool launcher; [resource guide](docs/apktool-resource-analysis.md) |
|
||||
| Flutter apps | Dart snapshot hash, per-ABI library digests and engine build provenance | None (static APK parsing); [Flutter guide](docs/flutter-analysis.md) |
|
||||
| Firmware | Regions, extraction results and native-analysis handoffs | Binwalk / Unblob on Linux; [firmware guide](docs/firmware-analysis.md) |
|
||||
| Recorded packets | Selected fields, repeated values, filters and decode-as rules | Linux: TShark 4.x + prlimit; [capture guide](docs/packet-captures.md) |
|
||||
| Packet summaries | Endpoints, DNS, TLS SNI, protocol distribution and HTTP requests | Caller-supplied tshark and capinfos (Linux-verified); [pcap guide](docs/pcap-analysis.md) |
|
||||
| Recorded logic signals | Decoder annotations, sample ranges and diagnostics | Linux: sigrok-cli 0.7.2 + prlimit; [signal recordings](docs/signal-recordings.md) |
|
||||
| Packages and resources | File inventories, digests, plists, Apple bundle anatomy and extracted resources | [Artifact and JavaScript guide](docs/javascript-artifact-reconstruction.md), [Apple applications](docs/apple-application-analysis.md) |
|
||||
| Process behavior | Terminal output, interactions, exit and filesystem observations, and run comparisons | Linux/macOS with a native PTY; [process capture](docs/process-capture.md) |
|
||||
| PyInstaller executables | CArchive members, Python version, entry scripts and extracted bytecode | None (static only); [Python packaged applications](docs/python-packaged-applications.md) |
|
||||
| Target | What REA returns | Requirements and guide |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](docs/native-investigation.md) |
|
||||
| Offline ELF layout | Sections, segments, original symbols/relocations and static mitigation candidates | Caller-supplied pwntools on Linux x64; [binary diagnostics](docs/binary-diagnostics.md) |
|
||||
| Go binaries | Embedded compiler version, modules, replacements and build settings | Built-in static ELF/PE/thin Mach-O inspection; no Go installation required |
|
||||
| Rust binaries | Embedded cargo-auditable crate names, versions, sources and dependency edges | Built-in static ELF/PE/thin Mach-O inspection; [Rust binaries](docs/rust-binaries.md) |
|
||||
| WASM artifact | Validation, sections, exact import/export WAT forms and decoded WAT | Caller-supplied WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier on Linux x64; [offline EVM guide](docs/evm-bytecode.md) |
|
||||
| SQLite databases | Tables, columns, indexes, views, triggers and selected ordinary-table rows | Local database/WAL snapshot; Node.js 24.x >=24.15 or 26+ with native SQLite limits |
|
||||
| Recorded Linux crashes | Raw notes, every recorded thread's registers/signals and optional mapping candidates | Caller-supplied pwntools on Linux x64; optional GDB/pwndbg; [recorded crashes](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Modules, imports, source maps, routes, IPC and native add-on relationships | Node.js and npm; Linux: procps-compatible `ps`; [application analysis](docs/javascript-application-workflows.md) |
|
||||
| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](docs/browser-observation.md) |
|
||||
| Saved network captures | Requests, responses, exposed payloads and source locations | HAR or Fiddler Everywhere SAZ; mitmdump on Linux for native mitmproxy; [capture guide](docs/web-network-captures.md) |
|
||||
| .NET assemblies | Metadata, CIL instructions, declared native dependencies and build comparisons | None (static only); [managed-code guide](docs/managed-code-analysis.md) |
|
||||
| Android APKs | Manifest declarations, classes, decompiled methods and references | Headless JADX and a full JDK on Linux/macOS/Windows x64; [Android guide](docs/android-analysis.md) |
|
||||
| Android devices | Devices, packages, processes, logs, dumps, screen captures, and APK/file transfers | Caller-supplied adb (emulator or hardware); [ADB guide](docs/adb-device-analysis.md) |
|
||||
| JEB projects | Engine-backed project units, artifact digests and type/method pseudo-code | A caller-started JEB client serving MCP, selected with REA_JEB_MCP_URL; [JEB guide](docs/jeb-analysis.md) |
|
||||
| Android resources | Decoded manifest, string tables, locales and version facts | Caller-supplied Apktool launcher; [resource guide](docs/apktool-resource-analysis.md) |
|
||||
| Flutter apps | Dart snapshot hash, per-ABI library digests and engine build provenance | None (static APK parsing); [Flutter guide](docs/flutter-analysis.md) |
|
||||
| Firmware | Regions, extraction results and native-analysis handoffs | Binwalk / Unblob on Linux; [firmware guide](docs/firmware-analysis.md) |
|
||||
| Recorded packets | Selected fields, repeated values, filters and decode-as rules | Linux: TShark 4.x + prlimit; [capture guide](docs/packet-captures.md) |
|
||||
| Packet summaries | Endpoints, DNS, TLS SNI, protocol distribution and HTTP requests | Caller-supplied tshark and capinfos (Linux-verified); [pcap guide](docs/pcap-analysis.md) |
|
||||
| Recorded logic signals | Decoder annotations, sample ranges and diagnostics | Linux: sigrok-cli 0.7.2 + prlimit; [signal recordings](docs/signal-recordings.md) |
|
||||
| Packages and resources | File inventories, digests, plists, Apple bundle anatomy and extracted resources | [Artifact and JavaScript guide](docs/javascript-artifact-reconstruction.md), [Apple applications](docs/apple-application-analysis.md) |
|
||||
| Process behavior | Terminal output, interactions, exit and filesystem observations, and run comparisons | Linux/macOS with a native PTY; [process capture](docs/process-capture.md) |
|
||||
|
||||
Static JavaScript and .NET inspection read the supplied files without running
|
||||
the application. Runtime capture runs or interacts with the selected target
|
||||
@@ -255,12 +217,7 @@ for features added since the latest npm release.
|
||||
|
||||
## Showcases
|
||||
|
||||
<a href="https://rea.tools/showcase/">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/rea-showcases-dark.svg" />
|
||||
<img alt="Illustrations of the DX-Ball sound-pan, Notion clipboard-bridge, and TH04 bullet-ring showcases" src="docs/assets/rea-showcases.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
[](https://rea.tools/showcase/)
|
||||
|
||||
### DX-Ball: reconstruct a sound-pan calculation
|
||||
|
||||
@@ -287,53 +244,11 @@ historical compiler output.
|
||||
[Read the case study](https://rea.tools/showcase/th04/) ·
|
||||
[Reconstruction repository](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
### Build RL environments with REA
|
||||
|
||||
Record a web app session, turn its API into tools, and export an environment an AI agent can use. Run it with AgentEnv, then add tasks and reward checks for RL training or evaluation.
|
||||
|
||||
[Read the guide](https://rea.tools/guides/rl-environments/) ·
|
||||
[Watch the video](https://x.com/reatools/status/2112574822584918075?s=20)
|
||||
|
||||
If you've used REA on something interesting, we'd love to see it. Share your
|
||||
case in an [issue](https://github.com/morluto/rea/issues) or a
|
||||
[pull request](https://github.com/morluto/rea/pulls), including the target,
|
||||
your question, how REA helped, and what you found.
|
||||
|
||||
## Build RL environments
|
||||
|
||||
Give an AI agent a local environment to practice tasks such as finding an issue or creating a folder, without running the original app. Use it for reinforcement learning (RL) or to test how well an agent completes tasks.
|
||||
|
||||
REA turns recorded API requests and responses into tools the agent can call. The environment uses the recorded data and supports changes only when the recording confirms their effects.
|
||||
|
||||
With REA set up in your agent, replace the example URL and folder tasks below with your own:
|
||||
|
||||
```text
|
||||
Use REA to build an RL environment from my test web app at
|
||||
http://127.0.0.1:3000.
|
||||
Cover listing, creating, renaming, and deleting folders. You may create and
|
||||
remove test folders. Leave existing data unchanged.
|
||||
Save it in a folder named my-env. Check which operations work and tell me
|
||||
how to run it.
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Use the command line instead</summary>
|
||||
|
||||
Use these commands if you already have a file containing the app’s recorded browser requests and responses. Running the result also requires AgentEnv and Docker.
|
||||
|
||||
```sh
|
||||
rea environment-build ./session.har ./my-env --name my-env --json
|
||||
agent-env run ./my-env
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
REA exports a runnable environment with a basic test to check that it starts and responds. Use it with your RL framework to define task goals, add reward checks, and run training.
|
||||
|
||||
[Build an environment](https://rea.tools/guides/rl-environments/) ·
|
||||
[Environment reference](docs/agent-environments.md) ·
|
||||
[Watch the demo on X](https://x.com/reatools/status/2112574822584918075?s=20)
|
||||
|
||||
## FAQ
|
||||
|
||||
<details>
|
||||
@@ -346,13 +261,11 @@ Any agent that supports local MCP servers. Setup configures the
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Do I need a native analysis engine?</strong></summary>
|
||||
<summary><strong>Do I need Hopper, Ghidra or IDA?</strong></summary>
|
||||
|
||||
Deep native analysis uses Hopper, Ghidra, IDA or Kuna, depending on the target
|
||||
and operation. Static JavaScript and .NET inspection work without a native
|
||||
analysis engine. Setup can install Hopper after approval; Ghidra, IDA and Kuna
|
||||
use your existing installations. See [provider setup](docs/installation.md#hopper)
|
||||
for the options and their supported targets.
|
||||
Deep native analysis uses one of them. Static JavaScript and .NET inspection
|
||||
work without a native analysis engine. Setup can install Hopper after approval;
|
||||
Ghidra and IDA use your existing installations. See [provider setup](docs/installation.md#hopper).
|
||||
|
||||
</details>
|
||||
|
||||
@@ -432,7 +345,7 @@ Report vulnerabilities through [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Star history
|
||||
|
||||
🎉 **400,000 GitHub stars — thank you!**
|
||||
🎉 **250,000 GitHub stars — thank you!**
|
||||
|
||||
Thanks to everyone using REA, reporting bugs, requesting features, testing builds, and contributing fixes.
|
||||
|
||||
@@ -462,7 +375,7 @@ to improve the code or docs.
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and checks,
|
||||
[testing](docs/testing.md) for verification lanes, and the
|
||||
[architecture map](docs/architecture.mermaid) for selected layers and dependency paths.
|
||||
[architecture map](docs/architecture.mermaid) for the project structure.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ Do not disclose vulnerabilities, proof-of-concept exploits, credentials,
|
||||
private binaries, Hopper documents, or private Ghidra projects in a public
|
||||
issue.
|
||||
|
||||
Use **Report a vulnerability** in the repository's **Security** tab as the preferred way to submit the report privately. If GitHub private reporting is unavailable to you, email [[email protected]](mailto:[email protected]). Do not open a public issue to request a private contact channel.
|
||||
Use **Report a vulnerability** in the repository's **Security** tab to submit the report privately. Do not open a public issue to request a private contact channel.
|
||||
|
||||
Include the affected REA version, operating system/distribution, selected
|
||||
provider and version, impact, reproduction conditions, and any suggested
|
||||
|
||||
+21
-23
@@ -16,8 +16,6 @@ notices supplied with the software you actually obtain or redistribute.
|
||||
| Material | Use and licensing records |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| npm dependencies | [Complete inventory](licenses/npm-dependencies.txt) of every entry in `package-lock.json`, including transitive, optional platform, development, and website dependencies. [Runtime notices](licenses/npm-runtime-notices.txt) preserve the copyright, license, and NOTICE documents from integrity-verified release archives, plus the MIT notice for Rolldown's generated runtime helpers. Development dependencies are inventoried but are not all distributed in REA's npm package. |
|
||||
| pefile ordinal tables | Import-hash ordinal names are derived from [pefile 2024.8.26](https://github.com/erocarrera/pefile/tree/4b3b1e2e568a88d4f1897d694d684f23d9e270c4), copyright 2004–2024 Ero Carrera. Preserve the [MIT license](licenses/pefile-2024.8.26-LICENSE.txt). The Python package is not a runtime dependency. |
|
||||
| UF2 family names | UF2 family IDs and short names are copied from [microsoft/uf2 `utils/uf2families.json`](https://github.com/microsoft/uf2/blob/cf3bd7f9c274b949528f8cd6a19eef2502a687d2/utils/uf2families.json), copyright Microsoft Corporation. Preserve the [MIT license](licenses/microsoft-uf2-MIT.txt). |
|
||||
| NativeAOT parser | REA's derived parser uses the MIT-licensed [ghidra-nativeaot](https://github.com/Washi1337/ghidra-nativeaot/tree/effeb734fc570c32650f88b159608979dc7b423e) work of Washi and the NativeAOT layout definitions in [dotnet/runtime](https://github.com/dotnet/runtime/tree/a3b2d40328be0be3f8dd43f0e5cc925b8827b044). Preserve [Washi's MIT notice](licenses/nativeaot-parser-MIT.txt) and the [.NET Foundation MIT notice](licenses/dotnet-runtime-MIT.txt). |
|
||||
| EVM analysis | `[email protected]`, MIT, copyright 2023 Maxim Andreev. The published npm archive omits its license document; REA retains the [original upstream license](third_party/evmole/LICENSE) and [provenance](third_party/evmole/README.md). It is also included in the runtime notices. |
|
||||
| Windows native addon | The release addon is compiled with MinGW-w64 and statically links GCC runtime libraries. Preserve the [MinGW-w64 runtime notices](licenses/MinGW-w64-runtime.txt), [GPLv3 text](licenses/GPL-3.0.txt), and [GCC Runtime Library Exception 3.1](licenses/GCC-Runtime-Library-Exception-3.1.txt). The exception governs eligible compiled combinations; it does not relicense the runtime itself under MIT. The Node-API build headers are from Node 22.19.0; its [original license and component notices](licenses/Node-22.19.0-LICENSE.txt) are retained. Node itself is supplied by the user. Snapshot provenance and release-toolchain review requirements are in [licenses/README.md](licenses/README.md). |
|
||||
@@ -54,27 +52,27 @@ REA's adapters do not grant, replace, or waive these licenses. The upstream
|
||||
executables and their dependency graphs are supplied separately. Git submodules
|
||||
pin inspection sources; they are not included in the npm package.
|
||||
|
||||
| Project | License and source | REA relationship |
|
||||
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Ghidra | [Apache-2.0](https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/LICENSE), with the government-work and component qualifications in its [NOTICE](https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/NOTICE) | Bring your own Ghidra; see [supported versions](docs/installation.md#ghidra). Copies of the 12.1.4 [LICENSE](licenses/ghidra-12.1.4-LICENSE.txt), [NOTICE](licenses/ghidra-12.1.4-NOTICE.txt), and [DISCLAIMER](licenses/ghidra-12.1.4-DISCLAIMER.txt) are retained for reference. |
|
||||
| ghidra-nativeaot | [MIT](https://github.com/Washi1337/ghidra-nativeaot/blob/effeb734fc570c32650f88b159608979dc7b423e/LICENSE.md) | Pinned inspection source; derived parser attribution above. |
|
||||
| JADX | [Apache-2.0](https://github.com/skylot/jadx/blob/v1.5.6/LICENSE) | The audited jadx-headless-mcp source uses JADX 1.5.6 for Android decompilation; the engine JAR is separately supplied. |
|
||||
| jadx-headless-mcp | [Apache-2.0](https://github.com/1013503897/jadx-headless-mcp/blob/5844800a486d2248fa949b74c9896285f5b54de2/LICENSE) | v0.7.1; pinned submodule and user-supplied JAR. |
|
||||
| Kuna | [Apache-2.0](https://github.com/Noelo-Lab/kuna/blob/v1.990/LICENSE), with its [NOTICE](https://github.com/Noelo-Lab/kuna/blob/v1.990/NOTICE) | Bring your own `kuna`; REA verified 1.990 and never downloads, installs or bundles it. REA invokes it as a separate process. |
|
||||
| Apktool | [Apache-2.0](https://github.com/iBotPeaches/Apktool/blob/master/LICENSE.md) | User-supplied Android resource decoder. |
|
||||
| Binwalk | [MIT](https://github.com/ReFirmLabs/binwalk/blob/4fdab3d464d97b68e0af9088df3f9e2e1545b21c/LICENSE) | v3.1.0; pinned submodule and user-supplied firmware engine. |
|
||||
| Unblob | [MIT](https://github.com/onekey-sec/unblob/blob/1fcc7a0a584a70a96c31f5a276c20944d199a089/LICENSE) | 26.6.4; pinned submodule and user-supplied firmware engine. |
|
||||
| Wakaru | [Apache-2.0](https://github.com/pionxzh/wakaru/blob/8219bf5016a063d7fb215101a8c45d5959ec733f/LICENSE) | v1.14.0; pinned source and separately obtained JavaScript reconstruction tooling. |
|
||||
| WABT | [Apache-2.0](https://github.com/WebAssembly/wabt/blob/ff0ef7e0009402740c805a9744c09b05be063e48/LICENSE) | v1.0.42; external WebAssembly tools; [integration provenance](third_party/wabt/README.md). |
|
||||
| pwntools | [MIT, with GPL/BSD component qualifications](https://github.com/Gallopsled/pwntools/blob/3ad86ec5adab1abf9781ca7552a6f5d7cff7a33c/LICENSE-pwntools.txt) | 4.15.0; caller-supplied offline ELF analysis package. |
|
||||
| pyelftools | [Public-domain dedication and component notices](https://github.com/eliben/pyelftools/blob/v0.33/LICENSE) | 0.33; includes separately licensed bundled material, including Construct. |
|
||||
| Unicorn | [GPL-2.0 and component notices](https://github.com/unicorn-engine/unicorn/blob/2.1.2/COPYING) | 2.1.2; used by the caller-supplied ELF analysis profile for instruction emulation. |
|
||||
| pwndbg | [MIT](third_party/pwndbg/LICENSE.md) | 2026.09.15, commit `55596aa676a2756efb8d67034f31675170eabe48`; separately obtained debugger extension. |
|
||||
| GNU GDB | [GPL-3.0-or-later](https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=gdb/COPYING) | Caller-supplied debugger for the optional pwndbg core-file workflow. |
|
||||
| LLDB / LLVM | [Apache-2.0 with LLVM exceptions](https://github.com/llvm/llvm-project/blob/main/LICENSE.TXT) | Caller-supplied debugger; the installed distribution's component and historical notices also apply. |
|
||||
| mitmproxy | [MIT](https://github.com/mitmproxy/mitmproxy/blob/6c09d56e4c29a92f5ad01b03199977584b8ea14f/LICENSE) | 12.2.3; separately supplied network capture proxy. |
|
||||
| Android Debug Bridge (adb) | [Apache-2.0](https://android.googlesource.com/platform/packages/modules/adb/+/refs/heads/main/NOTICE) | User-supplied Android device tool. A complete Android SDK download has its own [SDK license terms](https://developer.android.com/studio/terms); adb's source license does not replace them. |
|
||||
| ILSpy / ICSharpCode.Decompiler | [MIT](https://github.com/icsharpcode/ILSpy/blob/master/ICSharpCode.Decompiler/Properties/AssemblyInfo.cs) | Optional separately installed .NET decompiler; not a redistributed REA dependency. |
|
||||
| Project | License and source | REA relationship |
|
||||
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Ghidra | [Apache-2.0](https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/LICENSE), with the government-work and component qualifications in its [NOTICE](https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/NOTICE) | Bring your own Ghidra 12.1.4. Copies of its [LICENSE](licenses/ghidra-12.1.4-LICENSE.txt), [NOTICE](licenses/ghidra-12.1.4-NOTICE.txt), and [DISCLAIMER](licenses/ghidra-12.1.4-DISCLAIMER.txt) are retained for reference. |
|
||||
| ghidra-nativeaot | [MIT](https://github.com/Washi1337/ghidra-nativeaot/blob/effeb734fc570c32650f88b159608979dc7b423e/LICENSE.md) | Pinned inspection source; derived parser attribution above. |
|
||||
| JADX | [Apache-2.0](https://github.com/skylot/jadx/blob/v1.5.6/LICENSE) | The audited jadx-headless-mcp source uses JADX 1.5.6 for Android decompilation; the engine JAR is separately supplied. |
|
||||
| jadx-headless-mcp | [Apache-2.0](https://github.com/1013503897/jadx-headless-mcp/blob/5844800a486d2248fa949b74c9896285f5b54de2/LICENSE) | v0.7.1; pinned submodule and user-supplied JAR. |
|
||||
| Kuna | [Apache-2.0](https://github.com/Noelo-Lab/kuna/blob/v1.833/LICENSE), with its [NOTICE](https://github.com/Noelo-Lab/kuna/blob/v1.833/NOTICE) | Bring your own `kuna`; REA verified 1.833 and never downloads, installs or bundles it. REA invokes it as a separate process. |
|
||||
| Apktool | [Apache-2.0](https://github.com/iBotPeaches/Apktool/blob/master/LICENSE.md) | User-supplied Android resource decoder. |
|
||||
| Binwalk | [MIT](https://github.com/ReFirmLabs/binwalk/blob/4fdab3d464d97b68e0af9088df3f9e2e1545b21c/LICENSE) | v3.1.0; pinned submodule and user-supplied firmware engine. |
|
||||
| Unblob | [MIT](https://github.com/onekey-sec/unblob/blob/1fcc7a0a584a70a96c31f5a276c20944d199a089/LICENSE) | 26.6.4; pinned submodule and user-supplied firmware engine. |
|
||||
| Wakaru | [Apache-2.0](https://github.com/pionxzh/wakaru/blob/8219bf5016a063d7fb215101a8c45d5959ec733f/LICENSE) | v1.14.0; pinned source and separately obtained JavaScript reconstruction tooling. |
|
||||
| WABT | [Apache-2.0](https://github.com/WebAssembly/wabt/blob/ff0ef7e0009402740c805a9744c09b05be063e48/LICENSE) | v1.0.42; external WebAssembly tools; [integration provenance](third_party/wabt/README.md). |
|
||||
| pwntools | [MIT, with GPL/BSD component qualifications](https://github.com/Gallopsled/pwntools/blob/3ad86ec5adab1abf9781ca7552a6f5d7cff7a33c/LICENSE-pwntools.txt) | 4.15.0; caller-supplied offline ELF analysis package. |
|
||||
| pyelftools | [Public-domain dedication and component notices](https://github.com/eliben/pyelftools/blob/v0.33/LICENSE) | 0.33; includes separately licensed bundled material, including Construct. |
|
||||
| Unicorn | [GPL-2.0 and component notices](https://github.com/unicorn-engine/unicorn/blob/2.1.2/COPYING) | 2.1.2; used by the caller-supplied ELF analysis profile for instruction emulation. |
|
||||
| pwndbg | [MIT](third_party/pwndbg/LICENSE.md) | 2026.09.15, commit `55596aa676a2756efb8d67034f31675170eabe48`; separately obtained debugger extension. |
|
||||
| GNU GDB | [GPL-3.0-or-later](https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=gdb/COPYING) | Caller-supplied debugger for the optional pwndbg core-file workflow. |
|
||||
| LLDB / LLVM | [Apache-2.0 with LLVM exceptions](https://github.com/llvm/llvm-project/blob/main/LICENSE.TXT) | Caller-supplied debugger; the installed distribution's component and historical notices also apply. |
|
||||
| mitmproxy | [MIT](https://github.com/mitmproxy/mitmproxy/blob/6c09d56e4c29a92f5ad01b03199977584b8ea14f/LICENSE) | 12.2.3; separately supplied network capture proxy. |
|
||||
| Android Debug Bridge (adb) | [Apache-2.0](https://android.googlesource.com/platform/packages/modules/adb/+/refs/heads/main/NOTICE) | User-supplied Android device tool. A complete Android SDK download has its own [SDK license terms](https://developer.android.com/studio/terms); adb's source license does not replace them. |
|
||||
| ILSpy / ICSharpCode.Decompiler | [MIT](https://github.com/icsharpcode/ILSpy/blob/master/ICSharpCode.Decompiler/Properties/AssemblyInfo.cs) | Optional separately installed .NET decompiler; not a redistributed REA dependency. |
|
||||
|
||||
Ghidra's original notice credits software developed at the National Security
|
||||
Agency. It explains that US-government-created portions are not subject to US
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# Plugin icon
|
||||
|
||||
`rea-plugin.png` is a square adaptation of the left panel of REA's GitHub
|
||||
social preview, retrieved on 2026-10-28:
|
||||
|
||||
https://repository-images.githubusercontent.com/1209966933/1a3fef9a-be4b-41f4-8830-a94c0e8288cc
|
||||
|
||||
The image was adapted with imagegen, preserving the cyan REA lettering,
|
||||
pixel typography, orange rule, and taglines. Both plugin manifests reference
|
||||
this shared asset; only the PNG is included in the npm package.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB |
File diff suppressed because it is too large.
Load diff
+105
-619
@@ -11,7 +11,6 @@ import json
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import socket
|
||||
import stat
|
||||
@@ -29,156 +28,14 @@ if os.name == "nt":
|
||||
import cutter
|
||||
|
||||
try:
|
||||
from PySide6.QtCore import QCoreApplication, QObject, Signal
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
except ImportError:
|
||||
from PySide2.QtCore import QCoreApplication, QObject, Signal
|
||||
|
||||
|
||||
# Request and response frame budget in bytes, excluding the trailing LF.
|
||||
# REA_BRIDGE_MAX_REQUEST_BYTES selects it. Unset stays 16777216.
|
||||
# 7464 is the authenticated frame of the largest bounded symbol name.
|
||||
# The 64-receipt recovery status is smaller with current_file null. A path
|
||||
# that fits is kept. A path that does not fit is null, not the descriptor path.
|
||||
# 536870887 is Node's 536870888-character string ceiling minus the framing LF.
|
||||
# The ceiling is the encoded payload; JSON expansion is measured before it.
|
||||
# A fitting response is signed from bounded JSON pieces and written in 64 KiB
|
||||
# chunks, so this maximum does not retain several copies of the frame.
|
||||
_BRIDGE_MAX_REQUEST_BYTES_SETTING = "REA_BRIDGE_MAX_REQUEST_BYTES"
|
||||
DEFAULT_BRIDGE_MAX_REQUEST_BYTES = 16 * 1024 * 1024
|
||||
MINIMUM_BRIDGE_MAX_REQUEST_BYTES = 7464
|
||||
MAXIMUM_BRIDGE_MAX_REQUEST_BYTES = 536870887
|
||||
# The outer authenticated frame around the escaped JSON payload, excluding LF.
|
||||
_AUTHENTICATED_PREFIX = b'{"response":"'
|
||||
_AUTHENTICATED_PROOF_MARK = b'","proof":"'
|
||||
_PROOF_HEX_LENGTH = 64
|
||||
_AUTHENTICATED_FRAME_OVERHEAD = (
|
||||
len(_AUTHENTICATED_PREFIX)
|
||||
+ len(_AUTHENTICATED_PROOF_MARK)
|
||||
+ _PROOF_HEX_LENGTH
|
||||
+ len(b'"}')
|
||||
)
|
||||
_AUTHENTICATED_WRITE_BUFFER_BYTES = 64 * 1024
|
||||
# Raw JSON is produced in pieces no larger than this many characters.
|
||||
# One character expands to at most six JSON bytes (\u00XX) before the outer
|
||||
# string escape, so a piece stays far below the frame ceiling.
|
||||
_JSON_CHUNK_CHARS = 64 * 1024
|
||||
# Fixed admission cap. It does not follow REA_BRIDGE_MAX_REQUEST_BYTES.
|
||||
# A further accept is closed. Starting a worker must not stop the listener.
|
||||
MAX_CONNECTION_WORKERS = 8
|
||||
|
||||
|
||||
def _close_socket(connection: socket.socket) -> None:
|
||||
try:
|
||||
connection.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _bridge_max_request_bytes_constraint():
|
||||
return (
|
||||
"REA_BRIDGE_MAX_REQUEST_BYTES must be a positive decimal integer "
|
||||
"from %d to %d bytes; the default is %d bytes (16 MiB). "
|
||||
"A value below %d bytes cannot carry the largest admitted "
|
||||
"symbol-name command, and a larger value is refused so the encoded "
|
||||
"payload plus its framing LF stays one string under Node's "
|
||||
"single-string ceiling."
|
||||
% (
|
||||
MINIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MAXIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
DEFAULT_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MINIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _parse_bridge_max_request_bytes(raw):
|
||||
"""Refuse a non-decimal or out-of-range ceiling. Never fall back."""
|
||||
constraint = _bridge_max_request_bytes_constraint()
|
||||
if isinstance(raw, bool):
|
||||
raise RuntimeError(constraint)
|
||||
if isinstance(raw, int):
|
||||
value = raw
|
||||
elif isinstance(raw, str):
|
||||
# ASCII digits only. str.isdigit() accepts numerals such as U+0668
|
||||
# that the TypeScript setting rejects.
|
||||
if re.fullmatch(r"[1-9][0-9]*", raw) is None:
|
||||
raise RuntimeError(constraint)
|
||||
value = int(raw)
|
||||
else:
|
||||
raise RuntimeError(constraint)
|
||||
if (
|
||||
value < MINIMUM_BRIDGE_MAX_REQUEST_BYTES
|
||||
or value > MAXIMUM_BRIDGE_MAX_REQUEST_BYTES
|
||||
):
|
||||
raise RuntimeError(constraint)
|
||||
return value
|
||||
|
||||
|
||||
def _resolve_bridge_max_request_bytes():
|
||||
"""Prefer an injected global, then the process environment, then 16 MiB."""
|
||||
injected = globals().get(_BRIDGE_MAX_REQUEST_BYTES_SETTING)
|
||||
if injected is not None:
|
||||
return _parse_bridge_max_request_bytes(injected), "setting"
|
||||
raw = os.environ.get(_BRIDGE_MAX_REQUEST_BYTES_SETTING)
|
||||
if raw is None:
|
||||
return DEFAULT_BRIDGE_MAX_REQUEST_BYTES, "default"
|
||||
return _parse_bridge_max_request_bytes(raw), "setting"
|
||||
|
||||
|
||||
MAX_FRAME_BYTES, BRIDGE_MAX_REQUEST_BYTES_SOURCE = (
|
||||
_resolve_bridge_max_request_bytes()
|
||||
)
|
||||
|
||||
|
||||
def _bridge_max_request_bytes_recovery():
|
||||
return (
|
||||
"The effective REA_BRIDGE_MAX_REQUEST_BYTES value is %d bytes "
|
||||
"(source %s; default %d, floor %d, maximum %d). "
|
||||
"Set REA_BRIDGE_MAX_REQUEST_BYTES in the environment that starts REA "
|
||||
"to a decimal integer from %d to %d, then restart REA. "
|
||||
"Restart Hopper or Cutter from that same environment so the bridge "
|
||||
"uses this ceiling."
|
||||
% (
|
||||
MAX_FRAME_BYTES,
|
||||
BRIDGE_MAX_REQUEST_BYTES_SOURCE,
|
||||
DEFAULT_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MINIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MAXIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MINIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MAXIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _frame_limit_exceeded():
|
||||
return ValueError(
|
||||
"frame-limit-exceeded: Bridge request exceeds the %d-byte payload limit. %s"
|
||||
% (MAX_FRAME_BYTES, _bridge_max_request_bytes_recovery())
|
||||
)
|
||||
from PySide2.QtCore import QObject, Signal
|
||||
|
||||
|
||||
MAX_FRAME_BYTES = 16 * 1024 * 1024
|
||||
SOCKET_TIMEOUT_SECONDS = 20
|
||||
AUTH_FRAME_TIMEOUT_SECONDS = 1
|
||||
# The client request deadline. The authenticated frame read may use up to
|
||||
# this long; it does not grow past it when the byte ceiling is raised.
|
||||
REQUEST_DEADLINE_SECONDS = 25
|
||||
|
||||
|
||||
def _authenticated_frame_timeout_seconds():
|
||||
"""Time to hash and transfer one maximum-sized authenticated frame.
|
||||
|
||||
The hello frame stays at one second. The following command frame is
|
||||
built only after that challenge, so its read budget follows the
|
||||
configured payload size, at one second per mebibyte, and never exceeds
|
||||
the 25 second client deadline.
|
||||
"""
|
||||
mebibytes = (MAX_FRAME_BYTES + (1024 * 1024) - 1) // (1024 * 1024)
|
||||
return min(
|
||||
REQUEST_DEADLINE_SECONDS,
|
||||
max(AUTH_FRAME_TIMEOUT_SECONDS, mebibytes),
|
||||
)
|
||||
|
||||
|
||||
MAX_DESCRIPTOR_BYTES = 64 * 1024
|
||||
MAX_COMPLETION_RECEIPTS = 64
|
||||
|
||||
@@ -469,16 +326,15 @@ class _RequestDispatcher(QObject):
|
||||
self._receipt_lock = threading.Lock()
|
||||
self.current_file = self._read_current_file()
|
||||
try:
|
||||
value = QCoreApplication.applicationVersion()
|
||||
self.cutter_version = value if isinstance(value, str) and value else None
|
||||
self.cutter_version = str(cutter.core().getVersionInformation())
|
||||
except Exception:
|
||||
self.cutter_version = None
|
||||
|
||||
@staticmethod
|
||||
def _read_current_file() -> str | None:
|
||||
try:
|
||||
value = cutter.core().getConfig("file.path")
|
||||
return value if isinstance(value, str) and value else None
|
||||
value = cutter.core().currentlyOpenFile()
|
||||
return str(value) if value else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -623,17 +479,6 @@ class _BridgeServer(threading.Thread):
|
||||
self.token = secrets.token_urlsafe(32)
|
||||
self.poisoned = False
|
||||
self.connection_ordinal = 0
|
||||
# The client proof is inside the authenticated frame, so that read
|
||||
# cannot start after proof. It runs off the accept loop. One
|
||||
# authenticated command still runs at a time. completed_connection
|
||||
# is the longest prefix of these ordinals whose handlers have
|
||||
# terminated, not "this status ordinal minus one".
|
||||
self._command_lock = threading.Lock()
|
||||
self._connections_lock = threading.Lock()
|
||||
self._connections: set[socket.socket] = set()
|
||||
self._workers: list[threading.Thread] = []
|
||||
self._finished_through = 0
|
||||
self._finished_ordinals: set[int] = set()
|
||||
self.directory = _bridge_directory()
|
||||
self.descriptor = self.directory / f"cutter-{os.getpid()}-{self.session_id}.json"
|
||||
|
||||
@@ -654,7 +499,6 @@ class _BridgeServer(threading.Thread):
|
||||
"current_file": self.dispatcher.current_file,
|
||||
"cutter_version": self.dispatcher.cutter_version,
|
||||
"identity_status": "partial",
|
||||
"max_request_bytes": MAX_FRAME_BYTES,
|
||||
}
|
||||
if os.name == "nt":
|
||||
_write_windows_descriptor(self.directory, self.descriptor, json.dumps(payload).encode("utf-8"))
|
||||
@@ -696,46 +540,8 @@ class _BridgeServer(threading.Thread):
|
||||
continue
|
||||
except OSError:
|
||||
break
|
||||
with self._connections_lock:
|
||||
self._workers = [
|
||||
worker for worker in self._workers if worker.is_alive()
|
||||
]
|
||||
at_capacity = len(self._workers) >= MAX_CONNECTION_WORKERS
|
||||
if at_capacity:
|
||||
_close_socket(connection)
|
||||
continue
|
||||
self.connection_ordinal += 1
|
||||
connection_ordinal = self.connection_ordinal
|
||||
worker = threading.Thread(
|
||||
target=self._serve_connection,
|
||||
args=(connection, connection_ordinal),
|
||||
name="rea-cutter-bridge-connection",
|
||||
daemon=True,
|
||||
)
|
||||
with self._connections_lock:
|
||||
self._connections.add(connection)
|
||||
self._workers.append(worker)
|
||||
try:
|
||||
worker.start()
|
||||
except RuntimeError:
|
||||
with self._connections_lock:
|
||||
self._connections.discard(connection)
|
||||
if worker in self._workers:
|
||||
self._workers.remove(worker)
|
||||
self._note_connection_finished(connection_ordinal)
|
||||
_close_socket(connection)
|
||||
with self._connections_lock:
|
||||
workers = list(self._workers)
|
||||
for worker in workers:
|
||||
worker.join(timeout=1)
|
||||
self.server.close()
|
||||
try:
|
||||
self.descriptor.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _serve_connection(self, connection: socket.socket, connection_ordinal: int) -> None:
|
||||
try:
|
||||
with connection:
|
||||
connection.settimeout(AUTH_FRAME_TIMEOUT_SECONDS)
|
||||
authenticated = False
|
||||
@@ -743,73 +549,60 @@ class _BridgeServer(threading.Thread):
|
||||
hello = self._receive_frame(connection)
|
||||
if not isinstance(hello, dict) or hello.get("kind") != "rea-auth-v2" or not isinstance(hello.get("nonce"), str) or len(hello["nonce"]) != 64:
|
||||
self._send(connection, {"ok": False, "error": "authentication-failed"})
|
||||
return
|
||||
continue
|
||||
client_nonce = hello["nonce"]
|
||||
server_nonce = f"{connection_ordinal:016x}{secrets.token_hex(24)}"
|
||||
server_proof = hmac.new(self.token.encode(), f"rea-cutter-server-v2:{self.session_id}:{client_nonce}:{server_nonce}".encode(), hashlib.sha256).hexdigest()
|
||||
self._send(connection, {"session_id": self.session_id, "nonce": server_nonce, "proof": server_proof})
|
||||
authenticated_frame = self._receive_frame(
|
||||
connection, _authenticated_frame_timeout_seconds()
|
||||
)
|
||||
authenticated_frame = self._receive_frame(connection)
|
||||
request_bytes = authenticated_frame.get("request") if isinstance(authenticated_frame, dict) else None
|
||||
presented_proof = authenticated_frame.get("proof") if isinstance(authenticated_frame, dict) else None
|
||||
if not isinstance(request_bytes, str) or not isinstance(presented_proof, str):
|
||||
expected_proof = hmac.new(self.token.encode(), f"rea-cutter-client-v2:{self.session_id}:{client_nonce}:{server_nonce}:{request_bytes}".encode(), hashlib.sha256).hexdigest()
|
||||
if not isinstance(request_bytes, str) or not isinstance(authenticated_frame.get("proof"), str) or not hmac.compare_digest(authenticated_frame["proof"], expected_proof):
|
||||
self._send(connection, {"ok": False, "error": "authentication-failed"})
|
||||
return
|
||||
# Update the prefix and the request separately. One joined
|
||||
# preimage would retain another copy of a maximum-sized command.
|
||||
expected_proof = hmac.new(self.token.encode(), digestmod=hashlib.sha256)
|
||||
expected_proof.update(
|
||||
f"rea-cutter-client-v2:{self.session_id}:{client_nonce}:{server_nonce}:".encode()
|
||||
)
|
||||
expected_proof.update(request_bytes.encode())
|
||||
if not hmac.compare_digest(presented_proof, expected_proof.hexdigest()):
|
||||
self._send(connection, {"ok": False, "error": "authentication-failed"})
|
||||
return
|
||||
continue
|
||||
request = json.loads(request_bytes)
|
||||
if not isinstance(request, dict) or request.get("session_id") != self.session_id:
|
||||
self._send(connection, {"ok": False, "error": "authentication-failed"})
|
||||
return
|
||||
continue
|
||||
authenticated = True
|
||||
with self._command_lock:
|
||||
connection.settimeout(SOCKET_TIMEOUT_SECONDS)
|
||||
if self.poisoned and request.get("kind") not in ("status", "ack"):
|
||||
self._send_authenticated(connection, {
|
||||
"ok": False,
|
||||
"error": "session-poisoned-after-ui-timeout",
|
||||
"execution_state": "not_started",
|
||||
}, client_nonce, server_nonce)
|
||||
return
|
||||
if (
|
||||
request.get("kind") == "command"
|
||||
and not self.dispatcher.can_accept_command()
|
||||
):
|
||||
self._send_authenticated(connection, {
|
||||
"ok": False,
|
||||
"error": "completion-receipt-capacity-exceeded",
|
||||
"execution_state": "not_started",
|
||||
}, client_nonce, server_nonce)
|
||||
return
|
||||
request["session_id"] = self.session_id
|
||||
event = threading.Event()
|
||||
cancelled = threading.Event()
|
||||
reply: dict[str, Any] = {}
|
||||
request["_event"] = event
|
||||
request["_cancelled"] = cancelled
|
||||
self.dispatcher.dispatch.emit(request, reply)
|
||||
if not event.wait(SOCKET_TIMEOUT_SECONDS):
|
||||
cancelled.set()
|
||||
self.poisoned = True
|
||||
self._send_authenticated(connection, {
|
||||
"ok": False,
|
||||
"error": "ui-thread-request-timeout",
|
||||
"execution_state": "unknown",
|
||||
"message": "The UI operation may still complete after this timeout; restart Cutter before issuing further commands.",
|
||||
}, client_nonce, server_nonce)
|
||||
else:
|
||||
if request.get("kind") == "status" and reply.get("ok") is True:
|
||||
reply["completed_connection"] = self._completion_watermark()
|
||||
self._send_authenticated(connection, reply, client_nonce, server_nonce)
|
||||
connection.settimeout(SOCKET_TIMEOUT_SECONDS)
|
||||
if self.poisoned and request.get("kind") not in ("status", "ack"):
|
||||
self._send_authenticated(connection, {
|
||||
"ok": False,
|
||||
"error": "session-poisoned-after-ui-timeout",
|
||||
"execution_state": "not_started",
|
||||
}, client_nonce, server_nonce)
|
||||
continue
|
||||
if (
|
||||
request.get("kind") == "command"
|
||||
and not self.dispatcher.can_accept_command()
|
||||
):
|
||||
self._send_authenticated(connection, {
|
||||
"ok": False,
|
||||
"error": "completion-receipt-capacity-exceeded",
|
||||
"execution_state": "not_started",
|
||||
}, client_nonce, server_nonce)
|
||||
continue
|
||||
request["session_id"] = self.session_id
|
||||
event = threading.Event()
|
||||
cancelled = threading.Event()
|
||||
reply: dict[str, Any] = {}
|
||||
request["_event"] = event
|
||||
request["_cancelled"] = cancelled
|
||||
self.dispatcher.dispatch.emit(request, reply)
|
||||
if not event.wait(SOCKET_TIMEOUT_SECONDS):
|
||||
cancelled.set()
|
||||
self.poisoned = True
|
||||
self._send_authenticated(connection, {
|
||||
"ok": False,
|
||||
"error": "ui-thread-request-timeout",
|
||||
"execution_state": "unknown",
|
||||
"message": "The UI operation may still complete after this timeout; restart Cutter before issuing further commands.",
|
||||
}, client_nonce, server_nonce)
|
||||
else:
|
||||
if request.get("kind") == "status" and reply.get("ok") is True:
|
||||
reply["completed_connection"] = f"{connection_ordinal - 1:016x}"
|
||||
self._send_authenticated(connection, reply, client_nonce, server_nonce)
|
||||
except socket.timeout:
|
||||
if authenticated:
|
||||
self._send_authenticated(connection, {
|
||||
@@ -820,217 +613,77 @@ class _BridgeServer(threading.Thread):
|
||||
}, client_nonce, server_nonce)
|
||||
except Exception as error:
|
||||
self._send(connection, {"ok": False, "error": str(error)})
|
||||
finally:
|
||||
self._note_connection_finished(connection_ordinal)
|
||||
with self._connections_lock:
|
||||
self._connections.discard(connection)
|
||||
|
||||
def _note_connection_finished(self, ordinal: int) -> None:
|
||||
"""Record one accepted connection as terminated.
|
||||
|
||||
The watermark is the longest prefix 1..W that has terminated. A later
|
||||
status cannot report an earlier connection that is still reading or
|
||||
still waiting to run.
|
||||
"""
|
||||
with self._connections_lock:
|
||||
if ordinal <= self._finished_through or ordinal in self._finished_ordinals:
|
||||
return
|
||||
self._finished_ordinals.add(ordinal)
|
||||
while self._finished_through + 1 in self._finished_ordinals:
|
||||
self._finished_through += 1
|
||||
self._finished_ordinals.discard(self._finished_through)
|
||||
|
||||
def _completion_watermark(self) -> str:
|
||||
with self._connections_lock:
|
||||
return f"{self._finished_through:016x}"
|
||||
self.server.close()
|
||||
try:
|
||||
self.descriptor.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _receive_frame(
|
||||
connection: socket.socket,
|
||||
timeout_seconds: float = AUTH_FRAME_TIMEOUT_SECONDS,
|
||||
) -> dict[str, Any]:
|
||||
def _receive_frame(connection: socket.socket) -> dict[str, Any]:
|
||||
data = bytearray()
|
||||
deadline = time.monotonic() + timeout_seconds
|
||||
newline = -1
|
||||
while newline < 0:
|
||||
if len(data) > MAX_FRAME_BYTES:
|
||||
raise _frame_limit_exceeded()
|
||||
deadline = time.monotonic() + AUTH_FRAME_TIMEOUT_SECONDS
|
||||
while b"\n" not in data:
|
||||
if len(data) >= MAX_FRAME_BYTES:
|
||||
raise ValueError("frame-limit-exceeded")
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise socket.timeout("authentication frame timed out")
|
||||
connection.settimeout(remaining)
|
||||
# MAX_FRAME_BYTES limits the JSON payload; the terminating LF is
|
||||
# framing and may be the one byte beyond that payload budget.
|
||||
part = connection.recv(min(65536, MAX_FRAME_BYTES + 1 - len(data)))
|
||||
if not part:
|
||||
raise ValueError("incomplete authentication frame")
|
||||
scanned = len(data)
|
||||
data.extend(part)
|
||||
# The delimiter is one byte, so it cannot straddle a chunk.
|
||||
# Search only the bytes just received.
|
||||
newline = data.find(b"\n", scanned)
|
||||
if newline > MAX_FRAME_BYTES:
|
||||
raise _frame_limit_exceeded()
|
||||
if newline != len(data) - 1:
|
||||
line, remainder = bytes(data).split(b"\n", 1)
|
||||
if len(line) > MAX_FRAME_BYTES or remainder:
|
||||
raise ValueError("invalid framed request")
|
||||
value = json.loads(bytes(data[:newline]))
|
||||
value = json.loads(line)
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
def _send_authenticated(self, connection: socket.socket, value: dict[str, Any], client_nonce: str, server_nonce: str) -> None:
|
||||
if self._deliver_authenticated(connection, value, client_nonce, server_nonce):
|
||||
encoded = self._encode_authenticated_response(value, client_nonce, server_nonce)
|
||||
if encoded is None:
|
||||
value = self._truncated_response(value)
|
||||
encoded = self._encode_authenticated_response(value, client_nonce, server_nonce)
|
||||
if encoded is None:
|
||||
# This fixed-size fallback is checked through the same signed framing path.
|
||||
value = {
|
||||
"ok": False,
|
||||
"error": "response-limit-exceeded",
|
||||
"execution_state": value.get("execution_state", "unknown"),
|
||||
"output": None,
|
||||
"output_truncated": True,
|
||||
"message": "The result exceeded the bridge frame limit; command completion state is preserved.",
|
||||
}
|
||||
encoded = self._encode_authenticated_response(value, client_nonce, server_nonce)
|
||||
if encoded is None:
|
||||
return
|
||||
# Keep the recovery payload. Include the observed path when it
|
||||
# fits. An explicit null path means unknown, so the client does
|
||||
# not pair this generation with the descriptor's older path.
|
||||
bounded = self._bounded_status_response(value)
|
||||
if bounded is not None:
|
||||
observed_path = value.get("current_file")
|
||||
if isinstance(observed_path, str) and observed_path:
|
||||
with_path = dict(bounded)
|
||||
with_path["current_file"] = observed_path
|
||||
with_path.pop("message", None)
|
||||
if self._deliver_authenticated(
|
||||
connection, with_path, client_nonce, server_nonce
|
||||
):
|
||||
return
|
||||
if self._deliver_authenticated(
|
||||
connection, bounded, client_nonce, server_nonce
|
||||
):
|
||||
return
|
||||
command = self._bounded_command_response(value)
|
||||
if command is not None:
|
||||
observed_path = value.get("current_file")
|
||||
if isinstance(observed_path, str) and observed_path:
|
||||
with_path = dict(command)
|
||||
with_path["current_file"] = observed_path
|
||||
with_path.pop("message", None)
|
||||
if self._deliver_authenticated(
|
||||
connection, with_path, client_nonce, server_nonce
|
||||
):
|
||||
return
|
||||
if self._deliver_authenticated(
|
||||
connection, command, client_nonce, server_nonce
|
||||
):
|
||||
return
|
||||
value = self._truncated_response(value)
|
||||
if self._deliver_authenticated(connection, value, client_nonce, server_nonce):
|
||||
return
|
||||
# This fixed-size fallback is checked through the same signed framing path.
|
||||
self._deliver_authenticated(connection, {
|
||||
"ok": False,
|
||||
"error": "response-limit-exceeded",
|
||||
"execution_state": value.get("execution_state", "unknown"),
|
||||
"output": None,
|
||||
"output_truncated": True,
|
||||
"message": "The result exceeded the bridge frame limit; command completion state is preserved.",
|
||||
}, client_nonce, server_nonce)
|
||||
|
||||
def _deliver_authenticated(
|
||||
self, connection: socket.socket, value: dict[str, Any], client_nonce: str, server_nonce: str
|
||||
) -> bool:
|
||||
proof = self._authenticated_proof(value, client_nonce, server_nonce)
|
||||
if proof is None:
|
||||
return False
|
||||
try:
|
||||
self._write_authenticated_frame(connection, value, proof)
|
||||
connection.sendall(encoded)
|
||||
except OSError:
|
||||
# A disconnected caller must not fall through to another payload.
|
||||
pass
|
||||
return True
|
||||
|
||||
def _authenticated_proof(
|
||||
self, value: dict[str, Any], client_nonce: str, server_nonce: str
|
||||
) -> str | None:
|
||||
def _encode_authenticated_response(self, value: dict[str, Any], client_nonce: str, server_nonce: str) -> bytes | None:
|
||||
if not _within_response_limit(value):
|
||||
return None
|
||||
# The payload budget excludes the terminating LF. Fragments are
|
||||
# measured and signed here, then discarded. The write pass encodes
|
||||
# them again so a maximum-sized reply is not held in several copies.
|
||||
frame_size = _AUTHENTICATED_FRAME_OVERHEAD
|
||||
proof = hmac.new(self.token.encode(), digestmod=hashlib.sha256)
|
||||
proof.update(
|
||||
f"rea-cutter-response-v2:{self.session_id}:{client_nonce}:{server_nonce}:".encode()
|
||||
)
|
||||
for fragment in _response_fragments(value):
|
||||
frame_size += len(_escape_json_fragment(fragment))
|
||||
prefix = b'{"response":"'
|
||||
suffix = b'","proof":"' + (b"0" * 64) + b'"}\n'
|
||||
frame_size = len(prefix) + len(suffix)
|
||||
response_parts: list[bytes] = []
|
||||
escaped_parts: list[bytes] = []
|
||||
for fragment in json.JSONEncoder(ensure_ascii=False, separators=(",", ":")).iterencode(value):
|
||||
raw = fragment.encode("utf-8")
|
||||
escaped = json.dumps(fragment, ensure_ascii=False)[1:-1].encode("utf-8")
|
||||
frame_size += len(escaped)
|
||||
if frame_size > MAX_FRAME_BYTES:
|
||||
return None
|
||||
proof.update(fragment.encode("utf-8"))
|
||||
return proof.hexdigest()
|
||||
|
||||
def _encode_authenticated_response(
|
||||
self, value: dict[str, Any], client_nonce: str, server_nonce: str
|
||||
) -> bytes | None:
|
||||
"""Return one exact frame. The socket path writes the same pieces."""
|
||||
proof = self._authenticated_proof(value, client_nonce, server_nonce)
|
||||
if proof is None:
|
||||
return None
|
||||
return b"".join(self._iter_authenticated_frame(value, proof))
|
||||
|
||||
def _iter_authenticated_frame(self, value: dict[str, Any], proof: str):
|
||||
yield _AUTHENTICATED_PREFIX
|
||||
for fragment in _response_fragments(value):
|
||||
yield _escape_json_fragment(fragment)
|
||||
yield _AUTHENTICATED_PROOF_MARK + proof.encode("ascii") + b'"}\n'
|
||||
|
||||
def _write_authenticated_frame(
|
||||
self, connection: socket.socket, value: dict[str, Any], proof: str
|
||||
) -> None:
|
||||
pending = bytearray()
|
||||
for chunk in self._iter_authenticated_frame(value, proof):
|
||||
pending.extend(chunk)
|
||||
if len(pending) >= _AUTHENTICATED_WRITE_BUFFER_BYTES:
|
||||
connection.sendall(pending)
|
||||
pending.clear()
|
||||
if pending:
|
||||
connection.sendall(pending)
|
||||
|
||||
@staticmethod
|
||||
def _bounded_status_response(value: dict[str, Any]) -> dict[str, Any] | None:
|
||||
if "completed_commands" not in value and "completed_connection" not in value:
|
||||
return None
|
||||
bounded: dict[str, Any] = {
|
||||
"ok": value.get("ok") is True,
|
||||
"session_id": value.get("session_id"),
|
||||
"document_generation": value.get("document_generation"),
|
||||
"identity_status": value.get("identity_status", "partial"),
|
||||
"completed_commands": value.get("completed_commands"),
|
||||
"current_file": None,
|
||||
"message": (
|
||||
"current_file is unknown because the status frame exceeded "
|
||||
"REA_BRIDGE_MAX_REQUEST_BYTES. Do not reuse an older descriptor "
|
||||
"path with this document_generation."
|
||||
),
|
||||
}
|
||||
if "completed_connection" in value:
|
||||
bounded["completed_connection"] = value["completed_connection"]
|
||||
return bounded
|
||||
|
||||
@staticmethod
|
||||
def _bounded_command_response(value: dict[str, Any]) -> dict[str, Any] | None:
|
||||
if "execution_state" not in value and "document_generation" not in value:
|
||||
return None
|
||||
bounded: dict[str, Any] = {
|
||||
"ok": value.get("ok") is True,
|
||||
"output": None,
|
||||
"execution_state": value.get("execution_state", "unknown"),
|
||||
"output_truncated": True,
|
||||
"current_file": None,
|
||||
"message": (
|
||||
"current_file is unknown because the result frame exceeded "
|
||||
"REA_BRIDGE_MAX_REQUEST_BYTES. The command completion state and "
|
||||
"document_generation are preserved. Do not reuse an older "
|
||||
"descriptor path with this document_generation."
|
||||
),
|
||||
}
|
||||
generation = value.get("document_generation")
|
||||
if isinstance(generation, int) and not isinstance(generation, bool):
|
||||
bounded["document_generation"] = generation
|
||||
if value.get("identity_status") == "partial":
|
||||
bounded["identity_status"] = "partial"
|
||||
if value.get("ok") is not True and isinstance(value.get("error"), str):
|
||||
bounded["error"] = value["error"]
|
||||
return bounded
|
||||
response_parts.append(raw)
|
||||
escaped_parts.append(escaped)
|
||||
proof = hmac.new(self.token.encode(), digestmod=hashlib.sha256)
|
||||
proof.update(f"rea-cutter-response-v2:{self.session_id}:{client_nonce}:{server_nonce}:".encode())
|
||||
for part in response_parts:
|
||||
proof.update(part)
|
||||
return prefix + b"".join(escaped_parts) + b'","proof":"' + proof.hexdigest().encode() + b'"}\n'
|
||||
|
||||
@staticmethod
|
||||
def _truncated_response(value: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -1088,17 +741,6 @@ class _BridgeServer(threading.Thread):
|
||||
self.server.close()
|
||||
except OSError:
|
||||
pass
|
||||
with self._connections_lock:
|
||||
connections = list(self._connections)
|
||||
for connection in connections:
|
||||
try:
|
||||
connection.shutdown(socket.SHUT_RDWR)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
connection.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
class ReaCutterPlugin(cutter.CutterPlugin):
|
||||
@@ -1132,128 +774,6 @@ def create_cutter_plugin() -> ReaCutterPlugin:
|
||||
return ReaCutterPlugin()
|
||||
|
||||
|
||||
def _json_object_key(key: Any) -> str:
|
||||
"""Match CPython's JSON object-key text before quoting."""
|
||||
if isinstance(key, str):
|
||||
return key
|
||||
if key is True:
|
||||
return "true"
|
||||
if key is False:
|
||||
return "false"
|
||||
if key is None:
|
||||
return "null"
|
||||
if isinstance(key, int):
|
||||
return str(key)
|
||||
if isinstance(key, float):
|
||||
return json.dumps(key, separators=(",", ":"))
|
||||
raise TypeError("keys must be str, int, float, bool or None")
|
||||
|
||||
|
||||
def _response_fragments(value: Any):
|
||||
"""Yield compact JSON pieces whose concatenation is the encoded value.
|
||||
|
||||
String contents are sliced before encoding so a large output never becomes
|
||||
one frame-sized fragment. Each piece is at most ``_JSON_CHUNK_CHARS``
|
||||
characters, or the six-byte ``\\u00XX`` expansion of one such slice.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
yield '"'
|
||||
for start in range(0, len(value), _JSON_CHUNK_CHARS):
|
||||
yield json.dumps(
|
||||
value[start:start + _JSON_CHUNK_CHARS], ensure_ascii=False
|
||||
)[1:-1]
|
||||
yield '"'
|
||||
return
|
||||
if isinstance(value, dict):
|
||||
yield "{"
|
||||
first = True
|
||||
for key, item in value.items():
|
||||
if not first:
|
||||
yield ","
|
||||
first = False
|
||||
yield from _response_fragments(_json_object_key(key))
|
||||
yield ":"
|
||||
yield from _response_fragments(item)
|
||||
yield "}"
|
||||
return
|
||||
if isinstance(value, (list, tuple)):
|
||||
yield from _list_fragments(value)
|
||||
return
|
||||
token = _scalar_token(value)
|
||||
if token is None:
|
||||
raise TypeError("response value is not JSON")
|
||||
yield token
|
||||
|
||||
|
||||
def _scalar_token(value: Any) -> str | None:
|
||||
"""Compact JSON for one scalar. None means the value needs structure."""
|
||||
if type(value) is int:
|
||||
return str(value)
|
||||
if value is None:
|
||||
return "null"
|
||||
if value is True:
|
||||
return "true"
|
||||
if value is False:
|
||||
return "false"
|
||||
if isinstance(value, float):
|
||||
return json.dumps(value, separators=(",", ":"))
|
||||
return None
|
||||
|
||||
|
||||
def _list_fragments(value: list[Any] | tuple[Any, ...]):
|
||||
"""Encode a list, batching scalars into bounded pieces.
|
||||
|
||||
One ``json.dumps`` per number makes a dense array miss the client
|
||||
deadline. A batch is at most ``_JSON_CHUNK_CHARS`` of compact JSON.
|
||||
"""
|
||||
yield "["
|
||||
first = True
|
||||
batch: list[str] = []
|
||||
batch_len = 0
|
||||
|
||||
def flush_batch() -> str | None:
|
||||
nonlocal first, batch, batch_len
|
||||
if not batch:
|
||||
return None
|
||||
piece = ",".join(batch)
|
||||
batch = []
|
||||
batch_len = 0
|
||||
if first:
|
||||
first = False
|
||||
return piece
|
||||
return "," + piece
|
||||
|
||||
for item in value:
|
||||
token = _scalar_token(item)
|
||||
if token is None:
|
||||
emitted = flush_batch()
|
||||
if emitted is not None:
|
||||
yield emitted
|
||||
if first:
|
||||
first = False
|
||||
else:
|
||||
yield ","
|
||||
yield from _response_fragments(item)
|
||||
continue
|
||||
extra = len(token) if not batch else len(token) + 1
|
||||
if batch and batch_len + extra > _JSON_CHUNK_CHARS:
|
||||
emitted = flush_batch()
|
||||
if emitted is not None:
|
||||
yield emitted
|
||||
extra = len(token)
|
||||
batch.append(token)
|
||||
batch_len += extra
|
||||
emitted = flush_batch()
|
||||
if emitted is not None:
|
||||
yield emitted
|
||||
yield "]"
|
||||
|
||||
|
||||
def _escape_json_fragment(fragment: str) -> bytes:
|
||||
"""Escape one raw JSON piece for the outer response string."""
|
||||
return json.dumps(fragment, ensure_ascii=False)[1:-1].encode("utf-8")
|
||||
|
||||
|
||||
def _within_response_limit(value: Any) -> bool:
|
||||
pending = [value]
|
||||
seen: set[int] = set()
|
||||
@@ -1262,37 +782,20 @@ def _within_response_limit(value: Any) -> bool:
|
||||
current = pending.pop()
|
||||
if isinstance(current, str):
|
||||
size += 2
|
||||
if len(current) > MAX_FRAME_BYTES - size:
|
||||
return False
|
||||
if current.isascii():
|
||||
size += len(current)
|
||||
size += current.count('"') + current.count("\\")
|
||||
size += sum(5 for character in current if ord(character) < 0x20)
|
||||
else:
|
||||
# Count UTF-8 bytes without allocating a copy of the whole output.
|
||||
try:
|
||||
size += sum(
|
||||
len(current[start:start + 65536].encode("utf-8"))
|
||||
for start in range(0, len(current), 65536)
|
||||
)
|
||||
except UnicodeEncodeError:
|
||||
# Unencodable output also needs the signed omission fallback.
|
||||
return False
|
||||
size += current.count('"') + current.count("\\")
|
||||
# A short escape is two JSON bytes and was already counted as one.
|
||||
# Other controls expand to a six-byte \\u00XX sequence.
|
||||
size += sum(
|
||||
1 if ord(character) in (0x08, 0x09, 0x0A, 0x0C, 0x0D) else 5
|
||||
for character in current
|
||||
if ord(character) < 0x20
|
||||
)
|
||||
size += 4 * len(current)
|
||||
size += current.count('"') + current.count("\\")
|
||||
size += sum(5 for character in current if ord(character) < 0x20)
|
||||
elif isinstance(current, dict):
|
||||
identity = id(current)
|
||||
if identity in seen:
|
||||
return False
|
||||
seen.add(identity)
|
||||
count = len(current)
|
||||
# Braces, one colon per entry, and commas between entries.
|
||||
# Keys and values are counted when they are visited.
|
||||
size += 2 + count + max(0, count - 1)
|
||||
size += 2 + 4 * len(current)
|
||||
if size > MAX_FRAME_BYTES:
|
||||
return False
|
||||
pending.extend(current.keys())
|
||||
@@ -1302,29 +805,12 @@ def _within_response_limit(value: Any) -> bool:
|
||||
if identity in seen:
|
||||
return False
|
||||
seen.add(identity)
|
||||
count = len(current)
|
||||
# Brackets and commas between items. Each item is counted below.
|
||||
size += 2 + max(0, count - 1)
|
||||
size += 2 + 2 * len(current)
|
||||
if size > MAX_FRAME_BYTES:
|
||||
return False
|
||||
pending.extend(current)
|
||||
elif current is None:
|
||||
size += 4
|
||||
elif current is True:
|
||||
size += 4
|
||||
elif current is False:
|
||||
size += 5
|
||||
elif isinstance(current, int):
|
||||
size += len(str(current))
|
||||
elif isinstance(current, float):
|
||||
size += len(json.dumps(current, separators=(",", ":")))
|
||||
else:
|
||||
try:
|
||||
size += len(
|
||||
json.dumps(current, ensure_ascii=False, separators=(",", ":"))
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
size += 16
|
||||
if size > MAX_FRAME_BYTES:
|
||||
return False
|
||||
return True
|
||||
@@ -1,91 +1,38 @@
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import ghidra.app.plugin.core.analysis.AutoAnalysisManager;
|
||||
import ghidra.app.services.Analyzer;
|
||||
import ghidra.app.util.headless.HeadlessScript;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.classfinder.ClassSearcher;
|
||||
|
||||
/**
|
||||
* Disable the named auto-analyzers before default auto-analysis.
|
||||
*
|
||||
* REA_GHIDRA_DISABLED_ANALYZERS is process-wide, but Ghidra registers only the
|
||||
* installed analyzers whose canAnalyze(program) accepts this program. A name an
|
||||
* installed analyzer provides without being registered here is not applicable
|
||||
* and is recorded instead of rejected; a name no installed analyzer provides
|
||||
* aborts the import. The outcome is stored as a program property that the
|
||||
* bridge reports during its handshake.
|
||||
*/
|
||||
/** Disable the named auto-analyzers before default auto-analysis. */
|
||||
public final class ReaGhidraAnalysisOptions extends HeadlessScript {
|
||||
static final String REPORT_OPTION = "REA disabled analyzers";
|
||||
private static final String SETTING = "REA_GHIDRA_DISABLED_ANALYZERS";
|
||||
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
String[] arguments = getScriptArgs();
|
||||
if (arguments.length != 1 || arguments[0].isBlank()) {
|
||||
throw new IllegalArgumentException(
|
||||
"ReaGhidraAnalysisOptions requires one comma-separated " + SETTING + " argument");
|
||||
"ReaGhidraAnalysisOptions requires one comma-separated analyzer-name argument");
|
||||
}
|
||||
String[] names = arguments[0].split(",");
|
||||
boolean[] registered = new boolean[names.length];
|
||||
AutoAnalysisManager analysis = AutoAnalysisManager.getAnalysisManager(currentProgram);
|
||||
Map<String, String> options = getCurrentAnalysisOptionsAndValues(currentProgram);
|
||||
Set<String> installed = null;
|
||||
for (int index = 0; index < names.length; index++) {
|
||||
for (String requestedName : names) {
|
||||
monitor.checkCancelled();
|
||||
String name = names[index].trim();
|
||||
names[index] = name;
|
||||
if (analysis.getAnalyzer(name) != null) {
|
||||
if (!options.containsKey(name)) {
|
||||
throw new IllegalStateException(SETTING + " names " + name +
|
||||
", which Ghidra registered for this program without an enablement option");
|
||||
}
|
||||
registered[index] = true;
|
||||
continue;
|
||||
}
|
||||
if (installed == null) installed = installedAnalyzerNames();
|
||||
if (!installed.contains(name)) {
|
||||
throw new IllegalArgumentException(SETTING + " names " + name +
|
||||
", which is not an analyzer in this Ghidra installation");
|
||||
String name = requestedName.trim();
|
||||
if (analysis.getAnalyzer(name) == null || !options.containsKey(name)) {
|
||||
throw new IllegalArgumentException("Unknown Ghidra analyzer for this program: " + name);
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < names.length; index++) {
|
||||
for (String requestedName : names) {
|
||||
monitor.checkCancelled();
|
||||
if (registered[index]) setAnalysisOption(currentProgram, names[index], "false");
|
||||
setAnalysisOption(currentProgram, requestedName.trim(), "false");
|
||||
}
|
||||
options = getCurrentAnalysisOptionsAndValues(currentProgram);
|
||||
JsonArray analyzers = new JsonArray();
|
||||
for (int index = 0; index < names.length; index++) {
|
||||
for (String requestedName : names) {
|
||||
monitor.checkCancelled();
|
||||
String name = names[index];
|
||||
if (registered[index] && !"false".equals(options.get(name))) {
|
||||
throw new IllegalStateException(SETTING + " names " + name +
|
||||
", which remained enabled for this program");
|
||||
String name = requestedName.trim();
|
||||
if (!"false".equals(options.get(name))) {
|
||||
throw new IllegalStateException("Ghidra analyzer remained enabled: " + name);
|
||||
}
|
||||
JsonObject analyzer = new JsonObject();
|
||||
analyzer.addProperty("name", name);
|
||||
analyzer.addProperty("status", registered[index] ? "disabled" : "not_applicable");
|
||||
analyzers.add(analyzer);
|
||||
}
|
||||
JsonObject report = new JsonObject();
|
||||
report.addProperty("format", "rea-ghidra-disabled-analyzers-v1");
|
||||
report.add("analyzers", analyzers);
|
||||
currentProgram.getOptions(Program.PROGRAM_INFO).setString(REPORT_OPTION, report.toString());
|
||||
println("REA disabled analyzers: " + report);
|
||||
}
|
||||
|
||||
/** The same analyzer universe AutoAnalysisManager filters by canAnalyze(program). */
|
||||
private static Set<String> installedAnalyzerNames() {
|
||||
Set<String> names = new HashSet<>();
|
||||
for (Analyzer analyzer : ClassSearcher.getInstances(Analyzer.class)) {
|
||||
names.add(analyzer.getName());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
}
|
||||
+137
-459
@@ -40,7 +40,6 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
@@ -141,7 +140,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
"params"
|
||||
);
|
||||
private static final String[] CAPABILITIES = {
|
||||
"read_binary_inventory_summary",
|
||||
"annotate_native_function",
|
||||
"set_address_name",
|
||||
"set_addresses_names",
|
||||
@@ -179,10 +177,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
private List<FunctionEntry> procedureInventory;
|
||||
private List<InventoryItem> stringInventory;
|
||||
private DecompInterface decompiler;
|
||||
/** Per-function native decompiler limit from the session; 0 leaves it unbounded. */
|
||||
private int decompileTimeoutSeconds;
|
||||
// DecompileProcess converts seconds to an int millisecond timer delay.
|
||||
private static final int MAX_DECOMPILE_TIMEOUT_SECONDS = Integer.MAX_VALUE / 1000;
|
||||
private final ReaGhidraExtensions analysisExtensions = new ReaGhidraExtensions();
|
||||
private static AddressSpace sessionDefaultAddressSpace;
|
||||
|
||||
@@ -194,7 +188,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
Path descriptorPath = Path.of(arguments[0]);
|
||||
SessionDescriptor descriptor = readDescriptor(descriptorPath);
|
||||
decompileTimeoutSeconds = descriptor.decompileTimeoutSeconds();
|
||||
// Windows retains a native immutable bearer descriptor lease until
|
||||
// runtime_close; POSIX removes it after this read. Normal cleanup removes
|
||||
// the Windows file, while abrupt owner death can leave private residue.
|
||||
@@ -405,8 +398,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
writeSuccess(writer, request.id, result);
|
||||
return;
|
||||
}
|
||||
decompileTimeoutSeconds = request.decompileTimeoutSeconds() == null
|
||||
? descriptor.decompileTimeoutSeconds() : request.decompileTimeoutSeconds();
|
||||
writeSuccess(writer, request.id, handleRequest(request, descriptor));
|
||||
}
|
||||
catch (RequestFailure failure) {
|
||||
@@ -435,10 +426,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
"Ghidra auto-analysis did not complete for this Program"
|
||||
);
|
||||
}
|
||||
if (descriptor.existingProject && isMutatingOperation(request.method))
|
||||
throw new RequestFailure("method_unavailable", "Existing Ghidra project sessions are read-only");
|
||||
return switch (request.method) {
|
||||
case "read_binary_inventory_summary" -> binaryInventorySummary(request.params);
|
||||
case "inspect_native_load_image" -> inspectNativeLoadImage(request.params);
|
||||
case "read_bytes" -> readMemoryBytes(request.params);
|
||||
case "search_bytes" -> searchMemoryBytes(request.params, descriptor);
|
||||
@@ -468,7 +456,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
FunctionAnalysisObservation observation = analyzeFunctionObservation(request.params);
|
||||
JsonObject result = functionObservation(observation.dossier(), observation.limitations());
|
||||
result.add("relationship_coverage", observation.relationshipCoverage());
|
||||
if (observation.decompilerFailed()) result.addProperty("cache_policy", "live");
|
||||
yield result;
|
||||
}
|
||||
case "set_address_name", "set_addresses_names" -> {
|
||||
@@ -489,10 +476,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean isMutatingOperation(String method) {
|
||||
return method.equals("annotate_native_function") || method.equals("set_address_name") || method.equals("set_addresses_names");
|
||||
}
|
||||
|
||||
private JsonObject sessionInfo(SessionDescriptor descriptor) throws Exception {
|
||||
boolean timedOut = analysisTimeoutOccurred();
|
||||
JsonObject provider = new JsonObject();
|
||||
@@ -518,16 +501,14 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
result.addProperty("run_id", descriptor.runId);
|
||||
result.addProperty("profile_digest", descriptor.profileDigest);
|
||||
result.add("provider", provider);
|
||||
boolean readOnly = descriptor.existingProject || !descriptor.transport.equals("unix-socket");
|
||||
boolean readOnly = !descriptor.transport.equals("unix-socket");
|
||||
result.addProperty("read_only", readOnly);
|
||||
if (descriptor.existingProject) {
|
||||
result.add("analysis_complete", JsonNull.INSTANCE);
|
||||
result.addProperty("existing_project", true);
|
||||
} else result.addProperty("analysis_complete", !timedOut);
|
||||
result.addProperty("analysis_complete", !timedOut);
|
||||
result.addProperty("analysis_timed_out", timedOut);
|
||||
result.add("capabilities", GSON.toJsonTree(
|
||||
java.util.Arrays.stream(CAPABILITIES)
|
||||
.filter(value -> !readOnly || !isMutatingOperation(value))
|
||||
.filter(value -> !readOnly || !(value.equals("annotate_native_function")
|
||||
|| value.equals("set_address_name") || value.equals("set_addresses_names")))
|
||||
.toArray(String[]::new)
|
||||
));
|
||||
result.add("target", target);
|
||||
@@ -539,21 +520,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
String seeds = currentProgram.getOptions(ghidra.program.model.listing.Program.PROGRAM_INFO)
|
||||
.getString("REA analysis seeds", null);
|
||||
if (seeds != null) result.add("analysis_seeds", JsonParser.parseString(seeds));
|
||||
// Written by ReaGhidraAnalysisOptions before auto-analysis; REA checks it
|
||||
// against the committed profile.
|
||||
String disabledAnalyzers = currentProgram.getOptions(ghidra.program.model.listing.Program.PROGRAM_INFO)
|
||||
.getString("REA disabled analyzers", null);
|
||||
if (disabledAnalyzers != null) result.add("disabled_analyzers", JsonParser.parseString(disabledAnalyzers));
|
||||
// Written by ReaGhidraMipsSoftFloat after analysis; REA requires it
|
||||
// exactly when the committed profile declares soft-float storage.
|
||||
String softFloat = currentProgram.getOptions(ghidra.program.model.listing.Program.PROGRAM_INFO)
|
||||
.getString("REA MIPS soft-float storage", null);
|
||||
if (softFloat != null) result.add("mips_soft_float_storage", JsonParser.parseString(softFloat));
|
||||
// Written by ReaGhidraRiscvCallingConvention after analysis; REA requires it
|
||||
// exactly when the committed profile declares a RISC-V calling-convention policy.
|
||||
String riscvCallingConvention = currentProgram.getOptions(ghidra.program.model.listing.Program.PROGRAM_INFO)
|
||||
.getString("REA RISC-V calling convention", null);
|
||||
if (riscvCallingConvention != null) result.add("riscv_calling_convention", JsonParser.parseString(riscvCallingConvention));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -659,9 +630,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
mapping.addProperty("initialized", block.isInitialized());
|
||||
mapping.addProperty("loaded", block.isLoaded());
|
||||
mapping.addProperty("overlay", block.isOverlay());
|
||||
mapping.addProperty("readable", block.isRead());
|
||||
mapping.addProperty("writable", block.isWrite());
|
||||
mapping.addProperty("executable", block.isExecute());
|
||||
mapping.addProperty("length", source.getLength());
|
||||
FileBytes backing = source.getFileBytes().orElse(null);
|
||||
int sourceIndex = backing == null ? -1 : files.indexOf(backing);
|
||||
@@ -693,7 +661,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
row.addProperty("status", relocation.getStatus().toString());
|
||||
row.addProperty("type", relocation.getType());
|
||||
row.add("symbol", GSON.toJsonTree(relocation.getSymbolName()));
|
||||
row.add("values", GSON.toJsonTree(relocation.getValues() == null ? new long[0] : relocation.getValues()));
|
||||
byte[] original = relocation.getBytes();
|
||||
row.add("original_bytes_hex", original == null ? JsonNull.INSTANCE : GSON.toJsonTree(bytesHex(original)));
|
||||
@@ -929,33 +896,17 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
requireKeys(params, Set.of("document", "address"));
|
||||
requireDocument(params);
|
||||
Address address = requireAddress(params, "address");
|
||||
String at = canonicalAddress(address);
|
||||
// A well-formed address can still lack one exact source offset. Each
|
||||
// outcome has its own code, so REA reports the reason rather than
|
||||
// schema repair. Initialization, not the loaded flag, decides whether
|
||||
// source bytes can exist: file-backed overlays are not loaded.
|
||||
MemoryBlock block = currentProgram.getMemory().getBlock(address);
|
||||
if (block == null)
|
||||
throw new RequestFailure("address_unmapped",
|
||||
"Address " + at + " is outside every Ghidra memory block");
|
||||
if (!block.isInitialized())
|
||||
throw new RequestFailure("address_uninitialized",
|
||||
"Address " + at + " is in uninitialized Ghidra memory block " + block.getName());
|
||||
if (block == null || !block.isInitialized())
|
||||
throw new RequestFailure("invalid_request", "Address has no initialized file-backed memory: " + canonicalAddress(address));
|
||||
TargetFileOffset mapping = targetFileOffset(block, address);
|
||||
if (mapping.mappings() == 0)
|
||||
throw new RequestFailure("file_mapping_absent",
|
||||
"Address " + at + " in initialized Ghidra memory block " + block.getName() +
|
||||
" has no authoritative mapping to the admitted target file");
|
||||
if (mapping.mappings() > 1)
|
||||
throw new RequestFailure("file_mapping_ambiguous",
|
||||
"Address " + at + " has " + mapping.mappings() +
|
||||
" authoritative mappings to the admitted target file");
|
||||
if (mapping.mappings() != 1)
|
||||
throw new RequestFailure("invalid_request", "Address has " + mapping.mappings() + " authoritative target-file mappings: " + canonicalAddress(address));
|
||||
Long fileOffset = mapping.offset();
|
||||
if (fileOffset == null)
|
||||
throw new RequestFailure("file_offset_unrepresentable",
|
||||
"Address " + at + " maps to an admitted target-file offset beyond the exact JSON integer range");
|
||||
throw new RequestFailure("invalid_request", "Address lacks a safe file offset: " + canonicalAddress(address));
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("address", at);
|
||||
result.addProperty("address", canonicalAddress(address));
|
||||
result.addProperty("file_offset", fileOffset);
|
||||
return result;
|
||||
}
|
||||
@@ -1138,8 +1089,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
Function function = resolveProcedure(requireString(params, "procedure"));
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("name", procedureName(function));
|
||||
Symbol symbol = function.getSymbol();
|
||||
result.add("symbol", symbol == null ? JsonNull.INSTANCE : symbolMetadata(symbol));
|
||||
result.addProperty("entrypoint", canonicalAddress(function.getEntryPoint()));
|
||||
result.addProperty("basicblock_count", basicBlocks(function).size());
|
||||
JsonObject body = functionBody(function);
|
||||
@@ -1311,11 +1260,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
result.add("procedure", function == null ? JsonNull.INSTANCE : GSON.toJsonTree(canonicalAddress(function.getEntryPoint())));
|
||||
result.addProperty("architecture", currentProgram.getLanguageID().getIdAsString());
|
||||
result.addProperty("mode", currentProgram.getLanguage().getLanguageDescription().getVariant());
|
||||
if (currentProgram.getLanguageID().getIdAsString().startsWith("MIPS:") ||
|
||||
currentProgram.getLanguageID().getIdAsString().startsWith("ARM:")) {
|
||||
if (currentProgram.getLanguageID().getIdAsString().startsWith("MIPS:")) {
|
||||
JsonArray decoderContext = new JsonArray();
|
||||
String regName = currentProgram.getLanguageID().getIdAsString().startsWith("MIPS:") ? "ISA_MODE" : "TMode";
|
||||
var modeRegister = currentProgram.getRegister(regName);
|
||||
var modeRegister = currentProgram.getRegister("ISA_MODE");
|
||||
if (modeRegister != null) {
|
||||
var modeValue = currentProgram.getProgramContext().getValue(modeRegister, address, false);
|
||||
JsonObject context = new JsonObject();
|
||||
@@ -1336,7 +1283,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
flow.addProperty("computed", false);
|
||||
if (instruction == null) {
|
||||
result.addProperty("status", !currentProgram.getMemory().contains(address) ? "outside-memory" : currentProgram.getListing().getInstructionContaining(address) != null
|
||||
? "not-instruction-boundary" : currentProgram.getListing().getDefinedDataContaining(address) != null ? "data" : "not-in-listing");
|
||||
? "not-instruction-boundary" : currentProgram.getListing().getDefinedDataContaining(address) != null ? "data" : "undecodable");
|
||||
for (String key : List.of("bytes", "length", "mnemonic", "raw_disassembly")) result.add(key, JsonNull.INSTANCE);
|
||||
} else {
|
||||
result.addProperty("status", "decoded");
|
||||
@@ -1408,7 +1355,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
result.add("incoming_references", incomingReferences);
|
||||
}
|
||||
List<String> limitations = new ArrayList<>(List.of("Operand components are Ghidra Listing decoder tokens; effective memory base/index/displacement roles and per-instruction context mode are unavailable. The mode field is the program language variant."));
|
||||
if (result.get("status").getAsString().equals("not-in-listing")) limitations.add("No listed instruction or defined data covers this in-memory address. REA did not attempt a decode; the memory may be uninitialized or contain valid instructions.");
|
||||
if (flow.get("kind").getAsString().equals("terminal")) limitations.add("No RETURN operation was observed in the provider's instruction p-code; terminal classification remains generic.");
|
||||
result.add("limitations", GSON.toJsonTree(limitations));
|
||||
return result;
|
||||
@@ -1592,24 +1538,12 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
requireKeys(params, Set.of("procedure"));
|
||||
Function function = resolveProcedure(requireString(params, "procedure"));
|
||||
InstructionScan scan = scanInstructions(function, Integer.MAX_VALUE);
|
||||
DecompileResults decompilation;
|
||||
String decompilerFailure = null;
|
||||
try {
|
||||
decompilation = decompile(function);
|
||||
} catch (RequestFailure failure) {
|
||||
// The listing, reference and CFG facts below do not depend on the
|
||||
// native decompiler. Cancellation still ends the request.
|
||||
if (!failure.code.equals("decompile_failed") &&
|
||||
!failure.code.equals("decompile_timed_out")) throw failure;
|
||||
decompilation = null;
|
||||
decompilerFailure = failure.getMessage().strip();
|
||||
}
|
||||
DecompileResults decompilation = decompile(function);
|
||||
String pseudocode = decompiledText(decompilation);
|
||||
JsonObject availability = new JsonObject();
|
||||
availability.add("pseudocode", facetAvailability(
|
||||
pseudocode != null,
|
||||
decompilerFailure != null ? decompilerFailure
|
||||
: function.isExternal() ? "Ghidra does not decompile external functions."
|
||||
function.isExternal() ? "Ghidra does not decompile external functions."
|
||||
: function.getBody().isEmpty() ? "Ghidra has no function body to decompile."
|
||||
: "Ghidra completed decompilation without returning pseudocode."
|
||||
));
|
||||
@@ -1658,16 +1592,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
result.add("basic_blocks", blocks);
|
||||
result.add(
|
||||
"native_api",
|
||||
decompilerFailure == null
|
||||
? nativeApiBoundary(function, decompilation)
|
||||
: unavailableNativeApiBoundary(decompilerFailure)
|
||||
);
|
||||
result.add(
|
||||
"native_value_flow",
|
||||
decompilerFailure == null
|
||||
? nativeValueFlow(decompilation)
|
||||
: unavailableNativeValueFlow(decompilerFailure)
|
||||
nativeApiBoundary(function, decompilation)
|
||||
);
|
||||
result.add("native_value_flow", nativeValueFlow(decompilation));
|
||||
// Successful full queries distinguish collected empty sets from missing facts.
|
||||
// Availability does not claim that Ghidra resolved every runtime relationship.
|
||||
for (String facet : List.of("comments", "callers", "callees", "references", "strings_names", "cfg")) {
|
||||
@@ -1714,12 +1641,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
!decompilation.getErrorMessage().isBlank()) {
|
||||
limitations.add("Ghidra decompiler diagnostic: " + decompilation.getErrorMessage());
|
||||
}
|
||||
if (decompilerFailure != null) {
|
||||
limitations.add(
|
||||
decompilerFailure + (decompilerFailure.endsWith(".") ? " " : ". ") +
|
||||
"Pseudocode, the native API boundary and p-code value flow are unavailable for this procedure; assembly, comments, callers, callees, references, strings, names and basic blocks come from the Ghidra program listing and were still collected."
|
||||
);
|
||||
}
|
||||
result.add("limitations", limitations);
|
||||
monitor.checkCancelled();
|
||||
int missingCallers = 0;
|
||||
@@ -1728,16 +1649,25 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
if (unrepresentedCaller(reference, callerFunctions)) missingCallers++;
|
||||
}
|
||||
return new FunctionAnalysisObservation(result, calls.limitations(),
|
||||
callRelationshipCoverage(calls, missingCallers), decompilerFailure != null);
|
||||
callRelationshipCoverage(calls, missingCallers));
|
||||
}
|
||||
|
||||
private JsonObject nativeValueFlow(DecompileResults decompilation) throws Exception {
|
||||
HighFunction highFunction =
|
||||
decompilation == null ? null : decompilation.getHighFunction();
|
||||
if (highFunction == null) {
|
||||
return unavailableNativeValueFlow(
|
||||
JsonObject unavailable = new JsonObject();
|
||||
unavailable.addProperty("available", false);
|
||||
unavailable.addProperty(
|
||||
"reason",
|
||||
"Ghidra has no HighFunction p-code model for this procedure"
|
||||
);
|
||||
JsonArray limitations = new JsonArray();
|
||||
limitations.add(
|
||||
"No p-code operations or def-use relationships were recovered for this procedure."
|
||||
);
|
||||
unavailable.add("limitations", limitations);
|
||||
return unavailable;
|
||||
}
|
||||
|
||||
List<PcodeOpAST> selected = new ArrayList<>();
|
||||
@@ -1954,62 +1884,41 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return symbol == null ? JsonNull.INSTANCE : GSON.toJsonTree(symbol.getName(true));
|
||||
}
|
||||
|
||||
// Analyst naming: a function entry renames the function; another mapped
|
||||
// address renames its non-dynamic primary label or receives a new global
|
||||
// label. Resolve that destination without editing the program.
|
||||
private AddressNameTarget addressNameTarget(Address address, String name)
|
||||
throws ghidra.util.exception.InvalidInputException {
|
||||
// Analyst naming: a function entry renames the function, other addresses get a primary label.
|
||||
private boolean applyAddressName(Address address, String name) throws Exception {
|
||||
if (!currentProgram.getMemory().contains(address))
|
||||
throw new RequestFailure("invalid_request", "Address is not mapped: " + canonicalAddress(address));
|
||||
validateAnnotationText(name, "name");
|
||||
Symbol edited = editedNameSymbol(address);
|
||||
return edited == null
|
||||
? new AddressNameTarget(address, currentProgram.getGlobalNamespace(), name)
|
||||
: new AddressNameTarget(address, edited.getParentNamespace(), symbolLeafName(edited, name));
|
||||
}
|
||||
|
||||
private Symbol editedNameSymbol(Address address) {
|
||||
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
|
||||
if (function != null) return function.getSymbol();
|
||||
Symbol primary = currentProgram.getSymbolTable().getPrimarySymbol(address);
|
||||
return primary == null || primary.isDynamic() ? null : primary;
|
||||
}
|
||||
|
||||
// Every address whose label or function symbol holds the destination.
|
||||
private List<Address> addressNameHolders(AddressNameTarget target) {
|
||||
Set<Address> holders = new TreeSet<>();
|
||||
for (Symbol symbol : currentProgram.getSymbolTable().getLabelOrFunctionSymbols(target.leaf(), target.namespace()))
|
||||
holders.add(symbol.getAddress());
|
||||
return List.copyOf(holders);
|
||||
}
|
||||
|
||||
// Apply one planned destination in its own transaction. Null means the
|
||||
// edit committed with matching primary-symbol readback.
|
||||
private String applyAddressName(AddressNameTarget target) {
|
||||
Address address = target.address();
|
||||
int transaction = currentProgram.startTransaction("REA address name");
|
||||
boolean commit = false;
|
||||
try {
|
||||
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
|
||||
Symbol edited = editedNameSymbol(address);
|
||||
SymbolTable table = currentProgram.getSymbolTable();
|
||||
Symbol primary = function == null ? table.getPrimarySymbol(address) : function.getSymbol();
|
||||
Namespace namespace = primary == null
|
||||
? currentProgram.getGlobalNamespace()
|
||||
: primary.getParentNamespace();
|
||||
String expected = primary == null ? name : symbolLeafName(primary, name);
|
||||
for (Symbol symbol : table.getLabelOrFunctionSymbols(expected, namespace)) {
|
||||
if (!address.equals(symbol.getAddress())) return false;
|
||||
}
|
||||
if (function != null) {
|
||||
function.setName(target.leaf(), SourceType.USER_DEFINED);
|
||||
} else if (edited != null) {
|
||||
edited.setName(target.leaf(), SourceType.USER_DEFINED);
|
||||
function.setName(expected, SourceType.USER_DEFINED);
|
||||
} else {
|
||||
Symbol created = currentProgram.getSymbolTable().createLabel(address, target.leaf(), SourceType.USER_DEFINED);
|
||||
created.setPrimary();
|
||||
if (primary != null && !primary.isDynamic()) {
|
||||
primary.setName(expected, SourceType.USER_DEFINED);
|
||||
} else {
|
||||
Symbol created = table.createLabel(address, expected, SourceType.USER_DEFINED);
|
||||
created.setPrimary();
|
||||
}
|
||||
}
|
||||
invalidateAnalysisCaches();
|
||||
Symbol readback = currentProgram.getSymbolTable().getPrimarySymbol(address);
|
||||
commit = readback != null && target.leaf().equals(readback.getName());
|
||||
return commit ? null : "readback_mismatch";
|
||||
commit = readback != null && expected.equals(readback.getName());
|
||||
return commit;
|
||||
}
|
||||
catch (ghidra.util.exception.InvalidInputException exception) {
|
||||
return "invalid_name";
|
||||
}
|
||||
catch (ghidra.util.exception.DuplicateNameException exception) {
|
||||
return "duplicate_name";
|
||||
catch (ghidra.util.exception.InvalidInputException | ghidra.util.exception.DuplicateNameException exception) {
|
||||
return false;
|
||||
}
|
||||
finally {
|
||||
currentProgram.endTransaction(transaction, commit);
|
||||
@@ -2019,17 +1928,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
private JsonElement setAddressName(JsonObject params) throws Exception {
|
||||
requireKeys(params, Set.of("document", "address", "name"));
|
||||
requireDocument(params);
|
||||
String key = requireString(params, "address");
|
||||
Address address = requireAddress(params, "address");
|
||||
String name = requireString(params, "name");
|
||||
// A single edit keeps request errors for unmapped addresses and
|
||||
// unrepresentable text; other outcomes share the batch representation.
|
||||
if (!currentProgram.getMemory().contains(address))
|
||||
throw new RequestFailure("invalid_request", "Address is not mapped: " + canonicalAddress(address));
|
||||
validateAnnotationText(name, "name");
|
||||
Map<String, String> requested = new LinkedHashMap<>();
|
||||
requested.put(key, name);
|
||||
return applyAddressNames(requested);
|
||||
return GSON.toJsonTree(applyAddressName(requireAddress(params, "address"), requireString(params, "name")));
|
||||
}
|
||||
|
||||
private JsonElement setAddressesNames(JsonObject params) throws Exception {
|
||||
@@ -2037,129 +1936,34 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
requireDocument(params);
|
||||
if (!params.has("names") || !params.get("names").isJsonObject())
|
||||
throw new RequestFailure("invalid_request", "names must be an object of address -> name");
|
||||
Map<String, String> requested = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, JsonElement> entry : params.getAsJsonObject("names").entrySet())
|
||||
requested.put(entry.getKey(), entry.getValue().getAsString());
|
||||
return applyAddressNames(requested);
|
||||
}
|
||||
|
||||
// Decide every request from the names held before any edit, so object
|
||||
// member order never selects which address receives a name. Reply with
|
||||
// per-key results and reasons; a contested destination is listed once in
|
||||
// conflicts and failures refer to it by index.
|
||||
private JsonObject applyAddressNames(Map<String, String> requested) {
|
||||
Map<String, Address> addressesByKey = new LinkedHashMap<>();
|
||||
Map<Address, List<AddressNameRequest>> requestsByAddress = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, String> entry : requested.entrySet()) {
|
||||
Address address = null;
|
||||
JsonObject result = new JsonObject();
|
||||
for (Map.Entry<String, JsonElement> entry : params.getAsJsonObject("names").entrySet()) {
|
||||
try {
|
||||
address = parseReaAddress(entry.getKey());
|
||||
Address address = parseReaAddress(entry.getKey());
|
||||
requestsByAddress.computeIfAbsent(address, ignored -> new ArrayList<>())
|
||||
.add(new AddressNameRequest(entry.getKey(), address, entry.getValue()));
|
||||
.add(new AddressNameRequest(entry.getKey(), address, entry.getValue().getAsString()));
|
||||
}
|
||||
catch (RequestFailure failure) {
|
||||
// The key keeps its own invalid_address outcome below.
|
||||
}
|
||||
addressesByKey.put(entry.getKey(), address);
|
||||
}
|
||||
Map<Address, AddressNameFailure> failures = new HashMap<>();
|
||||
Map<AddressNameDestination, List<AddressNameTarget>> destinations = new LinkedHashMap<>();
|
||||
for (Map.Entry<Address, List<AddressNameRequest>> selected : requestsByAddress.entrySet()) {
|
||||
Address address = selected.getKey();
|
||||
String name = selected.getValue().get(0).name;
|
||||
if (selected.getValue().stream().anyMatch(request -> !request.name.equals(name))) {
|
||||
failures.put(address, new AddressNameFailure("conflicting_alias_names", null));
|
||||
continue;
|
||||
}
|
||||
if (!currentProgram.getMemory().contains(address)) {
|
||||
failures.put(address, new AddressNameFailure("unmapped_address", null));
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
AddressNameTarget target = addressNameTarget(address, name);
|
||||
destinations.computeIfAbsent(target.destination(), ignored -> new ArrayList<>()).add(target);
|
||||
}
|
||||
catch (RequestFailure | ghidra.util.exception.InvalidInputException exception) {
|
||||
failures.put(address, new AddressNameFailure("invalid_name", null));
|
||||
result.addProperty(entry.getKey(), false);
|
||||
}
|
||||
}
|
||||
// Reject a destination that several selected addresses request, or
|
||||
// that another address holds, instead of letting order pick a winner.
|
||||
List<AddressNameConflict> conflicts = new ArrayList<>();
|
||||
List<AddressNameTarget> independent = new ArrayList<>();
|
||||
for (List<AddressNameTarget> requesters : destinations.values()) {
|
||||
AddressNameTarget first = requesters.get(0);
|
||||
List<Address> holders = addressNameHolders(first);
|
||||
String reason;
|
||||
if (requesters.size() > 1) {
|
||||
reason = "duplicate_requested_name";
|
||||
} else if (holders.stream().allMatch(first.address()::equals)) {
|
||||
independent.add(first);
|
||||
continue;
|
||||
} else if (holders.stream().anyMatch(holder ->
|
||||
!holder.equals(first.address()) && !requestsByAddress.containsKey(holder))) {
|
||||
reason = "name_held_by_unselected_address";
|
||||
} else {
|
||||
reason = "name_held_by_selected_address";
|
||||
for (List<AddressNameRequest> requests : requestsByAddress.values()) {
|
||||
String name = requests.get(0).name;
|
||||
boolean conflictingAliasNames = requests.stream()
|
||||
.anyMatch(request -> !request.name.equals(name));
|
||||
boolean success = false;
|
||||
if (!conflictingAliasNames) {
|
||||
try {
|
||||
success = applyAddressName(requests.get(0).address, name);
|
||||
}
|
||||
catch (RequestFailure failure) {
|
||||
// Keep per-address failures in the existing batch result shape.
|
||||
}
|
||||
}
|
||||
AddressNameConflict conflict = new AddressNameConflict(
|
||||
first.namespace(),
|
||||
first.leaf(),
|
||||
requesters.stream().map(AddressNameTarget::address).sorted().toList(),
|
||||
holders
|
||||
);
|
||||
conflicts.add(conflict);
|
||||
for (AddressNameTarget target : requesters)
|
||||
failures.put(target.address(), new AddressNameFailure(reason, conflict));
|
||||
for (AddressNameRequest request : requests)
|
||||
result.addProperty(request.inputAddress, success);
|
||||
}
|
||||
// Independent edits have distinct destinations that no other address
|
||||
// holds, so their results do not depend on application order.
|
||||
for (AddressNameTarget target : independent) {
|
||||
String reason = applyAddressName(target);
|
||||
if (reason != null) failures.put(target.address(), new AddressNameFailure(reason, null));
|
||||
}
|
||||
// Requesters belong to exactly one destination, so ordering conflicts
|
||||
// by their first requester gives indexes independent of member order.
|
||||
conflicts.sort(Comparator.comparing((AddressNameConflict conflict) -> conflict.requesters().get(0)));
|
||||
Map<AddressNameConflict, Integer> indexes = new IdentityHashMap<>();
|
||||
JsonArray contested = new JsonArray();
|
||||
for (AddressNameConflict conflict : conflicts) {
|
||||
indexes.put(conflict, contested.size());
|
||||
JsonObject value = new JsonObject();
|
||||
if (conflict.namespace().isGlobal()) value.add("namespace", JsonNull.INSTANCE);
|
||||
else value.addProperty("namespace", conflict.namespace().getName(true));
|
||||
value.addProperty("name", conflict.leaf());
|
||||
value.add("requested_by", canonicalAddresses(conflict.requesters()));
|
||||
value.add("held_by", canonicalAddresses(conflict.holders()));
|
||||
contested.add(value);
|
||||
}
|
||||
JsonObject results = new JsonObject();
|
||||
JsonObject failed = new JsonObject();
|
||||
for (Map.Entry<String, Address> entry : addressesByKey.entrySet()) {
|
||||
Address address = entry.getValue();
|
||||
AddressNameFailure failure = address == null
|
||||
? new AddressNameFailure("invalid_address", null)
|
||||
: failures.get(address);
|
||||
results.addProperty(entry.getKey(), failure == null);
|
||||
if (failure == null) continue;
|
||||
JsonObject value = new JsonObject();
|
||||
if (address == null) value.add("address", JsonNull.INSTANCE);
|
||||
else value.addProperty("address", canonicalAddress(address));
|
||||
value.addProperty("reason", failure.reason());
|
||||
if (failure.conflict() == null) value.add("conflict", JsonNull.INSTANCE);
|
||||
else value.addProperty("conflict", indexes.get(failure.conflict()));
|
||||
failed.add(entry.getKey(), value);
|
||||
}
|
||||
JsonObject result = new JsonObject();
|
||||
result.add("results", results);
|
||||
result.add("failures", failed);
|
||||
result.add("conflicts", contested);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static JsonArray canonicalAddresses(List<Address> addresses) {
|
||||
JsonArray result = new JsonArray();
|
||||
for (Address address : addresses) result.add(canonicalAddress(address));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -2170,18 +1974,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return result;
|
||||
}
|
||||
|
||||
private JsonObject binaryInventorySummary(JsonObject params) throws Exception {
|
||||
requireKeys(params, Set.of());
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("document", currentProgram.getName());
|
||||
result.add("segments", segments());
|
||||
// Count the owning inventories, preserving their exact discovery rules
|
||||
// without serializing function identities or string values for counts.
|
||||
result.addProperty("procedure_count", procedures().size());
|
||||
result.addProperty("string_count", strings().size());
|
||||
return result;
|
||||
}
|
||||
|
||||
private JsonArray listNames(JsonObject params) throws Exception {
|
||||
requireKeys(params, Set.of("document", "address"));
|
||||
requireDocument(params);
|
||||
@@ -2208,10 +2000,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
private JsonArray listSegments(JsonObject params) {
|
||||
requireKeys(params, Set.of("document"));
|
||||
requireDocument(params);
|
||||
return segments();
|
||||
}
|
||||
|
||||
private JsonArray segments() {
|
||||
JsonArray result = new JsonArray();
|
||||
String imageBase = canonicalAddress(currentProgram.getImageBase());
|
||||
MemoryBlock[] blocks = currentProgram.getMemory().getBlocks();
|
||||
@@ -2249,7 +2037,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
requireKeys(params, Set.of("document", "address"));
|
||||
requireDocument(params);
|
||||
Address query = requireAddress(params, "address");
|
||||
Function function = functionAtOrContaining(query);
|
||||
Function function = currentProgram.getFunctionManager().getFunctionAt(query);
|
||||
if (function == null && query.isMemoryAddress()) {
|
||||
function = currentProgram.getFunctionManager().getFunctionContaining(query);
|
||||
}
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("query_address", canonicalAddress(query));
|
||||
if (function != null) {
|
||||
@@ -2265,9 +2056,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
|
||||
private JsonArray search(JsonObject params, boolean procedureSearch) throws Exception {
|
||||
Set<String> keys = new HashSet<>(Set.of("pattern", "mode", "case_sensitive", "document"));
|
||||
if (params.has("literal_ascii_candidate")) keys.add("literal_ascii_candidate");
|
||||
requireKeys(params, keys);
|
||||
requireKeys(
|
||||
params,
|
||||
Set.of("pattern", "mode", "case_sensitive", "document")
|
||||
);
|
||||
requireDocument(params);
|
||||
String expression = requireString(params, "pattern");
|
||||
String mode = requireString(params, "mode");
|
||||
@@ -2275,39 +2067,14 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
throw new RequestFailure("invalid_request", "mode must be literal or regex");
|
||||
}
|
||||
boolean caseSensitive = requireBoolean(params, "case_sensitive");
|
||||
ValueMatcher matcher;
|
||||
if (mode.equals("literal") && !caseSensitive) {
|
||||
if (!params.has("literal_ascii_candidate"))
|
||||
throw new RequestFailure("invalid_request", "Insensitive literal search requires an ASCII candidate");
|
||||
matcher = literalCandidateMatcher(optionalString(params, "literal_ascii_candidate"));
|
||||
} else {
|
||||
if (params.has("literal_ascii_candidate"))
|
||||
throw new RequestFailure("invalid_request", "ASCII candidate applies only to insensitive literals");
|
||||
matcher = mode.equals("literal")
|
||||
? literalMatcher(expression)
|
||||
: regexMatcher(expression, caseSensitive);
|
||||
}
|
||||
List<InventoryItem> inventory = procedureSearch
|
||||
? procedures().stream().map(FunctionEntry::item).toList()
|
||||
: strings();
|
||||
ValueMatcher matcher = mode.equals("literal")
|
||||
? literalMatcher(expression, caseSensitive)
|
||||
: regexMatcher(expression, caseSensitive);
|
||||
JsonArray items = new JsonArray();
|
||||
if (procedureSearch) {
|
||||
// Search the names exact lookup accepts, keeping one row per
|
||||
// procedure under its primary name.
|
||||
for (FunctionEntry entry : procedures()) {
|
||||
monitor.checkCancelled();
|
||||
ProcedureNameMatch match = matchProcedureNames(entry, matcher);
|
||||
if (!match.matched()) {
|
||||
continue;
|
||||
}
|
||||
JsonObject item = resultItem(entry.item, null);
|
||||
if (!match.aliases().isEmpty()) {
|
||||
JsonArray aliases = new JsonArray();
|
||||
for (String alias : match.aliases()) aliases.add(alias);
|
||||
item.add("matched_aliases", aliases);
|
||||
}
|
||||
items.add(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
for (InventoryItem item : strings()) {
|
||||
for (InventoryItem item : inventory) {
|
||||
monitor.checkCancelled();
|
||||
if (!matcher.matches(item.value)) {
|
||||
continue;
|
||||
@@ -2317,36 +2084,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* The one definition of the names that select a procedure, shared by exact
|
||||
* lookup and search: its primary name and every other symbol at its exact
|
||||
* entry, each in leaf or namespace-qualified spelling. A label inside the
|
||||
* body never names the procedure. Matching secondary symbols are reported
|
||||
* by their qualified names, sorted.
|
||||
*/
|
||||
private ProcedureNameMatch matchProcedureNames(FunctionEntry entry, ValueMatcher matcher) throws Exception {
|
||||
Function function = entry.function;
|
||||
boolean primary = spellingMatches(function.getName(), entry.item.value, matcher);
|
||||
Set<String> aliases = null;
|
||||
for (Symbol symbol : currentProgram.getSymbolTable().getSymbols(function.getEntryPoint())) {
|
||||
monitor.checkCancelled();
|
||||
String qualified = symbol.getName(true);
|
||||
if (!spellingMatches(symbol.getName(), qualified, matcher)) continue;
|
||||
if (qualified.equals(entry.item.value)) {
|
||||
primary = true;
|
||||
continue;
|
||||
}
|
||||
if (aliases == null) aliases = new TreeSet<>();
|
||||
aliases.add(qualified);
|
||||
}
|
||||
return new ProcedureNameMatch(primary, aliases == null ? List.of() : List.copyOf(aliases));
|
||||
}
|
||||
|
||||
/** A leaf is the suffix of its qualified spelling, so a literal found in it is found in both. */
|
||||
private static boolean spellingMatches(String leaf, String qualified, ValueMatcher matcher) {
|
||||
return matcher.matches(qualified) || (!leaf.equals(qualified) && matcher.matches(leaf));
|
||||
}
|
||||
|
||||
private List<InventoryItem> names() throws Exception {
|
||||
if (nameInventory != null) {
|
||||
return nameInventory;
|
||||
@@ -2360,8 +2097,13 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
if (!address.isMemoryAddress() && !address.isExternalAddress()) {
|
||||
continue;
|
||||
}
|
||||
JsonObject facts = symbolMetadata(symbol);
|
||||
result.add(new InventoryItem(address, symbol.getName(true), facts, null));
|
||||
JsonObject facts = new JsonObject();
|
||||
facts.addProperty("primary", symbol.isPrimary());
|
||||
facts.addProperty("dynamic", symbol.isDynamic());
|
||||
facts.addProperty("external", symbol.isExternal());
|
||||
facts.addProperty("type", normalizedSymbolType(symbol));
|
||||
facts.addProperty("source", normalizedSource(symbol.getSource()));
|
||||
result.add(new InventoryItem(address, symbol.getName(true), facts));
|
||||
}
|
||||
result.sort(INVENTORY_ORDER);
|
||||
nameInventory = List.copyOf(result);
|
||||
@@ -2405,8 +2147,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
InventoryItem item = new InventoryItem(
|
||||
function.getEntryPoint(),
|
||||
symbol == null ? function.getName() : symbol.getName(true),
|
||||
facts,
|
||||
symbol == null ? null : symbolMetadata(symbol)
|
||||
facts
|
||||
);
|
||||
destination.add(new FunctionEntry(function, item));
|
||||
}
|
||||
@@ -2437,7 +2178,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
instance.isMissingNullTerminator() ? "missing" : "present_or_not_required"
|
||||
);
|
||||
facts.addProperty("byte_length", Math.max(0, data.getLength()));
|
||||
result.add(new InventoryItem(data.getAddress(), value, facts, null));
|
||||
result.add(new InventoryItem(data.getAddress(), value, facts));
|
||||
}
|
||||
result.sort(INVENTORY_ORDER);
|
||||
stringInventory = List.copyOf(result);
|
||||
@@ -2449,13 +2190,26 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
// exact symbol name, even when every character is a hexadecimal digit.
|
||||
boolean explicitAddress = isExplicitReaAddress(value);
|
||||
if (!explicitAddress) {
|
||||
// Imported labels (for example Mach-O _main) can share a function
|
||||
// entry without becoming its primary symbol; search accepts the same.
|
||||
ValueMatcher selector = value::equals;
|
||||
List<Function> matches = new ArrayList<>();
|
||||
for (FunctionEntry entry : procedures()) {
|
||||
monitor.checkCancelled();
|
||||
if (matchProcedureNames(entry, selector).matched()) matches.add(entry.function);
|
||||
Function function = entry.function;
|
||||
Symbol symbol = function.getSymbol();
|
||||
String qualified = symbol == null ? function.getName() : symbol.getName(true);
|
||||
boolean matchesName = function.getName().equals(value) || qualified.equals(value);
|
||||
if (!matchesName) {
|
||||
// Imported labels (for example Mach-O _main) can share a
|
||||
// function entry without becoming its primary symbol. Only
|
||||
// exact entries qualify; an interior label is not a selector.
|
||||
for (Symbol entrySymbol : currentProgram.getSymbolTable().getSymbols(function.getEntryPoint())) {
|
||||
monitor.checkCancelled();
|
||||
if (entrySymbol.getName().equals(value) || entrySymbol.getName(true).equals(value)) {
|
||||
matchesName = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (matchesName) matches.add(function);
|
||||
}
|
||||
if (matches.size() > 1)
|
||||
throw new RequestFailure("ambiguous", "Ghidra procedure name is ambiguous: " + value +
|
||||
@@ -2479,7 +2233,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return function;
|
||||
}
|
||||
|
||||
/** Exact entries include externals; containment applies only to memory addresses. */
|
||||
private Function functionAtOrContaining(Address address) {
|
||||
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
|
||||
return function == null && address.isMemoryAddress()
|
||||
@@ -2586,18 +2339,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static JsonObject unavailableNativeValueFlow(String reason) {
|
||||
JsonObject unavailable = new JsonObject();
|
||||
unavailable.addProperty("available", false);
|
||||
unavailable.addProperty("reason", reason);
|
||||
JsonArray limitations = new JsonArray();
|
||||
limitations.add(
|
||||
"No p-code operations or def-use relationships were recovered for this procedure."
|
||||
);
|
||||
unavailable.add("limitations", limitations);
|
||||
return unavailable;
|
||||
}
|
||||
|
||||
private static JsonObject unavailableNativeApiBoundary(String reason) {
|
||||
JsonObject unavailable = new JsonObject();
|
||||
unavailable.addProperty("available", false);
|
||||
@@ -3184,7 +2925,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
if (!currentProgram.getLanguageID().getIdAsString().startsWith("AARCH64:")) {
|
||||
return null;
|
||||
}
|
||||
Function function = functionAtOrContaining(jumpTable.getSwitchAddress());
|
||||
Function function = containingFunction(jumpTable.getSwitchAddress());
|
||||
if (function == null) return null;
|
||||
|
||||
List<Instruction> instructions = new ArrayList<>();
|
||||
@@ -3567,22 +3308,12 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
DecompileResults results = decompiler.decompileFunction(
|
||||
function,
|
||||
decompileTimeoutSeconds,
|
||||
0,
|
||||
monitor
|
||||
);
|
||||
if (results.isCancelled()) {
|
||||
throw new RequestFailure("decompile_cancelled", "Ghidra decompilation was cancelled");
|
||||
}
|
||||
// DecompInterface disposes the native process at the limit and starts a
|
||||
// fresh one for the next request, so the session stays usable.
|
||||
if (results.isTimedOut()) {
|
||||
throw new RequestFailure(
|
||||
"decompile_timed_out",
|
||||
"Ghidra decompilation of " + canonicalAddress(function.getEntryPoint()) +
|
||||
" exceeded the " + decompileTimeoutSeconds +
|
||||
"-second configured decompilation limit (ghidra.decompile_timeout_seconds; launch seed REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS)"
|
||||
);
|
||||
}
|
||||
if (!results.decompileCompleted()) {
|
||||
throw new RequestFailure(
|
||||
"decompile_failed",
|
||||
@@ -3684,7 +3415,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
|
||||
private boolean unrepresentedCaller(Reference reference, Set<Function> callers) {
|
||||
if (!reference.getReferenceType().isCall()) return false;
|
||||
Function caller = functionAtOrContaining(reference.getFromAddress());
|
||||
Function caller = containingFunction(reference.getFromAddress());
|
||||
return caller == null || !callers.contains(caller);
|
||||
}
|
||||
|
||||
@@ -3831,7 +3562,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
void observe(Reference reference) {
|
||||
if (!reference.getReferenceType().isCall()) return;
|
||||
// Reference identity follows the same containing-function owner as referenceEdge.
|
||||
Function target = functionAtOrContaining(reference.getToAddress());
|
||||
Function target = containingFunction(reference.getToAddress());
|
||||
if (returnedCallees != null && (target == null || !returnedCallees.contains(target)))
|
||||
unrepresented++;
|
||||
if (target == null) unresolved(reference.getFromAddress(),
|
||||
@@ -4001,8 +3732,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("source_address", canonicalAddress(reference.getFromAddress()));
|
||||
result.addProperty("target_address", canonicalAddress(reference.getToAddress()));
|
||||
Function source = functionAtOrContaining(reference.getFromAddress());
|
||||
Function target = functionAtOrContaining(reference.getToAddress());
|
||||
Function source = containingFunction(reference.getFromAddress());
|
||||
Function target = containingFunction(reference.getToAddress());
|
||||
result.add(
|
||||
"source_procedure",
|
||||
source == null ? JsonNull.INSTANCE : procedureIdentity(source)
|
||||
@@ -4037,6 +3768,14 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return result;
|
||||
}
|
||||
|
||||
private Function containingFunction(Address address) {
|
||||
Function result = currentProgram.getFunctionManager().getFunctionAt(address);
|
||||
if (result == null && address.isMemoryAddress()) {
|
||||
result = currentProgram.getFunctionManager().getFunctionContaining(address);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String procedureName(Function function) {
|
||||
Symbol symbol = function.getSymbol();
|
||||
return symbol == null ? function.getName() : symbol.getName(true);
|
||||
@@ -4614,9 +4353,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
if (factsName != null) {
|
||||
result.add(factsName, item.facts.deepCopy());
|
||||
}
|
||||
if (item.symbol != null) {
|
||||
result.add("symbol", item.symbol.deepCopy());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -4644,8 +4380,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("address", canonicalAddress(function.getEntryPoint()));
|
||||
result.addProperty("name", procedureName(function));
|
||||
Symbol symbol = function.getSymbol();
|
||||
result.add("symbol", symbol == null ? JsonNull.INSTANCE : symbolMetadata(symbol));
|
||||
result.add("classification", functionClassification(function));
|
||||
result.add("body", functionBody(function));
|
||||
return result;
|
||||
@@ -4874,32 +4608,15 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return value.isEmpty() ? "unknown" : value;
|
||||
}
|
||||
|
||||
private static JsonObject symbolMetadata(Symbol symbol) {
|
||||
JsonObject facts = new JsonObject();
|
||||
facts.addProperty("primary", symbol.isPrimary());
|
||||
facts.addProperty("dynamic", symbol.isDynamic());
|
||||
facts.addProperty("external", symbol.isExternal());
|
||||
facts.addProperty("type", normalizedSymbolType(symbol));
|
||||
facts.addProperty("source", normalizedSource(symbol.getSource()));
|
||||
return facts;
|
||||
}
|
||||
|
||||
private static String normalizedSource(SourceType source) {
|
||||
return source.name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static ValueMatcher literalMatcher(String pattern) {
|
||||
return value -> value.contains(pattern);
|
||||
}
|
||||
|
||||
// REA's host owns Unicode simple folding. Keep all non-ASCII source values
|
||||
// for its final predicate, and filter ASCII-only values without Java regexes.
|
||||
private static ValueMatcher literalCandidateMatcher(String asciiPattern) {
|
||||
if (asciiPattern != null && asciiPattern.chars().anyMatch(character -> character > 127))
|
||||
throw new RequestFailure("invalid_request", "ASCII candidate contains a non-ASCII character");
|
||||
private static ValueMatcher literalMatcher(String pattern, boolean caseSensitive) {
|
||||
String needle = caseSensitive ? pattern : pattern.toLowerCase(Locale.ROOT);
|
||||
return value -> {
|
||||
if (value.chars().anyMatch(character -> character > 127)) return true;
|
||||
return asciiPattern != null && value.toLowerCase(Locale.ROOT).contains(asciiPattern);
|
||||
String candidate = caseSensitive ? value : value.toLowerCase(Locale.ROOT);
|
||||
return candidate.contains(needle);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4931,11 +4648,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
};
|
||||
}
|
||||
|
||||
// The host queue owns session lifetime, including concurrent cancellation.
|
||||
private static RequestFailure regexStackExhausted(String expression, String stage) {
|
||||
return new RequestFailure("regex_stack_exhausted",
|
||||
"Ghidra's Java regex engine exhausted its stack while " + stage +
|
||||
" pattern " + expression + ".");
|
||||
" pattern " + expression + ". Use literal mode or simplify the regex; the session remains usable.");
|
||||
}
|
||||
|
||||
private static SessionDescriptor readDescriptor(Path path) throws IOException {
|
||||
@@ -4946,17 +4662,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
Set<String> keys = new HashSet<>(DESCRIPTOR_KEYS);
|
||||
if (parsed.isJsonObject() && parsed.getAsJsonObject().has("analysis_extensions"))
|
||||
keys.add("analysis_extensions");
|
||||
if (parsed.isJsonObject() && parsed.getAsJsonObject().has("decompile_timeout_seconds"))
|
||||
keys.add("decompile_timeout_seconds");
|
||||
if (parsed.isJsonObject() && parsed.getAsJsonObject().has("existing_project"))
|
||||
keys.add("existing_project");
|
||||
JsonObject object = requireObject(parsed, keys);
|
||||
int decompileTimeout = 0;
|
||||
if (object.has("decompile_timeout_seconds")) {
|
||||
decompileTimeout = requireInteger(object, "decompile_timeout_seconds");
|
||||
if (decompileTimeout < 1 || decompileTimeout > MAX_DECOMPILE_TIMEOUT_SECONDS)
|
||||
throw new IllegalArgumentException("REA session descriptor is invalid");
|
||||
}
|
||||
return new SessionDescriptor(
|
||||
requireString(object, "transport"),
|
||||
requireString(object, "endpoint_path"),
|
||||
@@ -4965,25 +4671,12 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
requireSha256(object, "target_sha256"),
|
||||
requireString(object, "provider_version"),
|
||||
requireString(object, "profile_digest"),
|
||||
object.has("analysis_extensions") ? object.getAsJsonArray("analysis_extensions") : new JsonArray(),
|
||||
decompileTimeout,
|
||||
object.has("existing_project") && requireBoolean(object, "existing_project")
|
||||
object.has("analysis_extensions") ? object.getAsJsonArray("analysis_extensions") : new JsonArray()
|
||||
);
|
||||
}
|
||||
|
||||
private static Request parseRequest(String line, String expectedToken) {
|
||||
JsonElement parsed = JsonParser.parseString(line);
|
||||
Set<String> keys = new HashSet<>(REQUEST_KEYS);
|
||||
if (parsed.isJsonObject() && parsed.getAsJsonObject().has("decompile_timeout_seconds"))
|
||||
keys.add("decompile_timeout_seconds");
|
||||
JsonObject object = requireObject(parsed, keys);
|
||||
Integer timeout = null;
|
||||
if (object.has("decompile_timeout_seconds")) {
|
||||
JsonElement value = object.get("decompile_timeout_seconds");
|
||||
timeout = value.isJsonNull() ? 0 : requireInteger(object, "decompile_timeout_seconds");
|
||||
if (!value.isJsonNull() && (timeout < 1 || timeout > MAX_DECOMPILE_TIMEOUT_SECONDS))
|
||||
throw new IllegalArgumentException("Invalid per-request decompile deadline");
|
||||
}
|
||||
JsonObject object = requireObject(JsonParser.parseString(line), REQUEST_KEYS);
|
||||
int id = requireInteger(object, "id");
|
||||
if (id <= 0 || !constantTimeEquals(requireString(object, "token"), expectedToken)) {
|
||||
throw new IllegalArgumentException("Bridge request authentication failed");
|
||||
@@ -4992,7 +4685,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
if (params == null || !params.isJsonObject()) {
|
||||
throw new IllegalArgumentException("Bridge request parameters are invalid");
|
||||
}
|
||||
return new Request(id, requireString(object, "method"), params.getAsJsonObject(), timeout);
|
||||
return new Request(id, requireString(object, "method"), params.getAsJsonObject());
|
||||
}
|
||||
|
||||
private static JsonObject requireObject(JsonElement element, Set<String> keys) {
|
||||
@@ -5161,23 +4854,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
.thenComparingInt(Reference::getOperandIndex)
|
||||
.thenComparing(Reference::isPrimary);
|
||||
|
||||
private record InventoryItem(Address address, String value, JsonObject facts, JsonObject symbol) {}
|
||||
private record InventoryItem(Address address, String value, JsonObject facts) {}
|
||||
private record AddressNameRequest(String inputAddress, Address address, String name) {}
|
||||
private record AddressNameDestination(long namespace, String leaf) {}
|
||||
private record AddressNameTarget(Address address, Namespace namespace, String leaf) {
|
||||
AddressNameDestination destination() {
|
||||
return new AddressNameDestination(namespace.getID(), leaf);
|
||||
}
|
||||
}
|
||||
private record AddressNameConflict(Namespace namespace, String leaf, List<Address> requesters, List<Address> holders) {}
|
||||
private record AddressNameFailure(String reason, AddressNameConflict conflict) {}
|
||||
private record FunctionEntry(Function function, InventoryItem item) {}
|
||||
private record ProcedureNameMatch(boolean primary, List<String> aliases) {
|
||||
boolean matched() {
|
||||
return primary || !aliases.isEmpty();
|
||||
}
|
||||
}
|
||||
private record FunctionAnalysisObservation(JsonObject dossier, List<String> limitations, JsonObject relationshipCoverage, boolean decompilerFailed) {}
|
||||
private record FunctionAnalysisObservation(JsonObject dossier, List<String> limitations, JsonObject relationshipCoverage) {}
|
||||
private record CallBoundaryObservation(List<String> limitations, Integer unrepresentedCalleeCalls, JsonArray unresolvedCalls) {}
|
||||
private record SwiftStringLiteralDecode(JsonArray literals, JsonArray omissions) {}
|
||||
private record SwiftLiteralLayout(
|
||||
@@ -5196,11 +4876,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
String targetSha256,
|
||||
String providerVersion,
|
||||
String profileDigest,
|
||||
JsonArray analysisExtensions,
|
||||
int decompileTimeoutSeconds,
|
||||
boolean existingProject
|
||||
JsonArray analysisExtensions
|
||||
) {}
|
||||
private record Request(int id, String method, JsonObject params, Integer decompileTimeoutSeconds) {}
|
||||
private record Request(int id, String method, JsonObject params) {}
|
||||
|
||||
private static final class RequestFailure extends RuntimeException {
|
||||
private final String code;
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.program.database.mem.FileBytes;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.address.AddressSpace;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.mem.Memory;
|
||||
import ghidra.program.model.mem.MemoryBlock;
|
||||
import ghidra.program.model.mem.MemoryBlockSourceInfo;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Rebuild a GameCube/Wii DOL memory map from the validated load ranges REA's TypeScript
|
||||
* admission wrote. BinaryLoader maps the whole file once at address 0; this
|
||||
* script replaces that block with text, data, and BSS blocks at their load addresses.
|
||||
*/
|
||||
public final class ReaGhidraPrepareDol extends GhidraScript {
|
||||
// Canonical v2 header, 18 section rows, 12 gap rows and entry, using uint32 hex.
|
||||
private static final int MAX_PLAN_BYTES = 13 + 18 * 49 + 12 * 30 + 17;
|
||||
private static final class Section {
|
||||
final String kind;
|
||||
final int index;
|
||||
final long fileOffset;
|
||||
final long address;
|
||||
final long size;
|
||||
|
||||
Section(String kind, int index, long fileOffset, long address, long size) {
|
||||
this.kind = kind;
|
||||
this.index = index;
|
||||
this.fileOffset = fileOffset;
|
||||
this.address = address;
|
||||
this.size = size;
|
||||
}
|
||||
}
|
||||
|
||||
private record BssGap(long address, long size) {}
|
||||
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
String[] arguments = getScriptArgs();
|
||||
if (arguments.length != 1) {
|
||||
throw new IllegalArgumentException(
|
||||
"GameCube DOL preparation requires exactly one section table path"
|
||||
);
|
||||
}
|
||||
if (!currentProgram.getExecutableFormat().equals("Raw Binary") ||
|
||||
!currentProgram.getLanguageID().getIdAsString().startsWith("PowerPC:BE:32:")) {
|
||||
throw new IllegalArgumentException(
|
||||
"GameCube DOL preparation requires a BinaryLoader import in big-endian 32-bit PowerPC"
|
||||
);
|
||||
}
|
||||
Path tablePath = Path.of(arguments[0]);
|
||||
if (!Files.isRegularFile(tablePath, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IllegalArgumentException("GameCube DOL section table is not a regular file");
|
||||
}
|
||||
if (Files.size(tablePath) > MAX_PLAN_BYTES) {
|
||||
throw new IllegalArgumentException("GameCube DOL plan exceeds the canonical format's byte bound");
|
||||
}
|
||||
|
||||
List<Section> sections = new ArrayList<>();
|
||||
List<BssGap> bssGaps = new ArrayList<>();
|
||||
int expectedSections = -1;
|
||||
int expectedGaps = -1;
|
||||
long entry = -1;
|
||||
for (String line : Files.readAllLines(tablePath)) {
|
||||
String[] fields = line.trim().split("\\s+");
|
||||
switch (fields[0]) {
|
||||
case "plan" -> {
|
||||
requireFields(fields, 4, line);
|
||||
if (expectedSections >= 0 || !fields[1].equals("2")) {
|
||||
throw new IllegalArgumentException("Duplicate or unsupported GameCube DOL plan header");
|
||||
}
|
||||
expectedSections = Integer.parseInt(fields[2]);
|
||||
expectedGaps = Integer.parseInt(fields[3]);
|
||||
// A DOL has 18 slots, including at most 11 data slots and 12 BSS gaps.
|
||||
if (expectedSections < 1 || expectedSections > 18 || expectedGaps < 0 || expectedGaps > 12) {
|
||||
throw new IllegalArgumentException("GameCube DOL plan counts exceed the format's slot bounds");
|
||||
}
|
||||
}
|
||||
case "section" -> {
|
||||
requireFields(fields, 6, line);
|
||||
if (expectedSections < 0 || sections.size() >= expectedSections) {
|
||||
throw new IllegalArgumentException("GameCube DOL section exceeds its declared count");
|
||||
}
|
||||
sections.add(new Section(
|
||||
fields[1],
|
||||
Integer.parseInt(fields[2]),
|
||||
Long.decode(fields[3]),
|
||||
Long.decode(fields[4]),
|
||||
Long.decode(fields[5])
|
||||
));
|
||||
}
|
||||
case "bss-gap" -> {
|
||||
requireFields(fields, 3, line);
|
||||
if (expectedGaps < 0 || bssGaps.size() >= expectedGaps) {
|
||||
throw new IllegalArgumentException("GameCube DOL BSS gap exceeds its declared count");
|
||||
}
|
||||
long address = Long.decode(fields[1]);
|
||||
long size = Long.decode(fields[2]);
|
||||
if (address < 0 || size <= 0) {
|
||||
throw new IllegalArgumentException("GameCube DOL BSS gap is not a positive range");
|
||||
}
|
||||
bssGaps.add(new BssGap(address, size));
|
||||
}
|
||||
case "entry" -> {
|
||||
requireFields(fields, 2, line);
|
||||
entry = Long.decode(fields[1]);
|
||||
}
|
||||
default -> throw new IllegalArgumentException("Unknown GameCube DOL table line: " + line);
|
||||
}
|
||||
}
|
||||
if (expectedSections != sections.size() || expectedGaps != bssGaps.size() || entry < 0) {
|
||||
throw new IllegalArgumentException("GameCube DOL section table is incomplete");
|
||||
}
|
||||
|
||||
Memory memory = currentProgram.getMemory();
|
||||
List<FileBytes> files = memory.getAllFileBytes();
|
||||
if (files.size() != 1) {
|
||||
throw new IllegalStateException("GameCube DOL preparation requires exactly one imported file image");
|
||||
}
|
||||
FileBytes fileBytes = files.get(0);
|
||||
AddressSpace space = currentProgram.getAddressFactory().getDefaultAddressSpace();
|
||||
|
||||
// Remove the whole-file block before creating section blocks, so no two blocks claim one address.
|
||||
for (MemoryBlock block : memory.getBlocks()) {
|
||||
for (MemoryBlockSourceInfo source : block.getSourceInfos()) {
|
||||
if (source.getFileBytes().isPresent() && source.getFileBytes().get().equals(fileBytes)) {
|
||||
memory.removeBlock(block, monitor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Section section : sections) {
|
||||
if (section.fileOffset + section.size > fileBytes.getSize()) {
|
||||
throw new IllegalArgumentException(
|
||||
"GameCube DOL " + section.kind + " " + section.index + " exceeds the imported file"
|
||||
);
|
||||
}
|
||||
boolean text = section.kind.equals("text");
|
||||
if (!text && !section.kind.equals("data")) {
|
||||
throw new IllegalArgumentException("Unknown GameCube DOL section kind: " + section.kind);
|
||||
}
|
||||
MemoryBlock block = memory.createInitializedBlock(
|
||||
"." + section.kind + section.index,
|
||||
space.getAddress(section.address),
|
||||
fileBytes,
|
||||
section.fileOffset,
|
||||
section.size,
|
||||
false
|
||||
);
|
||||
block.setRead(true);
|
||||
block.setWrite(!text);
|
||||
block.setExecute(text);
|
||||
}
|
||||
|
||||
// Apply the domain owner's exact uninitialized ranges; do not reinterpret BSS here.
|
||||
int gapIndex = 0;
|
||||
for (BssGap gap : bssGaps) {
|
||||
MemoryBlock block = memory.createUninitializedBlock(
|
||||
".bss" + gapIndex++,
|
||||
space.getAddress(gap.address()),
|
||||
gap.size(),
|
||||
false
|
||||
);
|
||||
block.setRead(true);
|
||||
block.setWrite(true);
|
||||
block.setExecute(false);
|
||||
}
|
||||
|
||||
Address entryAddress = space.getAddress(entry);
|
||||
MemoryBlock entryBlock = memory.getBlock(entryAddress);
|
||||
if (entryBlock == null || !entryBlock.isExecute()) {
|
||||
throw new IllegalArgumentException("GameCube DOL entry must lie in an executable section");
|
||||
}
|
||||
addEntryPoint(entryAddress);
|
||||
if (!disassemble(entryAddress) || createFunction(entryAddress, "__start") == null) {
|
||||
throw new IllegalStateException("GameCube DOL entry cannot be decoded as a function");
|
||||
}
|
||||
currentProgram.getOptions(Program.PROGRAM_INFO).setBoolean("REA raw binary prepared", true);
|
||||
currentProgram.getOptions(Program.PROGRAM_INFO).setBoolean("REA GameCube DOL prepared", true);
|
||||
}
|
||||
|
||||
private static void requireFields(String[] fields, int count, String line) {
|
||||
if (fields.length != count) {
|
||||
throw new IllegalArgumentException("Malformed GameCube DOL table line: " + line);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.program.database.SpecExtension;
|
||||
import ghidra.program.model.data.DataType;
|
||||
import ghidra.program.model.data.DoubleDataType;
|
||||
import ghidra.program.model.data.Float16DataType;
|
||||
import ghidra.program.model.data.FloatDataType;
|
||||
import ghidra.program.model.data.FunctionDefinition;
|
||||
import ghidra.program.model.data.IntegerDataType;
|
||||
import ghidra.program.model.data.StructureDataType;
|
||||
import ghidra.program.model.lang.CompilerSpec;
|
||||
import ghidra.program.model.lang.Language;
|
||||
import ghidra.program.model.lang.PrototypeModel;
|
||||
import ghidra.program.model.lang.Register;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.model.listing.Parameter;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.listing.VariableStorage;
|
||||
import ghidra.program.model.pcode.Varnode;
|
||||
import ghidra.program.model.pcode.XmlEncode;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.transform.OutputKeys;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
|
||||
/**
|
||||
* Apply the committed RISC-V calling-convention policy after analysis. Ghidra's generic RV32
|
||||
* compiler specification models ilp32d. Each policy derives a program-specific prototype from the
|
||||
* program's own default prototype, as its {@link Policy} row and docs/ghidra-riscv.md state. The
|
||||
* script installs the prototype as a specification extension and assigns it, with dynamic
|
||||
* storage, to every non-thunk function, external function and function definition.
|
||||
*/
|
||||
public final class ReaGhidraRiscvCallingConvention extends GhidraScript {
|
||||
static final String REPORT_KEY = "REA RISC-V calling convention";
|
||||
/** RISC-V floating-point registers by ABI name: ft0-ft11, fs0-fs11 and fa0-fa7. */
|
||||
private static final Pattern FLOAT_REGISTER = Pattern.compile("f[ast]\\d+");
|
||||
|
||||
/**
|
||||
* What a policy changes in the default ilp32d prototype. Every policy also passes values of 9
|
||||
* bytes or more, such as {@code long double}, by reference.
|
||||
*
|
||||
* @param floatEntryBytes the largest value a floating-point register may pass or return; 0
|
||||
* removes those entries and every rule that assigns floating-point storage
|
||||
* @param preservesFloatRegisters whether fs0-fs11 stay preserved across calls
|
||||
*/
|
||||
private record Policy(
|
||||
String id, String prototype, int floatEntryBytes, boolean preservesFloatRegisters) {}
|
||||
|
||||
private static final List<Policy> POLICIES =
|
||||
List.of(new Policy("ilp32-prototype-v1", "__rea_ilp32", 0, false));
|
||||
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
String[] arguments = getScriptArgs();
|
||||
Policy policy =
|
||||
POLICIES.stream()
|
||||
.filter(candidate -> arguments.length == 1 && candidate.id().equals(arguments[0]))
|
||||
.findFirst()
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new IllegalArgumentException(
|
||||
"REA RISC-V calling convention requires one of the policies "
|
||||
+ POLICIES.stream().map(Policy::id).toList()));
|
||||
Language language = currentProgram.getLanguage();
|
||||
if (!language.getProcessor().toString().equals("RISCV")
|
||||
|| currentProgram.getDefaultPointerSize() != 4)
|
||||
throw new IllegalStateException(
|
||||
"REA " + policy.id() + " requires a 32-bit RISC-V program, not "
|
||||
+ language.getLanguageID());
|
||||
CompilerSpec spec = currentProgram.getCompilerSpec();
|
||||
new SpecExtension(currentProgram)
|
||||
.addReplaceCompilerSpecExtension(
|
||||
derive(spec.getDefaultCallingConvention(), spec, language, policy), monitor);
|
||||
PrototypeModel model =
|
||||
currentProgram.getCompilerSpec().getCallingConvention(policy.prototype());
|
||||
if (model == null)
|
||||
throw new IllegalStateException("REA " + policy.prototype() + " was not installed");
|
||||
selfCheck(model, policy, language);
|
||||
int functions = 0;
|
||||
List<Function> callables = new ArrayList<>();
|
||||
currentProgram.getFunctionManager().getFunctions(true).forEach(callables::add);
|
||||
currentProgram.getFunctionManager().getExternalFunctions().forEach(callables::add);
|
||||
for (Function function : callables) {
|
||||
// A thunk's signature is its target's; the target is in this list.
|
||||
if (function.isThunk()) continue;
|
||||
if (function.hasCustomVariableStorage()) function.setCustomVariableStorage(false);
|
||||
function.setCallingConvention(policy.prototype());
|
||||
if (policy.floatEntryBytes() == 0) {
|
||||
List<VariableStorage> storage = new ArrayList<>();
|
||||
storage.add(function.getReturn().getVariableStorage());
|
||||
for (Parameter parameter : function.getParameters())
|
||||
storage.add(parameter.getVariableStorage());
|
||||
// An untyped function has no assigned storage yet; the decompiler applies the
|
||||
// prototype when it recovers one.
|
||||
requireIntegerStorage(
|
||||
storage.toArray(VariableStorage[]::new), function.getName(true), true);
|
||||
}
|
||||
functions += 1;
|
||||
}
|
||||
int definitions = 0;
|
||||
Iterator<DataType> types = currentProgram.getDataTypeManager().getAllDataTypes();
|
||||
while (types.hasNext()) {
|
||||
if (!(types.next() instanceof FunctionDefinition definition)) continue;
|
||||
definition.setCallingConvention(policy.prototype());
|
||||
definitions += 1;
|
||||
}
|
||||
// Written last: any failure above leaves no report, and REA refuses the session.
|
||||
currentProgram.getOptions(Program.PROGRAM_INFO).setString(
|
||||
REPORT_KEY,
|
||||
"{\"policy\":\"" + policy.id() + "\",\"functions\":" + functions
|
||||
+ ",\"definitions\":" + definitions + "}");
|
||||
println(
|
||||
"REA RISC-V " + policy.prototype() + ": functions=" + functions
|
||||
+ " definitions=" + definitions);
|
||||
}
|
||||
|
||||
/** The default ilp32d prototype changed as the policy states. */
|
||||
private static String derive(
|
||||
PrototypeModel base, CompilerSpec spec, Language language, Policy policy) throws Exception {
|
||||
XmlEncode encoder = new XmlEncode();
|
||||
base.encode(encoder, spec.getPcodeInjectLibrary());
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
||||
Document document =
|
||||
factory.newDocumentBuilder().parse(new InputSource(new StringReader(encoder.toString())));
|
||||
Element root = document.getDocumentElement();
|
||||
if (!root.getTagName().equals("prototype"))
|
||||
throw new IllegalStateException("Unexpected default prototype encoding " + root.getTagName());
|
||||
root.setAttribute("name", policy.prototype());
|
||||
List<Element> floatEntries = new ArrayList<>();
|
||||
NodeList entries = root.getElementsByTagName("pentry");
|
||||
for (int index = 0; index < entries.getLength(); index += 1) {
|
||||
Element entry = (Element) entries.item(index);
|
||||
if (entry.getAttribute("storage").equals("float")) floatEntries.add(entry);
|
||||
}
|
||||
if (floatEntries.isEmpty())
|
||||
throw new IllegalStateException("Default prototype has no floating-point entries");
|
||||
if (policy.floatEntryBytes() == 0) {
|
||||
List<Node> removed = new ArrayList<>(floatEntries);
|
||||
NodeList rules = root.getElementsByTagName("rule");
|
||||
for (int index = 0; index < rules.getLength(); index += 1) {
|
||||
Element rule = (Element) rules.item(index);
|
||||
if (usesFloatStorage(rule)) removed.add(rule);
|
||||
}
|
||||
if (removed.size() == floatEntries.size())
|
||||
throw new IllegalStateException("Default prototype has no floating-point rules to remove");
|
||||
for (Node node : removed) node.getParentNode().removeChild(node);
|
||||
} else
|
||||
for (Element entry : floatEntries)
|
||||
entry.setAttribute("maxsize", Integer.toString(policy.floatEntryBytes()));
|
||||
// Values over 2 x XLEN go by reference. Returns need no rule: a value that the return
|
||||
// registers cannot hold already becomes a caller buffer passed in a0.
|
||||
NodeList inputs = root.getElementsByTagName("input");
|
||||
if (inputs.getLength() != 1)
|
||||
throw new IllegalStateException("Default prototype has no single input list");
|
||||
Element input = (Element) inputs.item(0);
|
||||
Element last = null;
|
||||
for (Node child = input.getFirstChild(); child != null; child = child.getNextSibling())
|
||||
if (child instanceof Element element && element.getTagName().equals("rule")) last = element;
|
||||
if (last == null || !joinsAnyValue(last))
|
||||
throw new IllegalStateException("Default prototype's input list does not end with a join");
|
||||
Element byReference = document.createElement("rule");
|
||||
Element datatype = document.createElement("datatype");
|
||||
datatype.setAttribute("name", "any");
|
||||
datatype.setAttribute("minsize", "9");
|
||||
byReference.appendChild(datatype);
|
||||
byReference.appendChild(document.createElement("convert_to_ptr"));
|
||||
input.insertBefore(byReference, last);
|
||||
if (!policy.preservesFloatRegisters()) {
|
||||
NodeList killedLists = root.getElementsByTagName("killedbycall");
|
||||
Element killed = killedLists.getLength() == 0 ? null : (Element) killedLists.item(0);
|
||||
if (killed == null) {
|
||||
killed = document.createElement("killedbycall");
|
||||
root.appendChild(killed);
|
||||
}
|
||||
List<Element> moved = new ArrayList<>();
|
||||
NodeList preservedLists = root.getElementsByTagName("unaffected");
|
||||
for (int list = 0; list < preservedLists.getLength(); list += 1) {
|
||||
NodeList preserved = preservedLists.item(list).getChildNodes();
|
||||
for (int index = 0; index < preserved.getLength(); index += 1) {
|
||||
if (preserved.item(index) instanceof Element varnode
|
||||
&& isFloatRegister(varnode, language))
|
||||
moved.add(varnode);
|
||||
}
|
||||
}
|
||||
for (Element varnode : moved) {
|
||||
varnode.getParentNode().removeChild(varnode);
|
||||
killed.appendChild(varnode);
|
||||
}
|
||||
}
|
||||
StringWriter xml = new StringWriter();
|
||||
var transformer = TransformerFactory.newInstance().newTransformer();
|
||||
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
|
||||
transformer.transform(new DOMSource(document), new StreamResult(xml));
|
||||
return xml.toString();
|
||||
}
|
||||
|
||||
/** The installed prototype places each class of value as its policy states. */
|
||||
private void selfCheck(PrototypeModel model, Policy policy, Language language) {
|
||||
StructureDataType pair = new StructureDataType("rea_float_pair", 0);
|
||||
pair.add(FloatDataType.dataType);
|
||||
pair.add(FloatDataType.dataType);
|
||||
VariableStorage[] storage =
|
||||
model.getStorageLocations(
|
||||
currentProgram,
|
||||
new DataType[] {
|
||||
DoubleDataType.dataType, FloatDataType.dataType, DoubleDataType.dataType,
|
||||
IntegerDataType.dataType, pair, Float16DataType.dataType
|
||||
},
|
||||
false);
|
||||
VariableStorage quad = storage[storage.length - 1];
|
||||
if (quad.size() != currentProgram.getDefaultPointerSize() || quad.getRegister() == null)
|
||||
throw new IllegalStateException(
|
||||
"REA " + policy.prototype() + " does not pass a 16-byte value by reference: " + quad);
|
||||
if (policy.floatEntryBytes() == 0)
|
||||
requireIntegerStorage(storage, policy.prototype() + " self-check", false);
|
||||
if (policy.preservesFloatRegisters()) return;
|
||||
for (Varnode preserved : model.getUnaffectedList()) {
|
||||
Register register = language.getRegister(preserved.getAddress(), preserved.getSize());
|
||||
if (register != null && FLOAT_REGISTER.matcher(register.getName()).matches())
|
||||
throw new IllegalStateException(
|
||||
"REA " + policy.prototype() + " still preserves " + register.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean joinsAnyValue(Element rule) {
|
||||
List<Element> children = new ArrayList<>();
|
||||
for (Node child = rule.getFirstChild(); child != null; child = child.getNextSibling())
|
||||
if (child instanceof Element element) children.add(element);
|
||||
return children.size() == 2
|
||||
&& children.get(0).getTagName().equals("datatype")
|
||||
&& children.get(0).getAttribute("name").equals("any")
|
||||
&& children.get(1).getTagName().equals("join");
|
||||
}
|
||||
|
||||
/** Whether a rule's action, at any depth, assigns floating-point storage. */
|
||||
private static boolean usesFloatStorage(Element element) {
|
||||
NodeList children = element.getChildNodes();
|
||||
for (int index = 0; index < children.getLength(); index += 1) {
|
||||
if (!(children.item(index) instanceof Element child)) continue;
|
||||
if (child.getAttribute("storage").equals("float") || usesFloatStorage(child)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isFloatRegister(Element varnode, Language language) {
|
||||
if (!varnode.getTagName().equals("varnode")
|
||||
|| !varnode.getAttribute("space").equals("register"))
|
||||
return false;
|
||||
Register register =
|
||||
language.getRegister(
|
||||
language.getAddressFactory().getRegisterSpace()
|
||||
.getAddress(Long.decode(varnode.getAttribute("offset"))),
|
||||
Integer.parseInt(varnode.getAttribute("size")));
|
||||
return register != null && FLOAT_REGISTER.matcher(register.getName()).matches();
|
||||
}
|
||||
|
||||
private static void requireIntegerStorage(
|
||||
VariableStorage[] storage, String owner, boolean allowUnassigned) {
|
||||
for (VariableStorage item : storage) {
|
||||
if (item.isBadStorage() || (!allowUnassigned && item.isUnassignedStorage()))
|
||||
throw new IllegalStateException("REA RISC-V storage unresolved for " + owner);
|
||||
List<Register> registers = item.getRegisters();
|
||||
if (registers == null) continue; // stack-only storage
|
||||
for (Register register : registers) {
|
||||
if (FLOAT_REGISTER.matcher(register.getName()).matches())
|
||||
throw new IllegalStateException(
|
||||
"REA RISC-V storage still uses " + register.getName() + " for " + owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-331
@@ -8,103 +8,12 @@ API access on this thread: moving dispatch to a worker can deadlock Hopper.
|
||||
import json
|
||||
import hmac
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
from typing import Any, Optional, Protocol, Sequence
|
||||
|
||||
BAD_ADDRESSES = (-1, 0xFFFFFFFFFFFFFFFF, None)
|
||||
# The native label setter truncates NSString names at this UTF-16 extent.
|
||||
HOPPER_SYMBOL_NAME_UTF16_UNITS = 1024
|
||||
# Request JSON payload budget in bytes, excluding the trailing LF.
|
||||
# REA_BRIDGE_MAX_REQUEST_BYTES selects it. Unset stays 16777216.
|
||||
# 7464 is the authenticated Cutter frame of the largest bounded symbol name.
|
||||
# Cutter's 64-receipt recovery status is smaller with a null path. A path
|
||||
# that does not fit is null, not an older descriptor path.
|
||||
# 536870887 is Node's 536870888-character string ceiling minus the framing LF.
|
||||
# The ceiling is the encoded payload; JSON expansion is measured before it.
|
||||
# Encoded JSON allowance, in bytes, shared by one instruction list, one
|
||||
# native-caller list, and one caller-scan page. This release has no runtime override.
|
||||
CALLER_SCAN_ALLOWANCE_BYTES = 512 * 1024
|
||||
_BRIDGE_MAX_REQUEST_BYTES_SETTING = "REA_BRIDGE_MAX_REQUEST_BYTES"
|
||||
DEFAULT_BRIDGE_MAX_REQUEST_BYTES = 16 * 1024 * 1024
|
||||
MINIMUM_BRIDGE_MAX_REQUEST_BYTES = 7464
|
||||
MAXIMUM_BRIDGE_MAX_REQUEST_BYTES = 536870887
|
||||
|
||||
|
||||
def _bridge_max_request_bytes_constraint():
|
||||
return (
|
||||
"REA_BRIDGE_MAX_REQUEST_BYTES must be a positive decimal integer "
|
||||
"from %d to %d bytes; the default is %d bytes (16 MiB). "
|
||||
"A value below %d bytes cannot carry the largest admitted "
|
||||
"symbol-name command, and a larger value is refused so the encoded "
|
||||
"payload plus its framing LF stays one string under Node's "
|
||||
"single-string ceiling."
|
||||
% (
|
||||
MINIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MAXIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
DEFAULT_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MINIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _parse_bridge_max_request_bytes(raw):
|
||||
"""Refuse a non-decimal or out-of-range ceiling. Never fall back."""
|
||||
constraint = _bridge_max_request_bytes_constraint()
|
||||
if isinstance(raw, bool):
|
||||
raise RuntimeError(constraint)
|
||||
if isinstance(raw, int):
|
||||
value = raw
|
||||
elif isinstance(raw, str):
|
||||
# ASCII digits only. str.isdigit() accepts numerals such as U+0668
|
||||
# that the TypeScript setting rejects.
|
||||
if re.fullmatch(r"[1-9][0-9]*", raw) is None:
|
||||
raise RuntimeError(constraint)
|
||||
value = int(raw)
|
||||
else:
|
||||
raise RuntimeError(constraint)
|
||||
if (
|
||||
value < MINIMUM_BRIDGE_MAX_REQUEST_BYTES
|
||||
or value > MAXIMUM_BRIDGE_MAX_REQUEST_BYTES
|
||||
):
|
||||
raise RuntimeError(constraint)
|
||||
return value
|
||||
|
||||
|
||||
def _resolve_bridge_max_request_bytes():
|
||||
"""Prefer the bootstrap injection, then the process environment, then 16 MiB."""
|
||||
injected = globals().get(_BRIDGE_MAX_REQUEST_BYTES_SETTING)
|
||||
if injected is not None:
|
||||
return _parse_bridge_max_request_bytes(injected), "setting"
|
||||
raw = os.environ.get(_BRIDGE_MAX_REQUEST_BYTES_SETTING)
|
||||
if raw is None:
|
||||
return DEFAULT_BRIDGE_MAX_REQUEST_BYTES, "default"
|
||||
return _parse_bridge_max_request_bytes(raw), "setting"
|
||||
|
||||
|
||||
MAX_REQUEST_BYTES, BRIDGE_MAX_REQUEST_BYTES_SOURCE = (
|
||||
_resolve_bridge_max_request_bytes()
|
||||
)
|
||||
|
||||
|
||||
def _bridge_max_request_bytes_recovery():
|
||||
return (
|
||||
"The effective REA_BRIDGE_MAX_REQUEST_BYTES value is %d bytes "
|
||||
"(source %s; default %d, floor %d, maximum %d). "
|
||||
"Set REA_BRIDGE_MAX_REQUEST_BYTES in the environment that starts REA "
|
||||
"to a decimal integer from %d to %d, then restart REA. "
|
||||
"Restart Hopper or Cutter from that same environment so the bridge "
|
||||
"uses this ceiling."
|
||||
% (
|
||||
MAX_REQUEST_BYTES,
|
||||
BRIDGE_MAX_REQUEST_BYTES_SOURCE,
|
||||
DEFAULT_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MINIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MAXIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MINIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MAXIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
)
|
||||
)
|
||||
_rea_session_document = None
|
||||
_search_inventory_cache = {}
|
||||
_pseudocode_cache = {}
|
||||
@@ -235,9 +144,6 @@ EXHAUSTIVE_ANALYSIS_METHODS = frozenset(
|
||||
"procedure_assembly",
|
||||
"procedure_callees",
|
||||
"procedure_callers",
|
||||
"rea_caller_scan_page",
|
||||
"rea_caller_scan_target",
|
||||
"rea_caller_base",
|
||||
"procedure_info",
|
||||
"procedure_pseudo_code",
|
||||
"procedure_references",
|
||||
@@ -330,27 +236,11 @@ def _name_matches(document, address, name):
|
||||
return observed == name or (not name and observed is None)
|
||||
|
||||
|
||||
def _bookmark_api(document, name):
|
||||
method = getattr(document, name, None)
|
||||
if not callable(method):
|
||||
raise CapabilityUnavailableError(
|
||||
"Hopper's public Python API does not expose %s" % name
|
||||
)
|
||||
return method
|
||||
|
||||
|
||||
def _bookmark_name(document, address):
|
||||
return _bookmark_api(document, "getBookmarkName")(address)
|
||||
|
||||
|
||||
def _address_selection(document, value=None):
|
||||
"""Own the coordinate and whether it was selected by symbol or address."""
|
||||
def _address(document, value=None):
|
||||
"""Keep explicit hexadecimal coordinates distinct from symbol names."""
|
||||
if value is None:
|
||||
return document.getCurrentAddress(), "cursor"
|
||||
return document.getCurrentAddress()
|
||||
_api_text(value, "Address")
|
||||
if value.startswith("file-offset:"):
|
||||
raise InvalidRequestError("Hopper does not support file-offset: selectors; select Ghidra for source-file coordinates or use a Hopper analysis address")
|
||||
source = "address"
|
||||
if value.lower().startswith("0x"):
|
||||
try:
|
||||
address = int(value, 16)
|
||||
@@ -361,8 +251,6 @@ def _address_selection(document, value=None):
|
||||
# Prefer an observed symbol, retaining bare-hex compatibility only
|
||||
# when Hopper has no symbol with the supplied name.
|
||||
address = document.getAddressForName(value)
|
||||
if address not in BAD_ADDRESSES:
|
||||
source = "symbol"
|
||||
if address in BAD_ADDRESSES:
|
||||
try:
|
||||
address = int(value, 16)
|
||||
@@ -370,11 +258,7 @@ def _address_selection(document, value=None):
|
||||
raise InvalidRequestError("Unknown Hopper address or name: %s" % value) from error
|
||||
if not 0 <= address < 0xFFFFFFFFFFFFFFFF:
|
||||
raise InvalidRequestError("Address is outside Hopper's usable unsigned 64-bit range: %s" % value)
|
||||
return address, source
|
||||
|
||||
|
||||
def _address(document, value=None):
|
||||
return _address_selection(document, value)[0]
|
||||
return address
|
||||
|
||||
|
||||
def _segment(document, address):
|
||||
@@ -385,15 +269,10 @@ def _segment(document, address):
|
||||
|
||||
|
||||
def _procedure(document, value=None):
|
||||
address, source = _address_selection(document, value)
|
||||
address = document.getCurrentAddress() if value is None else _address(document, value)
|
||||
result = _segment(document, address).getProcedureAtAddress(address)
|
||||
if result is None:
|
||||
raise InvalidRequestError("No procedure exists at the requested address")
|
||||
if source == "symbol" and address != result.getEntryPoint():
|
||||
raise InvalidRequestError(
|
||||
"Symbol %s at %s is inside procedure %s at %s; select its entry name/address or use resolve_containing_procedure for explicit containment"
|
||||
% (value, _hex(address), _procedure_name(result), _hex(result.getEntryPoint()))
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -406,7 +285,6 @@ def _procedure_identity(procedure):
|
||||
return {
|
||||
"address": _hex(procedure.getEntryPoint()),
|
||||
"name": _procedure_name(procedure),
|
||||
"symbol": None,
|
||||
"classification": None,
|
||||
"body": {
|
||||
"available": False,
|
||||
@@ -491,30 +369,19 @@ def _instruction_addresses(procedure):
|
||||
return result
|
||||
|
||||
|
||||
def _iter_native_calls(procedure, direction):
|
||||
def _native_calls(procedure, direction):
|
||||
references = procedure.getAllCallees() if direction == "outgoing" else procedure.getAllCallers()
|
||||
kinds = {0: "none", 1: "unknown", 2: "direct", 3: "objective_c"}
|
||||
calls = {}
|
||||
unresolved = []
|
||||
for reference in references:
|
||||
source, target, kind = reference.fromAddress(), reference.toAddress(), reference.type()
|
||||
if source in BAD_ADDRESSES:
|
||||
yield None, None, None
|
||||
continue
|
||||
if target in BAD_ADDRESSES:
|
||||
yield source, None, {"address": _hex(source), "reason": "Native CallReference has no resolved target (type %s)" % kind}
|
||||
unresolved.append({"address": _hex(source), "reason": "Native CallReference has no resolved target (type %s)" % kind})
|
||||
continue
|
||||
yield source, target, {"classification": kinds.get(kind, "unknown"), "provider_type": kind, "provenance": "hopper-public-python-api:CallReference"}
|
||||
|
||||
|
||||
def _native_calls(procedure, direction):
|
||||
calls = {}
|
||||
unresolved = []
|
||||
for source, target, detail in _iter_native_calls(procedure, direction):
|
||||
if detail is None:
|
||||
continue
|
||||
if target is None:
|
||||
unresolved.append(detail)
|
||||
else:
|
||||
calls[(source, target)] = detail
|
||||
calls[(source, target)] = {"classification": kinds.get(kind, "unknown"), "provider_type": kind, "provenance": "hopper-public-python-api:CallReference"}
|
||||
return calls, sorted(unresolved, key=lambda item: int(item["address"], 16))
|
||||
|
||||
|
||||
@@ -564,134 +431,8 @@ def _procedure_map(document):
|
||||
return result
|
||||
|
||||
|
||||
def _caller_target_data(procedure):
|
||||
addresses = []
|
||||
seen = set()
|
||||
size = 0
|
||||
for block in procedure.basicBlockIterator():
|
||||
for address in _block_instruction_addresses(procedure, block):
|
||||
if address in seen:
|
||||
continue
|
||||
encoded = _hex(address)
|
||||
size += len(encoded) + 4
|
||||
if size > CALLER_SCAN_ALLOWANCE_BYTES:
|
||||
return {"target": _procedure_identity(procedure), "instructions": addresses, "instructions_complete": False}
|
||||
addresses.append(encoded)
|
||||
seen.add(address)
|
||||
return {"target": _procedure_identity(procedure), "instructions": addresses, "instructions_complete": True}
|
||||
|
||||
|
||||
def _caller_scan_target(document, params):
|
||||
return _caller_target_data(_procedure(document, params.get("procedure")))
|
||||
|
||||
|
||||
def _caller_base(document, params):
|
||||
procedure = _procedure(document, params.get("procedure"))
|
||||
callers = []
|
||||
size = 0
|
||||
complete = True
|
||||
for item in procedure.getAllCallerProcedures():
|
||||
address = _hex(item.getEntryPoint())
|
||||
size += len(address) + 4
|
||||
if size > CALLER_SCAN_ALLOWANCE_BYTES:
|
||||
complete = False
|
||||
break
|
||||
callers.append(address)
|
||||
return {
|
||||
**_caller_target_data(procedure),
|
||||
"callers": sorted(callers, key=lambda value: int(value, 16)),
|
||||
"callers_complete": complete,
|
||||
}
|
||||
|
||||
|
||||
def _caller_scan_page(document, params):
|
||||
"""Observe forward relationships in one bounded, stateless bridge request."""
|
||||
start = params.get("start")
|
||||
if isinstance(start, bool) or not isinstance(start, int) or start < 0:
|
||||
raise InvalidRequestError("Caller scan start must be a non-negative integer")
|
||||
segments = document.getSegmentsList()
|
||||
counts = [segment.getProcedureCount() for segment in segments]
|
||||
total = sum(counts)
|
||||
end = min(start + 64, total)
|
||||
if start > total:
|
||||
raise InvalidRequestError("Caller scan start exceeds the procedure inventory")
|
||||
items = []
|
||||
item_bytes = 0
|
||||
page_limit = CALLER_SCAN_ALLOWANCE_BYTES
|
||||
oversized = False
|
||||
identities = {}
|
||||
containing = {}
|
||||
|
||||
def identity(procedure):
|
||||
entry = procedure.getEntryPoint()
|
||||
if entry not in identities:
|
||||
identities[entry] = _procedure_identity(procedure)
|
||||
return identities[entry]
|
||||
|
||||
def owner(address):
|
||||
if address not in containing:
|
||||
procedure, _ = _containing_procedure(document, address)
|
||||
containing[address] = identity(procedure) if procedure is not None else None
|
||||
return containing[address]
|
||||
|
||||
segment_start = 0
|
||||
for segment, count in zip(segments, counts):
|
||||
begin, stop = max(start - segment_start, 0), min(end - segment_start, count)
|
||||
segment_start += count
|
||||
for index in range(begin, max(begin, stop)):
|
||||
candidate = segment.getProcedureAtIndex(index)
|
||||
item = {"caller": identity(candidate), "callees": [], "references": []}
|
||||
size = len(json.dumps(item, ensure_ascii=True).encode("utf-8")) + 1
|
||||
remaining = page_limit - item_bytes
|
||||
too_large = size > remaining
|
||||
if not too_large:
|
||||
for callee in candidate.getAllCalleeProcedures():
|
||||
address = _hex(callee.getEntryPoint())
|
||||
size += len(address) + 4
|
||||
if size > remaining:
|
||||
too_large = True
|
||||
break
|
||||
item["callees"].append(address)
|
||||
if not too_large:
|
||||
callee_entries = set(item["callees"])
|
||||
for source, destination, call in _iter_native_calls(candidate, "outgoing"):
|
||||
# Account for every native record, even when its target cannot
|
||||
# be associated with an observed procedure.
|
||||
size += 32
|
||||
if size > remaining:
|
||||
too_large = True
|
||||
break
|
||||
if destination is None:
|
||||
continue
|
||||
source_procedure = item["caller"] if _hex(source) == item["caller"]["address"] else owner(source)
|
||||
target_address = _hex(destination)
|
||||
if target_address in callee_entries:
|
||||
target_entry = target_address
|
||||
else:
|
||||
target_procedure = owner(destination)
|
||||
if target_procedure is None:
|
||||
continue
|
||||
target_entry = target_procedure["address"]
|
||||
edge = {
|
||||
"source_address": _hex(source), "target_address": _hex(destination),
|
||||
"source_procedure": "caller" if source_procedure is not None and source_procedure["address"] == item["caller"]["address"] else source_procedure,
|
||||
"target_procedure": target_entry,
|
||||
"call": call,
|
||||
}
|
||||
size += len(json.dumps(edge, ensure_ascii=True).encode("utf-8")) + 1
|
||||
if size > remaining:
|
||||
too_large = True
|
||||
break
|
||||
item["references"].append(edge)
|
||||
if too_large:
|
||||
oversized = not items
|
||||
break
|
||||
items.append(item)
|
||||
item_bytes += size
|
||||
else:
|
||||
continue
|
||||
break
|
||||
return {"start": start, "next": start + len(items), "total": total, "items": items, "oversized": oversized}
|
||||
def _strings(document):
|
||||
return {address: record["value"] for address, record in _string_inventory(document).items()}
|
||||
|
||||
|
||||
def _string_record(segment, address, display):
|
||||
@@ -780,7 +521,7 @@ def _search_inventory(document, kind):
|
||||
if kind == "procedure":
|
||||
values = _procedure_map(document)
|
||||
elif kind == "string":
|
||||
values = _string_inventory(document)
|
||||
values = _strings(document)
|
||||
elif kind == "name":
|
||||
values = {_hex(address): value for address, value in _name_map(document).items()}
|
||||
else:
|
||||
@@ -807,17 +548,23 @@ def _search_results(document, kind, params):
|
||||
else:
|
||||
raise CapabilityUnavailableError("Regex matching requires REA's supervised worker; the native bridge only executes literal searches")
|
||||
|
||||
selected = []
|
||||
for item in _search_inventory(document, kind):
|
||||
if not matches(item[1]):
|
||||
continue
|
||||
selected.append(item)
|
||||
if not selected:
|
||||
return []
|
||||
if kind == "string":
|
||||
return [
|
||||
{"address": address, **record}
|
||||
for address, record in _search_inventory(document, kind)
|
||||
if matches(record["value"])
|
||||
]
|
||||
records = _string_inventory(document)
|
||||
return [{"address": address, **records[address]} for address, _ in selected]
|
||||
return [
|
||||
{"address": address, "value": value}
|
||||
for address, value in _search_inventory(document, kind)
|
||||
if matches(value)
|
||||
]
|
||||
{
|
||||
"address": address,
|
||||
"value": value,
|
||||
}
|
||||
for address, value in selected
|
||||
]
|
||||
|
||||
|
||||
def _unavailable(reason):
|
||||
@@ -994,7 +741,6 @@ def _dispatch(method, params):
|
||||
"version": "1.0.0",
|
||||
"run_id": REA_RUN_ID,
|
||||
"process_id": os.getpid(),
|
||||
"max_request_bytes": MAX_REQUEST_BYTES,
|
||||
}
|
||||
if method in ("shutdown", "shutdown_document"):
|
||||
document = _session_document()
|
||||
@@ -1034,15 +780,6 @@ def _dispatch(method, params):
|
||||
if method in EXHAUSTIVE_ANALYSIS_METHODS:
|
||||
_api().require_analysis_complete(document, method)
|
||||
|
||||
if method == "rea_caller_scan_target":
|
||||
return _caller_scan_target(document, params)
|
||||
|
||||
if method == "rea_caller_base":
|
||||
return _caller_base(document, params)
|
||||
|
||||
if method == "rea_caller_scan_page":
|
||||
return _caller_scan_page(document, params)
|
||||
|
||||
if method == "analyze_function":
|
||||
return _analyze_function(document, params)
|
||||
if method == "read_function_instructions":
|
||||
@@ -1227,7 +964,6 @@ def _dispatch(method, params):
|
||||
length = sum(max(0, _basic_block_end(procedure, block) - block.getStartingAddress()) for block in blocks)
|
||||
return {
|
||||
"name": _procedure_name(procedure),
|
||||
"symbol": None,
|
||||
"entrypoint": _hex(procedure.getEntryPoint()),
|
||||
"basicblock_count": procedure.getBasicBlockCount(),
|
||||
"length": length,
|
||||
@@ -1268,41 +1004,23 @@ def _dispatch(method, params):
|
||||
observed = getter(address)
|
||||
return observed == comment or (comment == "" and observed is None)
|
||||
if method == "list_bookmarks":
|
||||
return [{"address": _hex(item), "name": _bookmark_name(document, item)} for item in _bookmark_api(document, "getBookmarks")()]
|
||||
return [{"address": _hex(item), "name": document.getBookmarkName(item)} for item in document.getBookmarks()]
|
||||
if method == "set_bookmark":
|
||||
address = _address(document, params.get("address"))
|
||||
_segment(document, address)
|
||||
name = params.get("name")
|
||||
if name is not None:
|
||||
_api_text(name, "Bookmark name")
|
||||
has_bookmark = _bookmark_api(document, "hasBookmarkAtAddress")
|
||||
exists = has_bookmark(address)
|
||||
if name is None and exists:
|
||||
# Omission means preserve the existing bookmark and its name.
|
||||
return True
|
||||
_bookmark_api(document, "getBookmarkName")
|
||||
if name == "" and exists:
|
||||
rename = _bookmark_api(document, "renameBookmarkAtAddress")
|
||||
rename(address, name)
|
||||
else:
|
||||
setter = _bookmark_api(document, "setBookmarkAtAddress")
|
||||
setter(address, name)
|
||||
if not has_bookmark(address):
|
||||
return False
|
||||
observed = _bookmark_name(document, address)
|
||||
if name is None:
|
||||
return observed is None or observed == ""
|
||||
return observed == name or (name == "" and observed is None)
|
||||
document.setBookmarkAtAddress(address, name)
|
||||
return document.hasBookmarkAtAddress(address)
|
||||
if method == "unset_bookmark":
|
||||
address = _address(document, params.get("address"))
|
||||
# Permit removing a legacy orphan bookmark, but do not report success
|
||||
# for an unmapped address with no bookmark to remove.
|
||||
has_bookmark = _bookmark_api(document, "hasBookmarkAtAddress")
|
||||
remove_bookmark = _bookmark_api(document, "removeBookmarkAtAddress")
|
||||
if not has_bookmark(address):
|
||||
if not document.hasBookmarkAtAddress(address):
|
||||
_segment(document, address)
|
||||
remove_bookmark(address)
|
||||
return not has_bookmark(address)
|
||||
document.removeBookmarkAtAddress(address)
|
||||
return not document.hasBookmarkAtAddress(address)
|
||||
raise InvalidRequestError("Unknown bridge method")
|
||||
|
||||
|
||||
@@ -1312,23 +1030,9 @@ def _serve_connection(connection):
|
||||
should_stop = False
|
||||
try:
|
||||
while True:
|
||||
# Bound the read itself. The payload budget excludes LF, so permit
|
||||
# one additional byte for the delimiter while still refusing an
|
||||
# oversized unterminated peer without waiting for EOF.
|
||||
line = file.readline(MAX_REQUEST_BYTES + 1)
|
||||
line = file.readline()
|
||||
if not line:
|
||||
break
|
||||
if not line.endswith(b"\n"):
|
||||
message = (
|
||||
"Bridge request exceeds the %d-byte payload limit. %s"
|
||||
% (MAX_REQUEST_BYTES, _bridge_max_request_bytes_recovery())
|
||||
if len(line) > MAX_REQUEST_BYTES
|
||||
else "Incomplete bridge request frame"
|
||||
)
|
||||
_write_message(file, {"id": 0, "error": _safe_diagnostic(
|
||||
InvalidRequestError(message)
|
||||
)})
|
||||
break
|
||||
request_id = None
|
||||
authenticated = False
|
||||
should_stop = False
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import Foundation
|
||||
import CoreFoundation
|
||||
|
||||
struct AccessibilityValue {
|
||||
let value: Any
|
||||
let gap: String?
|
||||
}
|
||||
|
||||
/** Preserve JSON scalars; report successful reads that JSON cannot represent faithfully. */
|
||||
func captureAccessibilityValue(_ source: Any?) -> AccessibilityValue {
|
||||
guard let source else { return AccessibilityValue(value: NSNull(), gap: nil) }
|
||||
if let text = source as? String { return AccessibilityValue(value: text, gap: nil) }
|
||||
if let number = source as? NSNumber {
|
||||
if CFGetTypeID(number) == CFBooleanGetTypeID() {
|
||||
return AccessibilityValue(value: number.boolValue, gap: nil)
|
||||
}
|
||||
let value = number.doubleValue
|
||||
let encoding = String(cString: number.objCType)
|
||||
let floating = encoding == "f" || encoding == "d"
|
||||
if number is NSDecimalNumber || !value.isFinite || (value == 0 && value.sign == .minus) || (!floating && abs(value) > 9_007_199_254_740_991) {
|
||||
return AccessibilityValue(value: NSNull(), gap: "AXValue number \(number.stringValue) cannot be represented faithfully as a JSON number")
|
||||
}
|
||||
return AccessibilityValue(value: number, gap: nil)
|
||||
}
|
||||
return AccessibilityValue(value: NSNull(), gap: "AXValue type \(type(of: source)) is not a supported JSON scalar")
|
||||
}
|
||||
@@ -43,13 +43,10 @@ func windowBounds(_ element: AXUIElement) -> CGRect? {
|
||||
return CGRect(origin: point, size: size)
|
||||
}
|
||||
func observe(_ request: Request) async throws -> [String: Any] {
|
||||
guard let app = NSRunningApplication(processIdentifier: request.pid) else {
|
||||
try fail("pid-not-application", "LaunchServices does not expose an application for the selected PID; this does not establish that the process is absent")
|
||||
guard let app = NSRunningApplication(processIdentifier: request.pid), let url = app.executableURL else {
|
||||
try fail("target-mismatch", "Selected PID does not expose an application executable URL")
|
||||
}
|
||||
guard let url = app.executableURL else {
|
||||
try fail("application-executable-unavailable", "The selected application does not expose an executable URL; select a PID with an identifiable application executable")
|
||||
}
|
||||
guard let canonicalPointer = realpath(url.path, nil) else { try fail("executable-path-unavailable", "Running executable path cannot be resolved") }
|
||||
guard let canonicalPointer = realpath(url.path, nil) else { try fail("target-mismatch", "Running executable path cannot be resolved") }
|
||||
defer { free(canonicalPointer) }
|
||||
let canonicalExecutable = String(cString: canonicalPointer)
|
||||
guard canonicalExecutable == request.executable else {
|
||||
@@ -65,10 +62,7 @@ func observe(_ request: Request) async throws -> [String: Any] {
|
||||
guard SHA256.hash(data: executableBytes).map({ String(format: "%02x", $0) }).joined() == request.sha256 else {
|
||||
try fail("target-mismatch", "Running executable bytes differ from the active target digest")
|
||||
}
|
||||
// .optionIncludingWindow omits a selected window while it is off screen, which
|
||||
// is not absence: select from every window, and let capture report what it can.
|
||||
guard let windows = (CGWindowListCopyWindowInfo([.optionAll], kCGNullWindowID) as? [[String: Any]])?
|
||||
.filter({ ($0[kCGWindowNumber as String] as? NSNumber)?.uint32Value == request.window_id }),
|
||||
guard let windows = CGWindowListCopyWindowInfo([.optionIncludingWindow], request.window_id) as? [[String: Any]],
|
||||
windows.count == 1, let window = windows.first,
|
||||
(window[kCGWindowOwnerPID as String] as? NSNumber)?.int32Value == request.pid,
|
||||
(window[kCGWindowLayer as String] as? NSNumber)?.intValue == 0,
|
||||
@@ -123,9 +117,7 @@ func observe(_ request: Request) async throws -> [String: Any] {
|
||||
let childCountResult = childCount(element)
|
||||
var actions: CFArray?
|
||||
AXUIElementCopyActionNames(element, &actions)
|
||||
let observedValue = captureAccessibilityValue(attribute(element, kAXValueAttribute))
|
||||
if let gap = observedValue.gap { gaps.append("\(gap) at path \(path)"); truncated = true }
|
||||
nodes.append(["path": path, "role": text(element, kAXRoleAttribute), "title": text(element, kAXTitleAttribute), "value": observedValue.value, "actions": actions as? [String] ?? [], "children_count": childCountResult.value.map { $0 as Any } ?? NSNull()])
|
||||
nodes.append(["path": path, "role": text(element, kAXRoleAttribute), "title": text(element, kAXTitleAttribute), "value": text(element, kAXValueAttribute), "actions": actions as? [String] ?? [], "children_count": childCountResult.value.map { $0 as Any } ?? NSNull()])
|
||||
guard let totalChildren = childCountResult.value else {
|
||||
truncated = true
|
||||
gaps.append("AX child count unavailable at path \(path): AXError \(childCountResult.error ?? -1)")
|
||||
|
||||
@@ -661,15 +661,18 @@ def trace(debugger, config):
|
||||
open(observation_path, "wb")
|
||||
)
|
||||
process = target.Launch(_launch_info(target, config, listener), error)
|
||||
process_created = process.IsValid()
|
||||
if not error.Success() or not process_created:
|
||||
if not error.Success() or not process.IsValid():
|
||||
exit_observed = _retire_failed_launch(
|
||||
process, config, listener, stdout, stderr
|
||||
)
|
||||
return {
|
||||
"status": "launch-error",
|
||||
"error": error.GetCString(),
|
||||
"target_process_created": process_created,
|
||||
**(
|
||||
{"target_process_created": True}
|
||||
if process.IsValid()
|
||||
else {}
|
||||
),
|
||||
**(
|
||||
{"target_process_exit_observed": True}
|
||||
if exit_observed
|
||||
|
||||
@@ -25,73 +25,6 @@ spec.loader.exec_module(tracer)
|
||||
|
||||
|
||||
class NativeTraceBudgetTests(unittest.TestCase):
|
||||
def test_invalid_launch_process_reports_explicit_no_creation(self):
|
||||
class InvalidProcess:
|
||||
def IsValid(self):
|
||||
return False
|
||||
|
||||
def Kill(self):
|
||||
raise AssertionError("an invalid process cannot own target cleanup")
|
||||
|
||||
class LaunchError:
|
||||
def Success(self):
|
||||
return False
|
||||
|
||||
def GetCString(self):
|
||||
return "No such file or directory: fixture/absent"
|
||||
|
||||
class Target:
|
||||
def IsValid(self):
|
||||
return True
|
||||
|
||||
def Launch(self, _launch_info, _error):
|
||||
return InvalidProcess()
|
||||
|
||||
class Debugger:
|
||||
def SetAsync(self, _enabled):
|
||||
pass
|
||||
|
||||
def GetInstanceName(self):
|
||||
return "fixture"
|
||||
|
||||
def CreateTarget(self, *_arguments):
|
||||
return Target()
|
||||
|
||||
class Pipe:
|
||||
def __init__(self, *_arguments):
|
||||
self.closed = False
|
||||
pipes.append(self)
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
def close(self, expected_exit=False):
|
||||
self.closed = True
|
||||
|
||||
pipes = []
|
||||
debugger_api = types.SimpleNamespace(SetInternalVariable=lambda *_arguments: None)
|
||||
with patch.object(tracer.lldb, "SBDebugger", debugger_api, create=True), patch.object(
|
||||
tracer.lldb, "SBError", LaunchError, create=True
|
||||
), patch.object(tracer.lldb, "SBListener", lambda _name: object(), create=True), patch.object(
|
||||
tracer, "_hash_file", return_value="expected"
|
||||
), patch.object(tracer, "_file_identity", return_value=(1, 2)), patch.object(
|
||||
tracer, "_module_identity", return_value=("fixture", (1, 2), "fixture-uuid")
|
||||
), patch.object(tracer, "_launch_info", return_value=object()), patch.object(
|
||||
tracer, "_OutputPipe", Pipe
|
||||
):
|
||||
result = tracer.trace(Debugger(), {
|
||||
"executable": "fixture", "architecture": "arm64", "expected_sha256": "expected",
|
||||
"breakpoints": [], "working_directory": "fixture/absent",
|
||||
"stdout_path": "stdout", "stdout_capture_path": "stdout-capture",
|
||||
"stderr_path": "stderr", "stderr_capture_path": "stderr-capture",
|
||||
})
|
||||
self.assertEqual(result["status"], "launch-error")
|
||||
self.assertEqual(result["error"], "No such file or directory: fixture/absent")
|
||||
self.assertIs(result["target_process_created"], False)
|
||||
self.assertNotIn("target_process_exit_observed", result)
|
||||
self.assertEqual(len(pipes), 2)
|
||||
self.assertTrue(all(pipe.closed for pipe in pipes))
|
||||
|
||||
def test_failed_launch_does_not_infer_exit_from_a_failed_kill(self):
|
||||
class Process:
|
||||
def IsValid(self):
|
||||
|
||||
@@ -12,32 +12,16 @@ struct ProcessIdentityObservation: Encodable {
|
||||
let pid: Int32
|
||||
let state: String
|
||||
let identity: String?
|
||||
let effective_uid: UInt32?
|
||||
let lifecycle: String?
|
||||
let relation: ProcessRelationObservation?
|
||||
/** Display-only kernel accounting name (p_comm, at most MAXCOMLEN bytes). */
|
||||
let name: String?
|
||||
let reason: String?
|
||||
}
|
||||
|
||||
struct ProcessRelationObservation: Encodable {
|
||||
let state: String
|
||||
let parent_pid: Int32
|
||||
let process_group_id: Int32
|
||||
}
|
||||
|
||||
struct ProcessMetadata {
|
||||
private struct ProcessMetadata {
|
||||
let identity: String
|
||||
let effectiveUid: UInt32
|
||||
let lifecycle: String
|
||||
let parentPid: Int32
|
||||
let processGroupId: Int32
|
||||
let pointerSize: Int
|
||||
let name: String
|
||||
}
|
||||
|
||||
struct ProcessSnapshot {
|
||||
let metadata: [Int32: ProcessMetadata]
|
||||
fileprivate let metadata: [Int32: ProcessMetadata]
|
||||
let failure: String?
|
||||
}
|
||||
|
||||
@@ -91,12 +75,7 @@ func readProcessSnapshot(
|
||||
process.kp_proc.p_pid,
|
||||
ProcessMetadata(
|
||||
identity: "\(start.tv_sec):\(start.tv_usec)",
|
||||
effectiveUid: process.kp_eproc.e_ucred.cr_uid,
|
||||
lifecycle: process.kp_proc.p_stat == SZOMB ? "zombie" : "non-zombie",
|
||||
parentPid: process.kp_eproc.e_ppid,
|
||||
processGroupId: process.kp_eproc.e_pgid,
|
||||
pointerSize: process.kp_proc.p_flag & P_LP64 != 0 ? 8 : 4,
|
||||
name: processName(process)
|
||||
pointerSize: process.kp_proc.p_flag & P_LP64 != 0 ? 8 : 4
|
||||
)
|
||||
)
|
||||
},
|
||||
@@ -107,88 +86,25 @@ func readProcessSnapshot(
|
||||
return ProcessSnapshot(metadata: [:], failure: "process_table_failed_\(ENOMEM)")
|
||||
}
|
||||
|
||||
/** Decode the NUL-terminated accounting name; invalid UTF-8 is replaced for display. */
|
||||
private func processName(_ process: kinfo_proc) -> String {
|
||||
withUnsafeBytes(of: process.kp_proc.p_comm) { bytes in
|
||||
String(decoding: bytes.prefix { $0 != 0 }, as: UTF8.self)
|
||||
}
|
||||
}
|
||||
|
||||
func readProcessIdentities(pids: [Int32]) -> [ProcessIdentityObservation] {
|
||||
readProcessIdentities(pids: pids, snapshot: readProcessSnapshot())
|
||||
}
|
||||
|
||||
func readProcessIdentities(
|
||||
pids: [Int32],
|
||||
snapshot: ProcessSnapshot
|
||||
) -> [ProcessIdentityObservation] {
|
||||
let snapshot = readProcessSnapshot()
|
||||
return pids.map { pid in
|
||||
guard pid > 0 else {
|
||||
return unavailableIdentity(pid: pid, reason: "invalid_process_id")
|
||||
}
|
||||
guard let metadata = snapshot.metadata[pid] else {
|
||||
if let failure = snapshot.failure {
|
||||
return unavailableIdentity(pid: pid, reason: failure)
|
||||
}
|
||||
// This records absence from this successful KERN_PROC_ALL snapshot only.
|
||||
// Unlike the Linux procfs path, it has no PID-specific kill(0)
|
||||
// corroboration; callers must not treat it as universal or future exit
|
||||
// proof.
|
||||
return ProcessIdentityObservation(
|
||||
return unavailableIdentity(
|
||||
pid: pid,
|
||||
state: "absent",
|
||||
identity: nil,
|
||||
effective_uid: nil,
|
||||
lifecycle: nil,
|
||||
relation: nil,
|
||||
name: nil,
|
||||
reason: "not-in-snapshot"
|
||||
reason: snapshot.failure ?? "process_not_in_snapshot"
|
||||
)
|
||||
}
|
||||
return ProcessIdentityObservation(
|
||||
pid: pid,
|
||||
state: "readable",
|
||||
identity: metadata.identity,
|
||||
effective_uid: metadata.effectiveUid,
|
||||
lifecycle: metadata.lifecycle,
|
||||
relation: ProcessRelationObservation(
|
||||
state: "observed",
|
||||
parent_pid: metadata.parentPid,
|
||||
process_group_id: metadata.processGroupId
|
||||
),
|
||||
name: metadata.name,
|
||||
reason: nil
|
||||
)
|
||||
return ProcessIdentityObservation(pid: pid, state: "readable", identity: metadata.identity, reason: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func unavailableIdentity(pid: Int32, reason: String) -> ProcessIdentityObservation {
|
||||
ProcessIdentityObservation(
|
||||
pid: pid,
|
||||
state: "unavailable",
|
||||
identity: nil,
|
||||
effective_uid: nil,
|
||||
lifecycle: nil,
|
||||
relation: nil,
|
||||
name: nil,
|
||||
reason: reason
|
||||
)
|
||||
}
|
||||
|
||||
enum ProcargsMalformedStage: String, CaseIterable {
|
||||
case header
|
||||
case argumentCount = "argument_count"
|
||||
case executablePath = "executable_path"
|
||||
case pointerSize = "pointer_size"
|
||||
case pathPadding = "path_padding"
|
||||
case arguments
|
||||
case terminator
|
||||
case trailingEntries = "trailing_entries"
|
||||
ProcessIdentityObservation(pid: pid, state: "unavailable", identity: nil, reason: reason)
|
||||
}
|
||||
|
||||
enum RunTokenReadError: Error {
|
||||
case invalidBuffer
|
||||
case invalidBufferAt(ProcargsMalformedStage)
|
||||
case duplicateToken
|
||||
case invalidTokenEncoding
|
||||
case environmentUnavailable
|
||||
@@ -228,22 +144,6 @@ func processRunTokenObservation(
|
||||
parse: Result<ParsedRunToken, RunTokenReadError>,
|
||||
codeSigningFlags: () -> UInt32?
|
||||
) -> RunTokenObservation {
|
||||
// Native inspection restrictions apply even when argv is incomplete or
|
||||
// overwritten, before parsing reaches the environment. Keep this fact at
|
||||
// the native boundary; cleanup still decides whether the process is related.
|
||||
func withheldEnvironment() -> RunTokenObservation? {
|
||||
guard let flags = codeSigningFlags() else { return nil }
|
||||
if (flags & csPlatformBinary) != 0 {
|
||||
return unavailable(pid: pid, reason: "platform_binary_environment_withheld")
|
||||
}
|
||||
if (flags & csRestrict) != 0 {
|
||||
return unavailable(pid: pid, reason: "restricted_process_environment_withheld")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func unavailableEnvironment(_ reason: String) -> RunTokenObservation {
|
||||
withheldEnvironment() ?? unavailable(pid: pid, reason: reason)
|
||||
}
|
||||
switch parse {
|
||||
case .success(let parsed):
|
||||
if let runId = parsed.runId {
|
||||
@@ -254,25 +154,38 @@ func processRunTokenObservation(
|
||||
if parsed.environmentEntryCount > 0 {
|
||||
return RunTokenObservation(pid: pid, state: "readable", run_id: nil, reason: nil)
|
||||
}
|
||||
if let withheld = withheldEnvironment() { return withheld }
|
||||
let flags = codeSigningFlags()
|
||||
if let flags, (flags & csPlatformBinary) != 0 {
|
||||
return unavailable(pid: pid, reason: "platform_binary_environment_withheld")
|
||||
}
|
||||
if let flags, (flags & csRestrict) != 0 {
|
||||
return unavailable(pid: pid, reason: "restricted_process_environment_withheld")
|
||||
}
|
||||
return RunTokenObservation(pid: pid, state: "readable", run_id: nil, reason: nil)
|
||||
case .failure(let failure):
|
||||
switch failure {
|
||||
case .environmentUnavailable:
|
||||
return unavailableEnvironment("environment_unavailable")
|
||||
let flags = codeSigningFlags()
|
||||
if let flags, (flags & csPlatformBinary) != 0 {
|
||||
return unavailable(pid: pid, reason: "platform_binary_environment_withheld")
|
||||
}
|
||||
if let flags, (flags & csRestrict) != 0 {
|
||||
return unavailable(pid: pid, reason: "restricted_process_environment_withheld")
|
||||
}
|
||||
return unavailable(pid: pid, reason: "environment_unavailable")
|
||||
case .ambiguousEnvironmentBoundary:
|
||||
return unavailableEnvironment("ambiguous_environment_boundary")
|
||||
// These failures already establish that token bytes were available.
|
||||
if let flags = codeSigningFlags(), (flags & csRestrict) != 0 {
|
||||
return unavailable(pid: pid, reason: "restricted_process_environment_withheld")
|
||||
}
|
||||
return unavailable(pid: pid, reason: "ambiguous_environment_boundary")
|
||||
case .duplicateToken:
|
||||
return unavailable(pid: pid, reason: "duplicate_run_token")
|
||||
case .invalidTokenEncoding:
|
||||
return unavailable(pid: pid, reason: "invalid_run_token_encoding")
|
||||
case .appleVectorUnavailable:
|
||||
return unavailableEnvironment("apple_vector_unavailable")
|
||||
return unavailable(pid: pid, reason: "apple_vector_unavailable")
|
||||
case .invalidBuffer:
|
||||
return unavailableEnvironment("malformed_procargs")
|
||||
case .invalidBufferAt(let stage):
|
||||
return unavailableEnvironment("malformed_procargs_\(stage.rawValue)")
|
||||
return unavailable(pid: pid, reason: "malformed_procargs")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,30 +244,27 @@ func parseProcessEnvironment(
|
||||
in bytes: [UInt8],
|
||||
pointerSize: Int = MemoryLayout<UnsafeRawPointer>.size
|
||||
) throws -> ParsedRunToken {
|
||||
guard bytes.count >= MemoryLayout<Int32>.size else { throw RunTokenReadError.invalidBufferAt(.header) }
|
||||
guard bytes.count >= MemoryLayout<Int32>.size else { throw RunTokenReadError.invalidBuffer }
|
||||
let argc = bytes.withUnsafeBytes { $0.loadUnaligned(as: Int32.self).littleEndian }
|
||||
guard argc >= 0, Int(argc) <= bytes.count else { throw RunTokenReadError.invalidBufferAt(.argumentCount) }
|
||||
guard argc >= 0, Int(argc) <= bytes.count else { throw RunTokenReadError.invalidBuffer }
|
||||
var offset = MemoryLayout<Int32>.size
|
||||
guard let executablePath = readString(in: bytes, offset: &offset) else {
|
||||
throw RunTokenReadError.invalidBufferAt(.executablePath)
|
||||
throw RunTokenReadError.invalidBuffer
|
||||
}
|
||||
guard pointerSize == 4 || pointerSize == 8 else { throw RunTokenReadError.invalidBufferAt(.pointerSize) }
|
||||
guard pointerSize == 4 || pointerSize == 8 else { throw RunTokenReadError.invalidBuffer }
|
||||
let pathRecordSize = Array("executable_path=".utf8).count + executablePath.count + 1
|
||||
let padding = (pointerSize - pathRecordSize % pointerSize) % pointerSize
|
||||
guard offset + padding <= bytes.count,
|
||||
bytes[offset..<(offset + padding)].allSatisfy({ $0 == 0 }) else {
|
||||
throw RunTokenReadError.invalidBufferAt(.pathPadding)
|
||||
throw RunTokenReadError.invalidBuffer
|
||||
}
|
||||
offset += padding
|
||||
let tokenPrefix = Array("REA_PROCESS_RUN_ID=".utf8)
|
||||
var tokenInArgument = false
|
||||
for _ in 0..<argc {
|
||||
guard let argument = readString(in: bytes, offset: &offset, allowEmpty: true) else {
|
||||
throw RunTokenReadError.invalidBufferAt(.arguments)
|
||||
guard readString(in: bytes, offset: &offset, allowEmpty: true) != nil else {
|
||||
throw RunTokenReadError.invalidBuffer
|
||||
}
|
||||
tokenInArgument = tokenInArgument || argument.starts(with: tokenPrefix)
|
||||
}
|
||||
guard bytes.last == 0 else { throw RunTokenReadError.invalidBufferAt(.terminator) }
|
||||
guard bytes.last == 0 else { throw RunTokenReadError.invalidBuffer }
|
||||
if offset == bytes.count {
|
||||
throw RunTokenReadError.environmentUnavailable
|
||||
}
|
||||
@@ -372,7 +282,7 @@ func parseProcessEnvironment(
|
||||
continue
|
||||
}
|
||||
guard let entry = readString(in: bytes, offset: &offset) else {
|
||||
throw RunTokenReadError.invalidBufferAt(.trailingEntries)
|
||||
throw RunTokenReadError.invalidBuffer
|
||||
}
|
||||
trailingEntries.append(entry)
|
||||
}
|
||||
@@ -393,28 +303,22 @@ func parseProcessEnvironment(
|
||||
throw RunTokenReadError.appleVectorUnavailable
|
||||
}
|
||||
if trailingEntries[appleVectorStart...].contains(where: {
|
||||
$0.starts(with: tokenPrefix)
|
||||
$0.starts(with: Array("REA_PROCESS_RUN_ID=".utf8))
|
||||
}) {
|
||||
throw RunTokenReadError.ambiguousEnvironmentBoundary
|
||||
}
|
||||
let environmentEntries = trailingEntries[..<appleVectorStart]
|
||||
var found: String?
|
||||
for entry in environmentEntries {
|
||||
if entry.starts(with: tokenPrefix) {
|
||||
if entry.starts(with: Array("REA_PROCESS_RUN_ID=".utf8)) {
|
||||
guard found == nil else { throw RunTokenReadError.duplicateToken }
|
||||
let value = entry.dropFirst(tokenPrefix.count)
|
||||
let value = entry.dropFirst("REA_PROCESS_RUN_ID=".utf8.count)
|
||||
guard let decoded = String(bytes: value, encoding: .utf8) else {
|
||||
throw RunTokenReadError.invalidTokenEncoding
|
||||
}
|
||||
found = decoded
|
||||
}
|
||||
}
|
||||
// A changed process title can overwrite the original argument boundaries,
|
||||
// causing a real environment entry to be consumed as an argument. A real
|
||||
// argument can have this form too, so it proves neither ownership nor absence.
|
||||
if tokenInArgument {
|
||||
throw RunTokenReadError.ambiguousEnvironmentBoundary
|
||||
}
|
||||
return ParsedRunToken(runId: found, environmentEntryCount: environmentEntries.count)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,8 @@ def inspect(request):
|
||||
root = Path(os.environ["REA_CORE_CONTEXT_REQUEST"]).parent
|
||||
empty = root / "no-libraries"
|
||||
empty.mkdir(mode=0o700)
|
||||
for command in ["set auto-load off", "set debuginfod enabled off", "set auto-solib-add off", "set history save off", "set pagination off", "set target-file-system-kind unix",
|
||||
"set sysroot ./no-libraries", "set solib-search-path ./no-libraries",
|
||||
"set debug-file-directory ./no-libraries"]:
|
||||
for command in ["set auto-load off", "set debuginfod enabled off", "set auto-solib-add off", "set history save off", "set pagination off",
|
||||
"set sysroot ./no-libraries", "set solib-search-path ./no-libraries"]:
|
||||
gdb.execute(command, to_string=True)
|
||||
entry = request["gdbinit"]
|
||||
try:
|
||||
@@ -39,19 +38,7 @@ def inspect(request):
|
||||
# relative filename in the owned cwd so no selected path becomes GDB syntax.
|
||||
if Path(request["snapshot_path"]) != root.parent / "object.snapshot":
|
||||
raise ContextFailure("decoder", "Core context request did not select the shared owned snapshot.")
|
||||
# GDB 16 searches AT_EXECFN/NT_FILE outside sysroot when no executable BFD
|
||||
# is selected. Occupy that slot with the already running, owned debugger;
|
||||
# exec-file loads no symbols and never runs it. Clear it before any query.
|
||||
# /proc/self/exe identifies this process, not the core's historical PID.
|
||||
architecture = {"x86_64-little-endian": "i386:x86-64", "aarch64-little-endian": "aarch64"}.get(request["architecture"])
|
||||
if architecture is None:
|
||||
raise ContextFailure("unsupported", "Unsupported recorded core architecture.")
|
||||
gdb.execute("exec-file /proc/self/exe", to_string=True)
|
||||
# The placeholder must not select the core's architecture. Reuse the
|
||||
# decoder's validated profile, including for cross-architecture GDB builds.
|
||||
gdb.execute("set architecture " + architecture, to_string=True)
|
||||
gdb.execute("core-file ../object.snapshot", to_string=True)
|
||||
gdb.execute("file", to_string=True)
|
||||
inferior = gdb.selected_inferior()
|
||||
if inferior.connection is None or inferior.connection.type != "core" or gdb.current_progspace().filename is not None:
|
||||
raise ContextFailure("decoder", "Requested core-only boundary was not established; no mapping query performed.")
|
||||
@@ -95,7 +82,7 @@ def main():
|
||||
try:
|
||||
encoded = json.dumps(reply, ensure_ascii=True, allow_nan=False).encode("utf-8")
|
||||
if len(encoded) > OUTPUT_BYTES:
|
||||
encoded = json.dumps({"ok": False, "reason": "output-limit", "message": "Complete debugger context exceeds the 64 MiB reply budget."}).encode("utf-8")
|
||||
encoded = json.dumps({"ok": False, "reason": "output-limit", "message": "Complete debugger context exceeds64 MiB reply budget."}).encode("utf-8")
|
||||
descriptor = os.open(request["reply_path"], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
with os.fdopen(descriptor, "wb") as handle:
|
||||
handle.write(encoded)
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
"""Producer regressions with controlled GDB state, not native debugger qualification."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import runpy
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
BRIDGE = Path(__file__).with_name("core_context.py")
|
||||
|
||||
|
||||
class CoreContextTests(unittest.TestCase):
|
||||
def inspect(self, failure=None, connection="core", symbols=None, architecture="x86_64-little-endian", objfile="recorded-executable"):
|
||||
commands, reads, queries = [], [], []
|
||||
state = {"executable": None, "symbols": None, "connection": None}
|
||||
gdb = types.ModuleType("gdb")
|
||||
gdb.VERSION = "16.3"
|
||||
gdb.selected_inferior = lambda: types.SimpleNamespace(
|
||||
connection=state["connection"], pid=123)
|
||||
gdb.current_progspace = lambda: types.SimpleNamespace(filename=state["symbols"])
|
||||
|
||||
def execute(command, to_string):
|
||||
self.assertTrue(to_string)
|
||||
commands.append(command)
|
||||
if command == failure:
|
||||
raise RuntimeError("controlled command failure")
|
||||
if command == "exec-file /proc/self/exe":
|
||||
state["executable"] = "/proc/self/exe"
|
||||
elif command == "core-file ../object.snapshot":
|
||||
if state["executable"] is None:
|
||||
reads.append("recorded-executable")
|
||||
state["executable"] = state["symbols"] = "recorded-executable"
|
||||
state["connection"] = types.SimpleNamespace(type=connection)
|
||||
elif command == "file":
|
||||
state["executable"] = None
|
||||
state["symbols"] = symbols
|
||||
|
||||
gdb.execute = execute
|
||||
pwndbg = types.ModuleType("pwndbg")
|
||||
pwndbg.aglib = types.ModuleType("pwndbg.aglib")
|
||||
pwndbg.aglib.vmmap = types.ModuleType("pwndbg.aglib.vmmap")
|
||||
pwndbg.lib = types.ModuleType("pwndbg.lib")
|
||||
pwndbg.lib.version = types.ModuleType("pwndbg.lib.version")
|
||||
pwndbg.lib.version.__version__ = "2026.09.15"
|
||||
|
||||
def maps():
|
||||
self.assertIsNone(state["executable"], "Helper executable must be discarded before mapping queries")
|
||||
queries.append("maps")
|
||||
return [types.SimpleNamespace(start=4096, end=8192, offset=0, flags=5, objfile=objfile)]
|
||||
|
||||
pwndbg.aglib.vmmap.get = maps
|
||||
modules = {module.__name__: module for module in [
|
||||
gdb, pwndbg, pwndbg.aglib, pwndbg.aglib.vmmap, pwndbg.lib, pwndbg.lib.version]}
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
stage = root / "debugger"
|
||||
stage.mkdir()
|
||||
plugin = root / "gdbinit.py"
|
||||
plugin.write_text("")
|
||||
request = stage / "request.json"
|
||||
reply = stage / "reply.json"
|
||||
request.write_text(json.dumps({"gdbinit": str(plugin), "architecture": architecture, "snapshot_path": str(root / "object.snapshot"), "reply_path": str(reply)}))
|
||||
with patch.dict(sys.modules, modules), patch.dict(os.environ, REA_CORE_CONTEXT_REQUEST=str(request)):
|
||||
runpy.run_path(str(BRIDGE))
|
||||
result = json.loads(reply.read_text())
|
||||
return result, commands, reads, queries
|
||||
|
||||
def test_core_load_never_opens_recorded_executable_and_queries_only_after_discard(self):
|
||||
result, commands, reads, queries = self.inspect()
|
||||
self.assertTrue(result["ok"], result)
|
||||
self.assertEqual(reads, [])
|
||||
self.assertEqual(queries, ["maps"])
|
||||
self.assertEqual(commands[-4:], ["exec-file /proc/self/exe", "set architecture i386:x86-64", "core-file ../object.snapshot", "file"])
|
||||
self.assertLess(commands.index("set debug-file-directory ./no-libraries"), commands.index("core-file ../object.snapshot"))
|
||||
self.assertIsNone(result["value"]["executable"])
|
||||
self.assertEqual(result["value"]["maps"][0]["current_file_identity"], "unknown")
|
||||
|
||||
def test_failed_guard_core_load_or_discard_never_queries_mappings(self):
|
||||
for command in ["exec-file /proc/self/exe", "set architecture i386:x86-64", "core-file ../object.snapshot", "file"]:
|
||||
with self.subTest(command=command):
|
||||
result, commands, reads, queries = self.inspect(failure=command)
|
||||
self.assertFalse(result["ok"])
|
||||
self.assertEqual(reads, [])
|
||||
self.assertEqual(queries, [])
|
||||
if command in ["exec-file /proc/self/exe", "set architecture i386:x86-64"]:
|
||||
self.assertNotIn("core-file ../object.snapshot", commands)
|
||||
|
||||
def test_cross_architecture_uses_the_validated_decoder_profile(self):
|
||||
result, commands, reads, queries = self.inspect(architecture="aarch64-little-endian")
|
||||
self.assertTrue(result["ok"], result)
|
||||
self.assertEqual(reads, [])
|
||||
self.assertEqual(queries, ["maps"])
|
||||
self.assertLess(commands.index("set architecture aarch64"), commands.index("core-file ../object.snapshot"))
|
||||
|
||||
def test_unknown_architecture_refuses_before_loading_core(self):
|
||||
result, commands, reads, queries = self.inspect(architecture="unknown")
|
||||
self.assertFalse(result["ok"])
|
||||
self.assertEqual(result["reason"], "unsupported")
|
||||
self.assertNotIn("core-file ../object.snapshot", commands)
|
||||
self.assertEqual(reads, [])
|
||||
self.assertEqual(queries, [])
|
||||
|
||||
def test_output_limit_message_separates_the_words_and_names_the_limit(self):
|
||||
result, _, reads, queries = self.inspect(objfile="m" * (64 * 1024 * 1024))
|
||||
self.assertFalse(result["ok"])
|
||||
self.assertEqual(result["reason"], "output-limit")
|
||||
self.assertNotIn("exceeds64", result["message"])
|
||||
self.assertEqual(result["message"], "Complete debugger context exceeds the 64 MiB reply budget.")
|
||||
self.assertEqual(reads, [])
|
||||
self.assertEqual(queries, ["maps"])
|
||||
|
||||
def test_wrong_connection_or_remaining_symbols_never_queries_mappings(self):
|
||||
for options in [{"connection": "native"}, {"symbols": "unexpected-symbols"}]:
|
||||
with self.subTest(options=options):
|
||||
result, _, reads, queries = self.inspect(**options)
|
||||
self.assertFalse(result["ok"])
|
||||
self.assertEqual(reads, [])
|
||||
self.assertEqual(queries, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -15,11 +15,6 @@ import sys
|
||||
PROFILE = "[email protected];[email protected];[email protected]"
|
||||
PACKAGES = {"pwntools": "4.15.0", "pyelftools": "0.33", "unicorn": "2.1.2"}
|
||||
OUTPUT_BYTES = 64 * 1024 * 1024
|
||||
# (e_machine, ELF class bits, byte order) -> architecture pwntools must also report.
|
||||
PROFILES = {(62, 64, "little"): "amd64", (183, 64, "little"): "aarch64", (3, 32, "little"): "i386",
|
||||
(40, 32, "little"): "arm", (8, 32, "little"): "mips", (8, 32, "big"): "mips"}
|
||||
PROFILE_SCOPE = ("Layout profile requires ELF64 x86-64, ELF64 AArch64 little-endian, ELF32 i386, "
|
||||
"ELF32 ARM little-endian or ELF32 MIPS little- or big-endian objects.")
|
||||
|
||||
|
||||
class LayoutFailure(Exception):
|
||||
@@ -121,16 +116,13 @@ def inspect_elf(path, cache):
|
||||
raise LayoutFailure("format", "Selected object does not contain an ELF header.")
|
||||
if len(content) < 16:
|
||||
raise LayoutFailure("format", "Selected ELF identification is truncated.")
|
||||
if content[4] not in (1, 2) or content[5] not in (1, 2):
|
||||
raise LayoutFailure("unsupported-target", "Selected ELF declares an unknown class or byte order.")
|
||||
bits, header_size = (32, 52) if content[4] == 1 else (64, 64)
|
||||
order = "little" if content[5] == 1 else "big"
|
||||
if len(content) < header_size:
|
||||
raise LayoutFailure("format", f"Selected ELF{bits} header is truncated.")
|
||||
profile = (int.from_bytes(content[18:20], order), bits, order)
|
||||
if profile not in PROFILES:
|
||||
raise LayoutFailure("unsupported-target", PROFILE_SCOPE)
|
||||
if int.from_bytes(content[16:18], order) not in (1, 2, 3):
|
||||
if content[4] != 2 or content[5] != 1:
|
||||
raise LayoutFailure("unsupported-target", "Initial layout profile requires ELF64 little-endian objects.")
|
||||
if len(content) < 64:
|
||||
raise LayoutFailure("format", "Selected ELF64 header is truncated.")
|
||||
if int.from_bytes(content[18:20], "little") != 62:
|
||||
raise LayoutFailure("unsupported-target", "Initial layout profile requires x86-64 ELF objects.")
|
||||
if int.from_bytes(content[16:18], "little") not in (1, 2, 3):
|
||||
raise LayoutFailure("unsupported-target", "Selected ELF type is outside the EXEC/DYN/REL layout profile; recorded core analysis is separate.")
|
||||
from pwnlib.context import context
|
||||
with context.local(cache_dir=str(cache), log_level="warning"):
|
||||
@@ -140,11 +132,6 @@ def inspect_elf(path, cache):
|
||||
try:
|
||||
headers = ELFFile(io.BytesIO(content))
|
||||
section_count = headers.num_sections()
|
||||
if section_count:
|
||||
location(headers.header.e_shoff, section_count * headers.header.e_shentsize, len(content))
|
||||
segment_count = headers.num_segments()
|
||||
if segment_count:
|
||||
location(headers.header.e_phoff, segment_count * headers.header.e_phentsize, len(content))
|
||||
if headers.header.e_shstrndx == 0xffff and section_count == 0:
|
||||
raise LayoutFailure("format", "ELF extended section-name index requires a declared section-zero header.")
|
||||
raw_name_index = headers.header.e_shstrndx
|
||||
@@ -160,21 +147,9 @@ def inspect_elf(path, cache):
|
||||
if names_header.sh_type != "SHT_STRTAB":
|
||||
raise LayoutFailure("format", f"ELF section-name table {section_name_index} is not SHT_STRTAB.")
|
||||
location(names_header.sh_offset, names_header.sh_size, len(content))
|
||||
except (ELFError, ValueError, OverflowError, AssertionError, IndexError) as error:
|
||||
except ELFError as error:
|
||||
raise LayoutFailure("format", str(error)) from error
|
||||
class LayoutELF(ELF):
|
||||
def search(self, needle, writable=False, executable=False):
|
||||
# This adapter exposes structural facts, GOT/PLT and mitigation
|
||||
# indicators, not kernel config/version discovery. Upstream's
|
||||
# constructor searches virtual memory for those unrelated
|
||||
# heuristics, materializing NOBITS tails and interpreting
|
||||
# ordinary IKCFG_ST data as a compressed kernel config.
|
||||
return iter(())
|
||||
|
||||
def _populate_kernel_version(self):
|
||||
# Kernel banner decoding is outside this report too.
|
||||
return None
|
||||
|
||||
def _get_section_name(self, header):
|
||||
# SHN_UNDEF declares no names; upstream would otherwise read
|
||||
# strings at the inactive section-zero offset.
|
||||
@@ -193,8 +168,8 @@ def inspect_elf(path, cache):
|
||||
if type is None or section.header.sh_type == type)
|
||||
try:
|
||||
image = LayoutELF(str(path), checksec=False)
|
||||
if (image.arch, image.bits, image.endian) != (PROFILES[profile], bits, order):
|
||||
raise LayoutFailure("unsupported-target", PROFILE_SCOPE)
|
||||
if image.arch != "amd64" or image.bits != 64 or image.endian != "little":
|
||||
raise LayoutFailure("unsupported-target", "Initial layout profile requires x86-64 ELF64 little-endian objects.")
|
||||
if image.header.e_type not in ("ET_EXEC", "ET_DYN", "ET_REL"):
|
||||
raise LayoutFailure("unsupported-target", "Selected ELF type is outside the EXEC/DYN/REL layout profile; recorded core analysis is separate.")
|
||||
length = len(content)
|
||||
@@ -216,7 +191,7 @@ def inspect_elf(path, cache):
|
||||
})
|
||||
if h.sh_type in ("SHT_SYMTAB", "SHT_DYNSYM"):
|
||||
if h.sh_entsize < image.structs.Elf_Sym.sizeof():
|
||||
raise LayoutFailure("format", f"ELF symbol entry is smaller than the decoded Elf{bits}_Sym structure.")
|
||||
raise LayoutFailure("format", "ELF symbol entry is smaller than the decoded Elf64_Sym structure.")
|
||||
if h.sh_link >= len(all_sections) or all_sections[h.sh_link].header.sh_type != "SHT_STRTAB":
|
||||
raise LayoutFailure("format", f"ELF symbol table {index} links to invalid string table {h.sh_link}.")
|
||||
strings = all_sections[h.sh_link]
|
||||
@@ -319,7 +294,6 @@ def inspect_elf(path, cache):
|
||||
"Relocatable relocation targets with SHN_UNDEF or inactive SHT_NULL headers retain their reported section index/offset as unknown; they do not establish an associated target section.",
|
||||
"GOT/PLT are derived upstream convenience maps; PLT inference can emulate selected instructions in Unicorn. They do not establish target runtime behavior; aliases may collapse and warnings may indicate incomplete coverage. Their completeness is unknown.",
|
||||
"Mitigations are upstream static heuristics, not runtime protection. ET_DYN does not prove an executable; absent canary symbols do not prove every function unprotected.",
|
||||
"Kernel configuration/version discovery is not performed; these memory-content scans do not supply the reported structural, linkage or mitigation facts.",
|
||||
"This profile inspects ELF EXEC/DYN/REL layout only. It does not analyze recorded cores, resolve loaded libraries, launch the target as a host process or start a debugger.",
|
||||
"Symbol/relocation inventories reflect original section tables. A sectionless image can still report dynamic dependency names; missing tables do not prove absence of dynamic symbols or relocations.",
|
||||
*(["Absent section-name table: section-dependent RELRO/canary heuristics may be incomplete; protection coverage is unknown."] if all_sections and section_name_index == 0 else []),
|
||||
@@ -328,7 +302,7 @@ def inspect_elf(path, cache):
|
||||
}
|
||||
except LayoutFailure:
|
||||
raise
|
||||
except (ELFError, ValueError, OverflowError, AssertionError, IndexError) as error:
|
||||
except (ELFError, ValueError, AssertionError, IndexError) as error:
|
||||
raise LayoutFailure("format", "Selected ELF failed unchanged upstream structural parsing: " + str(error)) from error
|
||||
|
||||
|
||||
|
||||
@@ -88,18 +88,16 @@ def inspect_core(path, cache):
|
||||
segments, notes, padding, threads, signals = [], [], [], [], []
|
||||
for segment_index, segment in enumerate(image.iter_segments()):
|
||||
h = segment.header
|
||||
header_offset = header.e_phoff + segment_index * header.e_phentsize
|
||||
type_id = int.from_bytes(content[header_offset:header_offset + 4], "little")
|
||||
backing = type_id != 0 and h.p_filesz > 0
|
||||
backing = h.p_type != "PT_NULL" and h.p_filesz > 0
|
||||
if backing:
|
||||
location(h.p_offset, h.p_filesz, len(content))
|
||||
if type_id == 1 and h.p_filesz > h.p_memsz:
|
||||
if h.p_type == "PT_LOAD" and h.p_filesz > h.p_memsz:
|
||||
raise CoreFailure("format", "Recorded PT_LOAD file bytes exceed memory size.")
|
||||
segments.append({"index": segment_index, "type": h.p_type, "type_id": type_id,
|
||||
"header_location": location(header_offset, header.e_phentsize, len(content)),
|
||||
segments.append({"index": segment_index, "type": h.p_type,
|
||||
"header_location": location(header.e_phoff + segment_index * header.e_phentsize, header.e_phentsize, len(content)),
|
||||
"offset": hex(h.p_offset), "file_size": hex(h.p_filesz), "memory_size": hex(h.p_memsz),
|
||||
"virtual_address": hex(h.p_vaddr), "physical_address": hex(h.p_paddr), "alignment": hex(h.p_align), "flags": hex(h.p_flags), "file_backing": "file" if backing else "none"})
|
||||
if type_id != 4 or h.p_filesz == 0:
|
||||
if h.p_type != "PT_NOTE" or h.p_filesz == 0:
|
||||
continue
|
||||
position, end = h.p_offset, h.p_offset + h.p_filesz
|
||||
header_size = image.structs.Elf_Nhdr.sizeof()
|
||||
@@ -111,7 +109,6 @@ def inspect_core(path, cache):
|
||||
padding.append({"segment_index": segment_index, "location": location(position, len(raw), end), "bytes_base64": encoded(raw)})
|
||||
break
|
||||
note = struct_parse(image.structs.Elf_Nhdr, image.stream, stream_pos=position)
|
||||
note_type_id = int.from_bytes(content[position + 8:position + 12], "little")
|
||||
owner_start = position + header_size
|
||||
descriptor_start = owner_start + ((note.n_namesz + 3) // 4) * 4
|
||||
note_end = descriptor_start + ((note.n_descsz + 3) // 4) * 4
|
||||
@@ -122,10 +119,7 @@ def inspect_core(path, cache):
|
||||
display = owner.split(b"\0", 1)[0].decode("utf-8", "replace") if owner else None
|
||||
descriptor = content[descriptor_start:descriptor_start + note.n_descsz]
|
||||
note_index = len(notes)
|
||||
# pyelftools selects ET_CORE labels without owner context. Keep
|
||||
# those labels only for exact CORE owners; other kinds are opaque.
|
||||
note_type = note.n_type if owner == b"CORE\0" else note_type_id
|
||||
notes.append({"index": note_index, "segment_index": segment_index, "type": note_type, "type_id": note_type_id,
|
||||
notes.append({"index": note_index, "segment_index": segment_index, "type": note.n_type,
|
||||
"location": location(position, note_end - position, end),
|
||||
"owner_location": location(owner_start, len(owner), end), "owner_bytes_base64": encoded(owner), "owner_display": display,
|
||||
"descriptor_location": location(descriptor_start, len(descriptor), end), "descriptor_bytes_base64": encoded(descriptor)})
|
||||
@@ -133,7 +127,7 @@ def inspect_core(path, cache):
|
||||
# Only the initial Linux CORE ABI is interpreted. Other owners retain exact bytes.
|
||||
if owner != b"CORE\0":
|
||||
continue
|
||||
if note_type_id == 1:
|
||||
if note.n_type == "NT_PRSTATUS":
|
||||
if len(descriptor) < ctypes.sizeof(prstatus_type):
|
||||
raise CoreFailure("format", "Linux " + profile_label + " NT_PRSTATUS descriptor is truncated.")
|
||||
status = prstatus_type.from_buffer_copy(descriptor)
|
||||
@@ -142,7 +136,7 @@ def inspect_core(path, cache):
|
||||
relative = prstatus_type.pr_reg.offset + getattr(registers_type, name).offset
|
||||
registers.append({"name": name, "value": hex(getattr(status.pr_reg, name)), "location": location(descriptor_start + relative, ctypes.sizeof(kind), end)})
|
||||
threads.append({"note_index": note_index, "historical_pid": ctypes.c_int32(status.pr_pid).value, "recorded_current_signal": status.pr_cursig, "registers": registers})
|
||||
elif note_type_id == 0x53494749:
|
||||
elif note.n_type == "NT_SIGINFO":
|
||||
if len(descriptor) < ctypes.sizeof(elf_siginfo_64):
|
||||
raise CoreFailure("format", "Linux " + profile_label + " NT_SIGINFO descriptor is truncated.")
|
||||
signal = elf_siginfo_64.from_buffer_copy(descriptor)
|
||||
|
||||
@@ -29,17 +29,7 @@ export default defineConfig({
|
||||
items: [
|
||||
{ text: "Native binaries", link: "/native-investigation" },
|
||||
{ text: "Go build metadata", link: "/go-binaries" },
|
||||
{ text: "IL2CPP metadata", link: "/il2cpp-metadata" },
|
||||
{ text: "Model artifacts", link: "/model-artifacts" },
|
||||
{ text: "Rust dependencies", link: "/rust-binaries" },
|
||||
{ text: "PE certificates", link: "/pe-certificates" },
|
||||
{ text: "PE imports", link: "/pe-imports" },
|
||||
{ text: "PE TLS/load config", link: "/pe-load-config" },
|
||||
{ text: "ELF notes", link: "/elf-notes" },
|
||||
{ text: "Apple IPS crash reports", link: "/apple-ips" },
|
||||
{ text: "Linux kernel modules", link: "/kernel-modules" },
|
||||
{ text: "CPython pyc files", link: "/python-pyc" },
|
||||
{ text: "Erlang BEAM", link: "/erlang-beam" },
|
||||
{
|
||||
text: "JavaScript and Electron",
|
||||
link: "/javascript-artifact-reconstruction",
|
||||
@@ -53,24 +43,13 @@ export default defineConfig({
|
||||
text: "Web network captures",
|
||||
link: "/web-network-captures",
|
||||
},
|
||||
{
|
||||
text: "Build RL environments",
|
||||
link: "/agent-environments-quickstart",
|
||||
},
|
||||
{ text: "Environment reference", link: "/agent-environments" },
|
||||
{ text: "Electron observation", link: "/electron-observation" },
|
||||
{ text: "Electron fuses", link: "/electron-fuses" },
|
||||
{ text: "Process capture", link: "/process-capture" },
|
||||
{ text: ".NET assemblies", link: "/managed-code-analysis" },
|
||||
{
|
||||
text: "Python packaged applications",
|
||||
link: "/python-packaged-applications",
|
||||
},
|
||||
{ text: "Android applications", link: "/android-analysis" },
|
||||
{ text: "Apple applications", link: "/apple-application-analysis" },
|
||||
{ text: "Desktop installations", link: "/desktop-applications" },
|
||||
{ text: "Firmware", link: "/firmware-analysis" },
|
||||
{ text: "iNES containers", link: "/ines-containers" },
|
||||
{ text: "SQLite databases", link: "/sqlite-databases" },
|
||||
{ text: "Packet captures", link: "/packet-captures" },
|
||||
{ text: "Signal recordings", link: "/signal-recordings" },
|
||||
@@ -87,7 +66,6 @@ export default defineConfig({
|
||||
{ text: "Ghidra raw binary", link: "/raw-binary" },
|
||||
{ text: "Ghidra NativeAOT", link: "/ghidra-nativeaot" },
|
||||
{ text: "Ghidra PowerPC", link: "/ghidra-ppc" },
|
||||
{ text: "Ghidra GameCube DOL", link: "/ghidra-dol" },
|
||||
{ text: "Ghidra semantics", link: "/ghidra-provider" },
|
||||
],
|
||||
},
|
||||
@@ -97,10 +75,6 @@ export default defineConfig({
|
||||
items: [
|
||||
{ text: "MCP contracts", link: "/mcp-contracts" },
|
||||
{ text: "Tool design", link: "/tool-design" },
|
||||
{
|
||||
text: "Admission limits and recovery",
|
||||
link: "/design/limits-and-recovery",
|
||||
},
|
||||
{ text: "Testing", link: "/testing" },
|
||||
{ text: "Client verification", link: "/client-verification" },
|
||||
{
|
||||
|
||||
+15
-82
@@ -90,7 +90,7 @@ existing analysis families directly, for example
|
||||
| `read_adb_logcat` | read | Bounded `logcat -d -t` dump for one buffer, optional pid filter |
|
||||
| `list_adb_packages` | read | `pm list packages -f` with scope; base APK device paths |
|
||||
| `resolve_adb_packages` | read | Case-insensitive package-name substring search with exact-match flag |
|
||||
| `inspect_adb_package` | read | `dumpsys package` projection: versions, install times, installer, flags, and count of listed requested-permission rows (including Unicode names) |
|
||||
| `inspect_adb_package` | read | `dumpsys package` projection: versions, install times, installer, flags, permission count |
|
||||
| `list_adb_directory` | read | NUL-framed immediate children, including hidden entries: kinds, permissions, owner, group, sizes, symlink targets |
|
||||
| `pull_adb_package` | read | Complete split APK set from `pm path`, each file digested |
|
||||
| `pull_adb_file` | read | One absolute device path pulled and digested |
|
||||
@@ -98,34 +98,11 @@ existing analysis families directly, for example
|
||||
| `collect_adb_bugreport` | read | Device-generated bugreport zip, digested |
|
||||
| `push_adb_file` | **mutates device** | One local file to a device path (e.g. pushing tools into `/data/local/tmp`); checks for existing targets and refuses them without explicit overwrite (the check is not atomic); compares digests when device `sha256sum` is available |
|
||||
| `install_adb_package` | **mutates device** | Local APK via `adb install [-r]`; preserves its Success/Failure verdict, records `replace_requested`, and leaves actual replacement `null` because prior package state is not observed |
|
||||
| `uninstall_adb_package` | **mutates device** | Requests package removal for user 0 via `adb shell pm uninstall --user 0`, without data retention |
|
||||
| `uninstall_adb_package` | **mutates device** | Removes a package and its data for user 0 |
|
||||
| `start_adb_app` | **mutates device** | Resolves the package's launcher activity (`cmd package resolve-activity --brief`) and starts it (`am start -n`) |
|
||||
| `start_adb_activity` | **mutates device** | Starts an intent (`am start -a` with optional URI, component, and up to 16 typed extras); preserves the activity manager's verdict |
|
||||
| `stop_adb_app` | **mutates device** | Force-stops every process of a package, ending a dynamic-analysis run |
|
||||
|
||||
Screen capture validates the PNG signature, IHDR framing, dimensions, and IHDR
|
||||
checksum before publishing. It does not validate encoding fields or decode and
|
||||
validate image contents; that limitation is retained in Evidence.
|
||||
|
||||
When `push_adb_file` receives an existing directory, adb places the local file
|
||||
inside it under the local basename; Evidence reports that effective file path
|
||||
while retaining the caller's directory path in the input parameters. The
|
||||
private read-only staging copy preserves the source file's executable bits.
|
||||
|
||||
For `inspect_adb_window`, `focus_contexts` retains each display header, its exact
|
||||
display ID when recognized, and every focus declaration with its original line.
|
||||
Declarations without a display header are explicitly `unscoped`. A missing
|
||||
declaration differs from a declaration whose `value` is JSON `null`.
|
||||
`source_section` identifies the selected dumpsys section; `observed_lines`
|
||||
preserves its focus declarations and display headers in source order.
|
||||
|
||||
`focus_summary` is `single_context` only for one context with no repeated focus
|
||||
field. The singular `focused_window` and `focused_app` summarize that context;
|
||||
they do not establish device-wide focus. Multiple contexts or repeated fields
|
||||
produce `ambiguous` with null singular fields and all declarations retained.
|
||||
When no focus field was declared, the status is `unavailable`. REA reads the
|
||||
displays section only when the windows section declares neither focus field.
|
||||
|
||||
For `install_adb_package`, `replace_requested` records whether `-r` was sent.
|
||||
The `replaced` result is `null`: the successful install response does not say
|
||||
whether the package was installed fresh or replaced, and REA does not query the
|
||||
@@ -140,38 +117,13 @@ and during shutdown.
|
||||
|
||||
## Interpret the result
|
||||
|
||||
- **Package numeric declarations retain failures.** `inspect_adb_package`
|
||||
admits the first valid `userId` or `appId` as `user_id`: a nonnegative safe
|
||||
integer, including zero. A null value means no valid declaration was found.
|
||||
Malformed declarations before or after a valid value remain in
|
||||
`numeric_issues`: the first ten issues with command, field, reason and original
|
||||
`raw_line`, plus the exact `total`. Reasons are `invalid_format` or
|
||||
`unsafe_integer`. Any issue makes package coverage partial; existing section
|
||||
omissions can also make it partial. `version_code` remains exact source text.
|
||||
- **Display nulls preserve uncertainty.** `inspect_adb_display` admits positive
|
||||
safe integers. Absent or malformed optional values are null; `numeric_issues`
|
||||
distinguishes malformed declarations from absence. It keeps the first ten
|
||||
issues and exact total, with command, field, original line and reason
|
||||
(`invalid_format`, `unsafe_integer` or `nonpositive`). The last recognized
|
||||
labeled declaration wins, even when malformed. A physical-density label
|
||||
suppresses bare-number fallback even when malformed; without that label, the
|
||||
first unsigned bare number wins, including zero or an unsafe integer that
|
||||
leaves the value null. Display coverage is complete only with known physical
|
||||
density and no numeric issues. Missing or malformed required physical size
|
||||
produces a protocol failure with `details.partial_observation`: an
|
||||
`inspect_adb_display` observation from provider `adb`, reason
|
||||
`physical_size_absent` or `physical_size_malformed`, and a `result` retaining
|
||||
client, serial, optional display facts, numeric issues, null physical size
|
||||
and partial coverage.
|
||||
- **Directory names retain whitespace and newlines.** Symlink targets do too,
|
||||
including trailing newlines. Records containing invalid UTF-8 remain as
|
||||
labelled hex in `unparsed_lines` and make directory coverage partial.
|
||||
- **Inferences carry their basis.** Device `kind` reports
|
||||
`kind_basis` (serial prefix or USB transport); APK `role` reports
|
||||
`role_basis: file_name`; `emulator_observed` is derived from the build's
|
||||
`ro.kernel.qemu` property: `true` only when its value is `"1"`, `false` for
|
||||
any other reported value, and `null` when the property is absent. The raw
|
||||
property value remains available in `properties`.
|
||||
`role_basis: file_name`; `emulator_observed` is the build's own
|
||||
`ro.kernel.qemu` value, `null` when absent.
|
||||
- **Device paths are not host paths.** `base_apk_device_path` values live on
|
||||
the device filesystem. Only pulls and captures produce host-local paths,
|
||||
each paired with byte size and SHA-256 digest. A pulled file's host name is
|
||||
@@ -196,19 +148,14 @@ and during shutdown.
|
||||
APKs keep other names report the `unknown` role rather than guessing.
|
||||
- **Partial results keep facts.** Failed individual pulls return successful
|
||||
artifacts with `coverage: "partial"` and per-file `failures`. Listings
|
||||
and package searches that contain unrecognized lines report the first ten
|
||||
as `unparsed_lines`, state the exact total in a limitation when more were
|
||||
seen, and mark partial coverage instead of dropping rows.
|
||||
that contain unrecognized lines report them as `unparsed_lines` and mark
|
||||
partial coverage instead of dropping rows.
|
||||
- **Mutations are explicit.** `push_adb_file`, `install_adb_package`, and
|
||||
`uninstall_adb_package` declare `mutatesTarget` and never run implicitly:
|
||||
pushes check for existing device files before transfer and refuse them
|
||||
unless `overwrite` is set. A failed or unexpected existence probe stops
|
||||
transfer even with `overwrite`; only a clean `test -e` exit of 1 with no
|
||||
output is interpreted as absence. The check is not atomic, so a different
|
||||
process can create the path before `adb push` writes it. When available, the device's
|
||||
`sha256sum` verifies the transfer. A missing command or output without a
|
||||
recognizable digest reports `device_digest_source: "unavailable"` with an
|
||||
unverified-transfer limitation; installs preserve the
|
||||
unless `overwrite` is set. The check is not atomic, so a different process
|
||||
can create the path before `adb push` writes it. When available, the device's
|
||||
`sha256sum` verifies the transfer; installs preserve the
|
||||
device's `Failure [...]` reason verbatim.
|
||||
- **Producer identity stays attached.** Every result embeds the producing
|
||||
adb client (`binary_path`, `path_source`, `version`, `installed_path`).
|
||||
@@ -220,12 +167,8 @@ and during shutdown.
|
||||
The app/activity lifecycle tools use `am start -W` and `am force-stop`.
|
||||
- Per-invocation budgets: client/devices 10-20 s, shell reads 20-60 s with
|
||||
64 KiB-8 MiB capture caps (logcat 4 MiB, screen 32 MiB), transfers and
|
||||
bugreports 600 s. Output capture overflow reports `resource_constraint`
|
||||
and names the byte allowance. Deadline expiration, including the
|
||||
client-version probe, reports `provider_timeout` and names the effective
|
||||
milliseconds. This release has no runtime override for those deadlines or
|
||||
capture caps. Command cancellation preserves the caller's exact reason;
|
||||
a later abort does not replace an already completed command failure.
|
||||
bugreports 600 s. Exceeded budgets report `resource_constraint`; stopped
|
||||
commands report `provider_timeout`.
|
||||
- Pull and capture outputs are additive: the destination must not already
|
||||
exist. Re-running a completed pull refuses the existing directory instead
|
||||
of overwriting evidence.
|
||||
@@ -257,25 +200,15 @@ beyond already-connected devices (`adb connect`) are not exposed yet.
|
||||
|
||||
## Real verification
|
||||
|
||||
`npm run verify:adb` exercises observation and transfer operations against a live device. It
|
||||
`npm run verify:adb` exercises every operation against a live device. It
|
||||
requires `adb` (or `REA_ADB_PATH`) and any attached emulator or phone;
|
||||
`--serial` selects one when several are attached, `--pull PACKAGE` pulls a
|
||||
real package and verifies the returned digests against the files on disk.
|
||||
The optional `--install-apk` lifecycle is unavailable until the selected APK
|
||||
identity and device cleanup ownership can be established safely.
|
||||
real package and verifies the returned digests against the files on disk,
|
||||
and `--install-apk PATH` drives a full install → resolve → uninstall
|
||||
lifecycle with a lane-owned probe.
|
||||
|
||||
Record: verified against adb 34.0.5 on Linux against an Android 14
|
||||
(API 34) x86_64 emulator — all operations above, a 224-package inventory,
|
||||
287 services, 92 features, a real two-APK split set pulled with byte-exact
|
||||
digests, a verified push/pull roundtrip, a 1.3 MB screen capture, and the
|
||||
complete package lifecycle; see `docs/testing.md` for the lane summary.
|
||||
|
||||
Intent integer extras use signed decimal text. The ADB argument encoder removes
|
||||
redundant leading zeros for Android's radix-sensitive `--ei` parser while retained
|
||||
Evidence keeps the caller's original spelling. Package inspection accepts both
|
||||
`userId` and Android 16's `appId` spelling for the legacy `user_id` result field.
|
||||
|
||||
A completed push and device digest verification are separate observations. A
|
||||
verification timeout, cancellation, or output budget failure returns the typed
|
||||
failure with partial Evidence of the completed transfer. An unavailable checksum
|
||||
command or unrecognized checksum output leaves verification explicitly unknown.
|
||||
@@ -1,204 +0,0 @@
|
||||
# Build RL environments with REA
|
||||
|
||||
REA turns a recorded web app session into a runnable environment for training
|
||||
and testing AI agents. An agent can practice tasks such as finding an issue,
|
||||
creating a folder, or editing a record without running the original app.
|
||||
|
||||
REA builds the tools the agent calls, the local data those tools use, and a
|
||||
test that checks the environment starts and responds. It supports changes
|
||||
when the recording confirms their effects. You can then use the environment
|
||||
with your RL framework to set task goals, check rewards, and run training.
|
||||
|
||||
## Before you start
|
||||
|
||||
Install [REA and its agent integration](installation.md). To record an app,
|
||||
you also need a running test instance and a Chrome-family browser. Choose an
|
||||
instance where the agent is allowed to create, edit, and delete data. REA
|
||||
records the app you provide; you start it and prepare its test data.
|
||||
|
||||
To run the exported environment, install the
|
||||
[AgentEnv framework](https://github.com/scaleapi/agentenv-framework#install)
|
||||
and start Docker. REA's [compatibility check](testing.md#exported-environment-bundles)
|
||||
uses AgentEnv version `0.9.1334` with Python 3.12.
|
||||
|
||||
There are two ways to build an environment: ask your agent to record the app,
|
||||
or start from a recording you already have.
|
||||
|
||||
## 1. Record and build with your agent
|
||||
|
||||
Give your agent the app URL, the operations you need, your browser's executable
|
||||
path, and an output directory. For an app with folders, adapt this prompt:
|
||||
|
||||
```text
|
||||
Use REA to build an AgentEnv environment from my test web app at
|
||||
http://127.0.0.1:3000. Cover listing, creating, renaming, and deleting folders.
|
||||
You may create and remove test folders. Leave the existing data unchanged.
|
||||
|
||||
Sign in as admin using the password in APP_PASSWORD. Use the browser at
|
||||
/absolute/path/to/chromium. Save the environment to
|
||||
/absolute/path/to/my-env, which does not exist yet.
|
||||
|
||||
Record the session, check which operations the environment supports, and
|
||||
export it. Report any unsupported operations and what is missing.
|
||||
```
|
||||
|
||||
Replace the URL, account, paths, and folder operations with your own. Set
|
||||
`APP_PASSWORD` in the environment that starts REA's MCP server so REA can use
|
||||
it without putting the password in the prompt.
|
||||
|
||||
Your agent inspects the app, records a session, checks the result, and exports
|
||||
a folder containing the runnable environment. It reports which operations
|
||||
work and which need more evidence. The app is used during recording; the
|
||||
exported environment runs from the saved data.
|
||||
|
||||
After your agent exports the folder, skip to
|
||||
[run the environment](#3-run-the-environment). The next section is an
|
||||
alternative for an existing recording.
|
||||
|
||||
## 2. Or build from a saved recording
|
||||
|
||||
A HAR file records a browser's network requests and responses. Export one
|
||||
with API response bodies from your browser's network panel, or use a
|
||||
recording you already have. To include create, edit, and delete operations,
|
||||
record those actions and the reads that show their results.
|
||||
|
||||
Build the environment into a new or empty directory:
|
||||
|
||||
```sh
|
||||
rea environment-build ./session.har ./my-env --name my-env --json
|
||||
```
|
||||
|
||||
You can also pass capture Evidence JSON saved by REA's
|
||||
`capture-browser-scenario` or `inspect-web-network-capture` commands. Here,
|
||||
Evidence means REA's saved recording with its source information. The build
|
||||
accepts HAR files and those JSON recordings. It reads local data and writes
|
||||
the environment folder without contacting the app.
|
||||
|
||||
Add `--dry-run` to review the build report without writing the folder. For an
|
||||
unreleased REA checkout, use `--package-tarball` with its packed package so
|
||||
the exported environment runs the same implementation.
|
||||
|
||||
The build report tells you what the environment supports:
|
||||
|
||||
| Report field | What to check |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `state` | Whether the environment can answer from local state or uses recorded-response replay. |
|
||||
| `coverage.reads` | Which read operations use state and which replay recorded answers. |
|
||||
| `coverage.writes` | Which create, edit, and delete operations are supported. Each withheld operation includes a reason and a recording hint. |
|
||||
| `warnings` | Recorded content or local paths to review before sharing the folder. |
|
||||
|
||||
A successful build means the folder was created. Check the report to confirm
|
||||
it supports the operations your tasks need. `coverage: null` means the build
|
||||
could not prepare state for this environment and it will use replay.
|
||||
|
||||
## 3. Run the environment
|
||||
|
||||
Whether your agent exported the folder or you built it from a HAR, run it
|
||||
with AgentEnv:
|
||||
|
||||
```sh
|
||||
agent-env run ./my-env
|
||||
```
|
||||
|
||||
Use the folder path your agent reported if it differs. The first run builds
|
||||
the container image, so it needs access to the image and package sources.
|
||||
|
||||
REA includes a basic test, called the smoke task. It checks the expected tool
|
||||
count and, when a suitable read tool exists, calls it and checks its response
|
||||
status. Passing this test shows the environment starts and responds in the
|
||||
tested setup. It does not yet score an agent on an application task.
|
||||
|
||||
The folder contains the recording, a tool manifest describing the available
|
||||
operations, a Dockerfile, environment configuration, and the test. The
|
||||
[export reference](agent-environments.md#export-a-bundle) describes each file.
|
||||
|
||||
## 4. Add tasks and reward checks
|
||||
|
||||
Start with one task the exported tools support. Give it a clear goal and a
|
||||
checkable result. For example, if folder creation is supported, ask your
|
||||
agent:
|
||||
|
||||
```text
|
||||
Use this exported environment to add an AgentEnv task that asks an agent
|
||||
to create a folder named Practice. Check the saved environment state to
|
||||
confirm the folder exists with that name, and use that check to score the
|
||||
result. Reset the environment before each run so attempts start with the
|
||||
same data. Use the AgentEnv task and verifier formats.
|
||||
```
|
||||
|
||||
This asks your agent to help build the task and its grading check around the
|
||||
environment REA generated. A retrieval task might check an answer against a
|
||||
recorded value. A task that changes data should check the resulting state.
|
||||
When state is available, the environment supports reset, load, and read-back
|
||||
operations for repeatable runs.
|
||||
|
||||
Use the [AgentEnv documentation](https://www.agentenvframework.com/docs) for
|
||||
task configuration, agent connections, scoring, and execution. Connect the
|
||||
resulting tasks to your chosen RL training workflow.
|
||||
|
||||
## Improve an operation that is missing
|
||||
|
||||
For REA to support a change, the recording needs to show what happened before
|
||||
and after it. When recording against your test app:
|
||||
|
||||
1. Read the list and an existing item before making changes. Repeat the
|
||||
initial reads to show which values change independently.
|
||||
2. Create a test item, read it back, and read the list before the next change.
|
||||
Repeat with a second item and different text.
|
||||
3. Edit each test item, then read the item and list again. Leave more than a
|
||||
second between creation and the first edit to show time-dependent changes.
|
||||
4. Delete the test items, then read them back and read the list to confirm
|
||||
their removal.
|
||||
|
||||
Also record filters, searches, sorting, and paging if your tasks need them.
|
||||
The agent should check that every recording step completed before using it.
|
||||
The [recording workflow](https://github.com/morluto/rea/blob/main/.agents/skills/reverse-engineer-anything/references/environments.md)
|
||||
has the detailed browser instructions, including timeouts and handling IDs
|
||||
assigned by the app.
|
||||
|
||||
Use the report's hints to decide what to record next:
|
||||
|
||||
| Reason | Next step |
|
||||
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `unconfirmed` | Read the item and its list after the change. |
|
||||
| `no-state-target` | Check the path template and recorded views; record missing reads. The route form may not be supported as a mutation target. |
|
||||
| `no-observation` | Read back a created item. For an edit or delete, also read it before changing it. |
|
||||
| `contradicted` or `side-effect` | Inspect the reported disagreement. The operation may need behavior REA cannot model yet. |
|
||||
|
||||
See the [full reason table](agent-environments.md#build-a-bundle-in-one-step)
|
||||
for the other cases. Rebuild from the new recording and check the report
|
||||
again. A new recording replaces the build input; combining separate recording
|
||||
sessions is not supported.
|
||||
|
||||
## Compare with the original app
|
||||
|
||||
For a stronger check, record a separate sequence of calls against the real
|
||||
app, starting from the same test data as the original recording. This is a
|
||||
probe. Restore that starting data before each probe sequence, then compare
|
||||
the saved probe recordings with the environment:
|
||||
|
||||
```sh
|
||||
rea environment-compare ./my-env/envs/my-env/manifest.json \
|
||||
--evidence ./my-env/envs/my-env/evidence.json \
|
||||
--probe ./probe-create-edit-delete.json --probe ./probe-reads.json --json
|
||||
```
|
||||
|
||||
Each probe file must be capture Evidence JSON from REA. The comparison runs
|
||||
offline and reports differences in responses, unsupported calls, and missing
|
||||
observations. It accounts for supported differences in IDs, times, and
|
||||
modeled generated values. Agreement applies to the calls those probes check.
|
||||
See the [comparison reference](agent-environments.md#measure-fidelity-against-the-application)
|
||||
for the exact rules.
|
||||
|
||||
## Before sharing the environment
|
||||
|
||||
The exported folder includes retained response bodies and local source paths.
|
||||
Standard authentication headers are excluded, but custom headers, query
|
||||
values, and body tokens can remain. Review the content you plan to share.
|
||||
Credential-name warnings point to values worth reviewing; they do not remove
|
||||
those values or establish that the recording contains no credentials.
|
||||
|
||||
For direct MCP serving, replay behavior, state controls, and resource limits,
|
||||
see the [environment reference](agent-environments.md). The
|
||||
[implementation index](specs/agent-environments/README.md) links the contributing
|
||||
PRs and their verification records.
|
||||
File diff suppressed because it is too large.
Load diff
+17
-442
@@ -30,115 +30,10 @@ code units; follow `coverage.next_offset`. The page preserves `source_status` an
|
||||
`reported_total_bytes`: exhausting retained text does not make a partial decoded
|
||||
manifest complete. Complete source Evidence remains exportable under the existing
|
||||
session lifetime. Servers without retention reject summary before starting JADX.
|
||||
One-shot CLI output remains complete by default. Select `--detail summary` with
|
||||
`--evidence_output /local/capture.json` to retain complete Evidence before returning
|
||||
a linked view. Older servers require the existing complete request after schema discovery. APK authenticity and runtime behavior remain
|
||||
One-shot CLI output remains complete. Older servers require the existing complete
|
||||
request after schema discovery. APK authenticity and runtime behavior remain
|
||||
unverified.
|
||||
|
||||
## Manifest component projection
|
||||
|
||||
`inspect_android_package` also returns a static inventory of declared
|
||||
activities, aliases, services, receivers and providers from the decoded
|
||||
manifest. The binary XML decoder preserves component class spelling, including
|
||||
`$` in nested classes, without applying JADX class aliases. `declared_name`
|
||||
preserves the decoded source attribute; `name` resolves a
|
||||
relative name only when the manifest package is known and the resolved copy
|
||||
fits the UTF-8 byte budget. An unresolved package or exhausted name budget
|
||||
retains the declared name and marks the inventory partial. Empty component
|
||||
names and names with forbidden DEX characters retain their raw spelling and
|
||||
are not treated as identities. Valid DEX spaces, including boundary spaces,
|
||||
are preserved when resolving component names and matching alias targets.
|
||||
Whitespace in the manifest package is rejected by its separate package grammar.
|
||||
Component permissions inherit the application
|
||||
permission when no component permission is declared, except an activity alias
|
||||
with no permission: Android documents that case as requiring no permission
|
||||
through the alias. `permission_source` distinguishes a local declaration,
|
||||
inherited value, no requirement (including an explicit empty override), and a
|
||||
value omitted because its resource/manifest-placeholder token is unresolved
|
||||
or the total permission-byte budget was reached. Resource and placeholder
|
||||
references are not expanded at this parser boundary. Repeated inherited
|
||||
values share one inventory-wide byte budget.
|
||||
Provider `android:readPermission` and `android:writePermission` overrides are
|
||||
not projected as separate fields; their presence marks the inventory partial
|
||||
and is called out in `limitations`. Relative component names are not resolved
|
||||
against a manifest package with surrounding whitespace.
|
||||
Class-name segments follow DEX `SimpleName` characters, including a leading
|
||||
digit or hyphen, rather than Java source identifier rules. Relative-name
|
||||
expansion does not prove class existence, DEX-version compatibility or runtime
|
||||
loadability. SDK defaults accept JADX's decimal and hexadecimal integer
|
||||
spellings; the original SDK declaration strings remain unchanged.
|
||||
|
||||
`exported` is the declared literal `true` or `false`, otherwise null.
|
||||
`exported_default` is the documented default only where it can be established;
|
||||
it remains null for unknown provider target SDKs, nonliteral declarations, or
|
||||
components with filters targeting API 31 or later that omit the now-required
|
||||
explicit declaration. A limitation records the latter validation error.
|
||||
Provider authorities and all inventory dimensions are bounded. `status:
|
||||
"partial"` and `limitations` identify omitted declarations or values; a
|
||||
capacity limitation appears only when an actual collection limit was reached.
|
||||
The projection does not merge manifests, expand resource references, or
|
||||
observe runtime enabled/export state.
|
||||
|
||||
`enabled` and `grant_uri_permissions` report only literal component-level
|
||||
`android:enabled` and provider-level `android:grantUriPermissions` declarations.
|
||||
They are `null` when absent or not a literal boolean; missing attributes are not
|
||||
filled from application defaults. A nonliteral declaration gets a limitation.
|
||||
`process` preserves the exact component-level `android:process` spelling (including
|
||||
empty or symbolic values), or `null` when absent or omitted under the inventory-wide
|
||||
1 MiB copy budget. Neither field resolves resources, merges manifest values or
|
||||
establishes the effective runtime process or enabled state. The exact boolean
|
||||
attributes are retained in `enabled_declaration` and
|
||||
`grant_uri_permissions_declaration`, including resource references.
|
||||
|
||||
`intent_filters` preserves each filter's source ordinal, exact priority spelling,
|
||||
actions, categories and individual data declarations (scheme, host, port, path,
|
||||
path prefix/pattern/advanced pattern/suffix and MIME type). These are declarations,
|
||||
not computed Android matching rules. `meta_data` lists names; values remain in the
|
||||
complete decoded manifest. Component, filter, data and metadata `location` values
|
||||
use one-based lines/columns and UTF-16 offsets into that manifest. A component's
|
||||
`declaration_index` preserves its original declaration ordinal, including gaps.
|
||||
|
||||
`declaration_coverage` distinguishes complete, partial and unknown observations.
|
||||
Historical Evidence omits newly introduced fields and defaults coverage to
|
||||
`unknown`; omission never establishes absence. Current records contain the fields,
|
||||
with null for absent or unresolved scalar declarations. The inventory retains at
|
||||
most 32 filters and 64 nested action/category/data/metadata records per component,
|
||||
and shares a 1 MiB structured-value copy budget across components. Omitted values
|
||||
or unsupported filter constructs mark coverage partial and include a limitation;
|
||||
complete retained manifest text remains accessible.
|
||||
|
||||
For a small answer, select `detail: "summary"` with an explicit view:
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "android",
|
||||
"facet": "components",
|
||||
"actions": ["android.provider.Telephony.SMS_RECEIVED"],
|
||||
"fields": ["name", "permission", "enabled", "process", "intent_filters"],
|
||||
"offset": 0,
|
||||
"limit": 16
|
||||
}
|
||||
```
|
||||
|
||||
The same JSON works with CLI `--view` or retained `inspect_analysis_view`.
|
||||
`kinds`, `names` and `actions` use exact matching; names accept either the recorded
|
||||
declared or resolved name, actions match any requested action, and different
|
||||
filters combine with AND. `exported` matches only explicit declarations, keeping
|
||||
`exported_default` separate. Empty selectors match nothing. Every selected row
|
||||
retains its parent-array `source_index` and `declaration_coverage`; source coverage,
|
||||
parent identity and limitations remain alongside the page. Follow
|
||||
`coverage.next_offset`. The 1 MiB projection budget can shorten a page; a single
|
||||
oversized row requires fewer selected fields. Exhausting a partial source does
|
||||
not establish complete manifest coverage. `facet: "component-summary"` applies
|
||||
the same filters and counts kinds, declaration coverage and explicit/default
|
||||
exported values separately. Both adapters use the same retained projection;
|
||||
repeated pages do not reload JADX.
|
||||
|
||||
The service `foreground_service_type` field preserves the declared attribute,
|
||||
including an empty value or surrounding spaces; `null` means the attribute is
|
||||
absent or the component is not a service. This field does not validate service
|
||||
type tokens or establish runtime eligibility.
|
||||
|
||||
## Supply tools
|
||||
|
||||
Use a **full JDK 17 or newer** (including its compiler module) and
|
||||
@@ -208,10 +103,9 @@ that every class or engine operation will load successfully. Discovery limits
|
||||
each ZIP metadata read to 8 MiB before allocation; a larger directory reports
|
||||
the exhausted read budget and the selected JAR path.
|
||||
|
||||
The filtered class-page bridge (protocol 2) is verified on Linux x86-64 with
|
||||
Temurin 21, public Appium ApiDemos 6.0.18 and NewPipe 0.29.1 APKs. Earlier
|
||||
metadata-bridge records covered macOS arm64/OpenJDK 21 and Windows x64/JDK 21;
|
||||
this protocol revision has not been rerun on those hosts or Linux/JDK 25.
|
||||
The current metadata bridge is verified on macOS arm64 with OpenJDK 21 and the
|
||||
public Appium ApiDemos fixture. The POSIX adapter also supports Linux; the new
|
||||
bridge has not yet undergone real verification on Linux/JDK 25.
|
||||
Windows x64 uses an owned Job Object with bidirectional stdio, atomic job
|
||||
assignment and kill-on-owner-close. Real Windows verification uses JDK 21 and
|
||||
the pinned JADX 0.7.1 JAR; see the real verification lane below.
|
||||
@@ -252,64 +146,10 @@ queries accept the same `overload_index`; the bridge resolves the selected
|
||||
method node and the result reports its index and displayed signature. These
|
||||
indices are tied to the current artifact and engine, not stable across builds.
|
||||
|
||||
Method reference queries also accept an external class, such as
|
||||
`android.app.NotificationManager --method-name notify`, when the APK directly
|
||||
invokes that exact DEX class and method name. If multiple prototypes are observed,
|
||||
the input error lists their exact DEX descriptors and local overload indices;
|
||||
repeat with an explicit `--overload-index`. These are the prototypes observed in
|
||||
the APK, not the complete method inventory of an Android framework or library.
|
||||
|
||||
Such results report `target_definition: "external"` and
|
||||
`reference_scope: "direct_dex_invokes"`. Each using method has `source_selector`
|
||||
with `class_name`, `method_name` and `overload_index`; pass those fields with the
|
||||
same APK path to `inspect_android_method` to inspect the caller. Repeated direct
|
||||
invokes in one method remain one using node with multiple instruction offsets.
|
||||
This scans raw invoke instructions in JADX's admitted APK methods without
|
||||
generating their source. It does not resolve reflection, JNI, method handles,
|
||||
inherited method lookup or possible runtime dispatch. Class-only queries and
|
||||
class/method inspection still require an APK definition; an external target has
|
||||
no invented body. Unknown class/method names remain input errors.
|
||||
|
||||
External prototype and caller metadata use a bridge-frame-derived representation
|
||||
allowance that reserves space for retained raw calls and CLI/MCP copies. Exhausting
|
||||
it returns a typed `resource_constraint`, rather than a truncated caller list
|
||||
reported as complete. Direct instruction offsets have their separate explicit
|
||||
`direct_invokes_exhaustive` coverage. Older retained Evidence may omit the new
|
||||
definition, scope and caller-selector fields; omission does not establish them.
|
||||
The bridge checks prototype expansion before copying descriptors or joining
|
||||
displayed signatures. It reuses one bounded prototype inventory for the exact
|
||||
class/method selection in the current APK session; replacing the session or
|
||||
selection invalidates that retained result.
|
||||
|
||||
`inspect_android_class` also reports `dex_static_value` for each `static final`
|
||||
field whose class definition encodes a value in its DEX static values, so
|
||||
compile-time constants are readable even when the class has no `<clinit>`.
|
||||
Each value is typed: `boolean`, `byte`, `short`, `char` (UTF-16 code unit) and
|
||||
`int` values are JSON booleans or numbers, while `long`, `float` and `double`
|
||||
values are strings holding Java's text for the value, so 64-bit values stay
|
||||
exact. The field reports `null` when no value is encoded, the encoded value is
|
||||
null or of another kind, or the field is not `static final`. The value is read from parsed
|
||||
metadata without generating source, and a static initializer can still assign
|
||||
the field; use `inspect_android_method` on `<clinit>` for that evidence.
|
||||
Only `inspect_android_class` asks the bridge for these values.
|
||||
`inspect_android_method` and `trace_android_references` read the member-only
|
||||
class summary, so large encoded strings cannot push their member lookup past
|
||||
the 8 MiB protocol frame limit.
|
||||
|
||||
Smali fallbacks select one complete block using the original method name and DEX
|
||||
descriptor, before applying the source byte limit. This preserves the selected
|
||||
overload even when Java code generation renames a method. If the selected Java
|
||||
body was omitted, including a method merged into a synthetic bridge, REA returns
|
||||
its original smali with the `java_body_unavailable` marker. The marker describes
|
||||
missing Java; it does not infer why the method was omitted or name a replacement.
|
||||
Unresolved or duplicate disassembly identities fail instead of attributing another
|
||||
method's instructions to the selection.
|
||||
|
||||
Native and abstract methods are identified from access flags
|
||||
captured before code generation. They report `body_status: "not_available"`, an
|
||||
empty source, and the `native_method` or `abstract_method` marker; their declarations
|
||||
are not passed through the engine's source slicer. Other missing decompiled bodies
|
||||
do not establish either access flag.
|
||||
The engine's smali fallback also joins same-name overloads; REA rejects that
|
||||
fallback for an overloaded method instead of attributing all bodies to one
|
||||
selected overload. Native/abstract methods with no decompiled body report
|
||||
`body_status: "not_available"` without inferring which implementation flag caused it.
|
||||
|
||||
Static initializers (`<clinit>`) return smali with the
|
||||
`static_initializer_disassembly` marker. Java decompilation can move their
|
||||
@@ -317,178 +157,13 @@ instructions into field declarations, leaving an empty or incomplete method
|
||||
body. The selected method's disassembly preserves those instructions and retains
|
||||
the same source byte limit and explicit truncation reporting.
|
||||
|
||||
## Apply an R8/ProGuard mapping
|
||||
|
||||
Release builds shrunk by R8 or ProGuard rename classes and members. Select the
|
||||
build's `mapping.txt` with `mapping_path` on `inspect_android_class` or
|
||||
`inspect_android_method`, or `--mapping-path` on the matching CLI command:
|
||||
|
||||
```sh
|
||||
rea inspect-android-class /path/Example.apk a.b --mapping-path /path/mapping.txt \
|
||||
--detail summary --evidence_output /tmp/class-evidence.json
|
||||
```
|
||||
|
||||
For agent workflows, select `detail: "summary"` on class and method inspection.
|
||||
It preserves the complete normalized member inventory or selected method body,
|
||||
including mapping annotations, while retaining duplicate raw provider calls in
|
||||
the complete parent Evidence. The reply links that parent's identity and digest;
|
||||
export it with `export_evidence_bundle` or read its summary again through
|
||||
`inspect_analysis_view`. CLI summaries require `--evidence_output` and return
|
||||
the portable bundle path. Omitted or `complete` detail preserves complete inline
|
||||
Evidence. Search and reference operations retain their existing contracts.
|
||||
Symbol summary views admit at most **1 MiB**, including view context. An admitted
|
||||
method's source can already consume that amount; a larger combined summary
|
||||
fails explicitly instead of silently truncating facts. MCP refusal includes
|
||||
the retained parent ID. Select complete detail, or inspect one method rather
|
||||
than a whole class, when the requested summary does not fit.
|
||||
|
||||
The mapping path must be absolute. Selectors still take the observed names, such as
|
||||
`a.b`, and observed rows are unchanged. The result adds one `mapping` block with
|
||||
the file's path, SHA-256 and size, the header values the file reports
|
||||
(`compiler`, `compiler_version`, `min_api`, `map_version`, `pg_map_id`,
|
||||
`pg_map_hash`), valid record counts and up to 16 malformed line samples. Its
|
||||
`symbols` list has one entry per observed class, inner class, method and field;
|
||||
`inspect_android_method` lists the class and the selected method. `coverage`
|
||||
counts each resolution status. With summary delivery, these unchanged facts are
|
||||
under the view's `summary` field; the view separately reports source coverage.
|
||||
|
||||
Lookups use the bridge's raw DEX names, not JADX display aliases such as a
|
||||
renamed reserved word. Each symbol resolves as:
|
||||
|
||||
- `mapped`: exactly one record matches. A class needs one record for its raw
|
||||
name (`verified_by: "class"`). A method must also match its DEX prototype
|
||||
(`"descriptor"`) and a field its DEX type (`"type"`). R8 residual signature
|
||||
metadata is used when present; otherwise original types are translated
|
||||
through the file's class records.
|
||||
- `ambiguous`: `multiple-records`, `descriptor-mismatch`, `type-mismatch`,
|
||||
`ambiguous-class` or `residual-signature-conflict`, with `candidate_count` and
|
||||
up to 8 candidates. Conflicting residual metadata never selects a winner. A name match
|
||||
whose prototype or type does not verify is never reported as mapped.
|
||||
- `unmapped`: no valid record matched the raw identity.
|
||||
- `unknown` with reason `projection-budget`: this claim did not fit the combined
|
||||
reply budget. The observed raw symbol remains present; `coverage.unknown`
|
||||
counts these omissions. Inspect that specific class or method separately,
|
||||
or consult the selected local mapping file for complete source records.
|
||||
|
||||
A line continues an inline frame stack when it repeats the previous line's
|
||||
minified range and obfuscated name and gives a single call-site line, such as
|
||||
`:7`. Only the outermost original of a stack becomes a candidate. Original
|
||||
line numbers and source files are not projected. Malformed records are counted
|
||||
and skipped, so an `unmapped` symbol might be described by one of them. A file
|
||||
that is missing, not a regular file, not UTF-8, larger than the byte limit or
|
||||
without any class record, such as `usage.txt` or `seeds.txt`, is refused before
|
||||
JADX is used. Requests read their mappings one at a time, in the same queue as
|
||||
the JADX work and under the same operation deadline. A mapping failure therefore never starts or retires the JADX
|
||||
session, and a different mapping reuses the loaded APK.
|
||||
|
||||
## Link native declarations to JNI candidates
|
||||
|
||||
When advertised, `inspect_android_jni_bindings` links original `ACC_NATIVE`
|
||||
declarations from standard root `classes*.dex` members to bundled ELF libraries.
|
||||
The supplied JAR's DEX reader preserves duplicate declarations before decompiler
|
||||
deduplication. This operation does not load the APK into the decompiler.
|
||||
MCP defaults to retained summary delivery when `detail` is omitted; select
|
||||
`complete` explicitly to receive the full report inline.
|
||||
|
||||
```json
|
||||
{
|
||||
"path": "/absolute/Example.apk",
|
||||
"class_name": "org.conscrypt.NativeCrypto",
|
||||
"method_names": ["EVP_PKEY_new_RSA"],
|
||||
"abis": ["arm64-v8a"],
|
||||
"detail": "summary"
|
||||
}
|
||||
```
|
||||
|
||||
Class/method names and optional `descriptors` select raw DEX identities.
|
||||
`library_paths` selects exact logical APK paths such as
|
||||
`lib/arm64-v8a/libconscrypt_jni.so`; each library's `source_path` preserves its
|
||||
original ZIP spelling. Omitted filters include all scoped facts; `[]` matches
|
||||
none. Class selection never narrows the global inventory used for table-class
|
||||
attribution.
|
||||
|
||||
Independent `declarations`, `libraries`, `candidates` and `bindings` collections
|
||||
have stable IDs and source ordinals. Candidates retain APK/library digests,
|
||||
ABI, function addresses and file offsets. Table candidates also retain section,
|
||||
table and entry identity plus all three name/signature/function pointer slots
|
||||
and their relocation sources. JNI exports follow the
|
||||
[UTF-16 escaping and short-before-long rules](https://docs.oracle.com/en/java/javase/21/docs/specs/jni/design.html#resolving-native-method-names);
|
||||
failed escaping does not acquire an invented export match. Tables require
|
||||
NUL-terminated [Modified UTF-8 strings](https://docs.oracle.com/en/java/javase/21/docs/specs/jni/types.html#modified-utf-8-strings),
|
||||
valid descriptors and executable load mappings. REL, RELA and RELR retain
|
||||
separate provenance; unsupported packed relocations and unresolved pointer
|
||||
spans stay explicit unknowns.
|
||||
|
||||
A static `JNINativeMethod` entry names no class. Attribute it uniquely only
|
||||
when exactly one declaration in the complete recorded native inventory has
|
||||
that `(name, descriptor)` pair. Multiple classes remain ambiguous after a
|
||||
class filter; a partial inventory cannot establish unique attribution.
|
||||
|
||||
Each selected declaration has a row per selected/observed ABI. `matched` means
|
||||
candidate presence was observed; `ambiguous` means multiple observed targets
|
||||
or class associations remain. Binding `coverage` is separate: a matched
|
||||
partial row does not establish complete uniqueness, and an empty partial scan
|
||||
is `unknown`, never `unmatched`. ELF/ABI label mismatches preserve facts without
|
||||
asserting a binding. No result establishes library loading, runtime registration
|
||||
or method invocation.
|
||||
|
||||
Summary returns the first 64 selected binding rows, their declaration/candidate
|
||||
closure and every selected library's identity, coverage and failure reason;
|
||||
counts and page coverage describe the full selected binding set. Selected
|
||||
declaration IDs have separate preview coverage, so native declarations remain
|
||||
visible even when the APK has no bundled ABI libraries. Complete
|
||||
Evidence stays retained. Read exact fields or further pages without reopening
|
||||
files or rerunning analysis:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": { "kind": "retained-evidence", "evidence_id": "ev_<digest>" },
|
||||
"view": {
|
||||
"kind": "android-jni",
|
||||
"collection": "bindings",
|
||||
"offset": 64,
|
||||
"limit": 64,
|
||||
"fields": ["declaration_id", "abi", "status", "coverage", "candidate_ids"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Collections also include `declarations`, `candidates` and `libraries`, with
|
||||
exact filters and batch lookup by declaration/candidate/library IDs. `row_index`
|
||||
is the original parent collection index; DEX and
|
||||
ZIP source ordinals stay separate. Complete detail returns the full report.
|
||||
CLI uses the same selection and delivery contracts. Its common one-shot
|
||||
default remains complete; summary delivery requires an explicit Evidence path:
|
||||
|
||||
```sh
|
||||
rea inspect-android-jni-bindings /absolute/Example.apk \
|
||||
--query '{"class_name":"org.conscrypt.NativeCrypto","method_names":["EVP_PKEY_new_RSA"],"abis":["arm64-v8a"]}' \
|
||||
--detail summary --evidence_output /absolute/fresh-jni-evidence.json
|
||||
```
|
||||
|
||||
Optional `--view` accepts the same JSON page and requires summary delivery.
|
||||
Evidence bundles never overwrite files. Complete declarations and one library
|
||||
selection are cached within the immutable APK session. Changing ABI/library
|
||||
selection replaces the scan; class/method selection reuses it. Fresh analysis
|
||||
still validates current caller APK bytes. Idle/close releases the caches;
|
||||
retained Evidence remains historical and independently readable.
|
||||
|
||||
## Interpret the result
|
||||
|
||||
- Package inspection retains decoded manifest XML, summary declarations,
|
||||
permissions and class/resource counts. REA's bridge selects the exact root
|
||||
`AndroidManifest.xml` resource by original archive name and manifest type,
|
||||
independently of asset order. One namespace-aware JDK XML parse supplies
|
||||
summary attributes and direct permission/SDK declarations; namespace prefixes
|
||||
and XML entity spelling do not change their values. Raw decoded XML is retained
|
||||
exactly and only its displayed text is subject to the existing source byte cap.
|
||||
Summaries use the complete selected XML before that cap. Document types are
|
||||
unsupported; parsing does not load external DTDs, schemas, or XInclude files.
|
||||
permissions and class/resource counts. Summary fields are extracted by the
|
||||
upstream engine and should be checked against the retained XML when needed.
|
||||
Signature verification is explicitly `not_performed`.
|
||||
- Class search filters the engine's cached top-level names before transferring
|
||||
pages, then follows contiguous inventory positions without rescanning earlier
|
||||
rows. It checks selection, scope, examined ranges and counts before reporting
|
||||
complete coverage. Complete producer replies stay in Evidence; an empty `query`
|
||||
- Class search consumes all pages and checks their counts. An empty `query`
|
||||
inventories all class-name rows; class names are decompiler representations.
|
||||
Repeated names remain repeated in `matches` so the inventory row count and
|
||||
producer evidence stay visible. `duplicate_names` identifies matching names
|
||||
@@ -498,22 +173,10 @@ retained Evidence remains historical and independently readable.
|
||||
full reported display types, fields and inner class names. Synthetic members
|
||||
that code generation hides can appear in the inventory; it stays consistent
|
||||
after method decompilation within the retained session.
|
||||
Class and method inspection and reference tracing accept either the exact
|
||||
reported class name or its original binary name, including `$` in nested and
|
||||
anonymous classes. The bridge keeps both identities from JADX; it does not
|
||||
replace separators or guess suffix matches. Ambiguous selectors are refused.
|
||||
Follow-up calls preserve the selected name, while results report the frozen
|
||||
JADX display name. Quote `$` names in the shell, for example
|
||||
`'com.example.Manager$WorkerThread'`.
|
||||
Exact DEX prototypes are reported from frozen JADX method metadata as
|
||||
`dex_descriptor`, for example `(I)V`. Display names can be JADX aliases;
|
||||
the descriptor alone is not a complete raw DEX method ID. Overload indices
|
||||
are tied to this artifact and engine, not stable identities across builds.
|
||||
- A `mapping` block appears only when the caller selected `mapping_path`. Its
|
||||
names are claims from that file, not observations of the APK. REA does not
|
||||
check that the mapping belongs to this build; `pg_map_id` is reported, not
|
||||
compared. Evidence keeps the selected path in its parameters and the file's
|
||||
SHA-256 in the result.
|
||||
- Method inspection reports Java or smali, fallback markers and complete/partial
|
||||
returned text. Decompiled source is a derived representation. A complete text
|
||||
result does not establish complete semantics or a byte-identical reconstruction.
|
||||
@@ -570,19 +233,11 @@ rea project-android-application-graph '{"inventory_evidence":[<inventory_artifac
|
||||
```
|
||||
|
||||
The projection reports exact component paths and hashes, runtime-family hints,
|
||||
and path-based bridge hypotheses. File and slice occurrences supply component
|
||||
endpoints; directory trees and symlinks retain their identities in the source
|
||||
inventory without being classified as code files. It does not decode DEX or
|
||||
claim observed runtime calls. DEX/JVM runtime hints summarize the classified
|
||||
bytecode component lists. Framework hints match complete native library names,
|
||||
such as
|
||||
and path-based bridge hypotheses. It does not decode DEX or claim observed
|
||||
runtime calls. Framework hints match complete native library names, such as
|
||||
`libreactnativejni.so`, `libreactnative.so`, `libhermes.so`,
|
||||
`libhermes_executor.so`, `libflutter.so`, and `libunity.so`; unrelated names
|
||||
such as `libcommunity.so` and `libreactivity.so` do not identify those families.
|
||||
The signing list retains the `META-INF/MANIFEST.MF`, `.SF`, `.RSA`, `.DSA`, and
|
||||
`.EC` paths selected by Android's v1 verifier, including nested paths. These are
|
||||
inventory metadata candidates, not proof that a v1 signature verifies or that
|
||||
the APK is signed; use `inspect-apk-signature` for signature analysis.
|
||||
The bridge basis is inferred from the native path and repeated for each managed
|
||||
component, so it does not identify a specific JNI binding. Bridge hypotheses form a DEX/JVM-class by native-library
|
||||
Cartesian product. The projection computes the product and exact serialized
|
||||
@@ -603,19 +258,6 @@ target/configuration changes, failure, timeout, cancellation, MCP disconnect or
|
||||
server shutdown. One-shot CLI commands always join cleanup before returning.
|
||||
REA never writes to the original APK or launches target code.
|
||||
|
||||
JNI scans reuse the analyzer process without loading the APK into the
|
||||
decompiler. They admit at most 64 MiB per DEX member and 256 MiB of DEX bytes,
|
||||
with a 4 MiB native-declaration JSON allowance before MCP string escaping.
|
||||
Native libraries are processed sequentially: one 64 MiB buffer, 256 MiB total
|
||||
selected read work, and a 16 MiB parsed-library-facts allowance. ZIP inventory
|
||||
and library counts also have explicit resource bounds. Unexamined occurrences
|
||||
remain unknown. A complete relationship report has a separate 16 MiB serialized
|
||||
allowance; summaries/pages allow 1 MiB including their provenance envelope.
|
||||
These are resource admissions, not claims about maximum valid Android formats.
|
||||
Select fewer ABI/library occurrences or smaller retained pages when needed.
|
||||
Completed parsed library facts survive failed reader finalization in typed
|
||||
partial observations; the exact reader owner remains available for cleanup retry.
|
||||
|
||||
On Windows, stdin writes preserve stream backpressure and run outside the Node
|
||||
event loop. Cancellation terminates the retained job, including descendants,
|
||||
before workspace cleanup. On Windows, a detached REA helper owns the private
|
||||
@@ -657,43 +299,9 @@ settings. Evidence reports the worker's observed `Runtime.maxMemory()` rounded
|
||||
down to MiB and retains exact bytes, Java version and visible processors in its
|
||||
raw runtime response. The observed allocatable heap can differ from configured
|
||||
`-Xmx`, especially with Serial GC. Heap is not a total RSS cap: the JVM also
|
||||
uses native memory. A typed JVM memory allocation failure during APK loading
|
||||
reports `resource_constraint` with the observed maximum heap in bytes. Known
|
||||
heap failures name `REA_JADX_HEAP_MIB` in remediation; other memory regions
|
||||
remain unknown. A healthy doctor configuration check does not establish that
|
||||
any particular APK fits. Failures before the bridge can report a typed result
|
||||
and upstream errors from other operations retain their existing diagnostics;
|
||||
error text alone does not establish memory exhaustion.
|
||||
|
||||
Independent REA instances have independent queues. Whole
|
||||
operations have a **120-second** execution deadline by default, starting after
|
||||
they reach the queue's front. For a known slow cold load, such as a large
|
||||
multidex APK, select another finite deadline:
|
||||
|
||||
```sh
|
||||
export REA_JADX_OPERATION_TIMEOUT_MS=600000
|
||||
```
|
||||
|
||||
The value must be a decimal millisecond count from 1 to 2147483647; anything
|
||||
else is refused before Java starts. REA reads it from the environment captured
|
||||
at process start. Changing that environment requires a new REA process. On a
|
||||
running MCP server, set `jadx.operation_timeout_ms` through
|
||||
`update_analysis_settings` to change the default for newly accepted operations;
|
||||
queued or running operations retain their accepted budget. The same deadline
|
||||
bounds each upstream call. Timeout recovery reports the effective milliseconds
|
||||
and range, with `details.limit` identifying the runtime setting. An invalid
|
||||
provider-local launch value can also be repaired by a runtime override; reset
|
||||
exposes that invalid launch value again. See the
|
||||
[settings design](design/limits-and-recovery.md#invalid-launch-configuration).
|
||||
A passing JADX configuration check in `rea doctor` reports its launch-derived
|
||||
`operation_timeout_ms`; it does not inspect another running server's overrides. Explicit
|
||||
cancellation still ends the operation immediately, and partial observations
|
||||
and owned-process cleanup are unchanged. Raising the deadline does not extend
|
||||
the bridge's 30-second startup handshake or the upstream **90-second**
|
||||
decompilation limit. MCP clients enforce their own tool-call timeouts, and
|
||||
setup's derived [client tool timeout](client-configuration.md) does not include
|
||||
this setting, so also raise the client timeout when selecting a longer
|
||||
deadline.
|
||||
uses native memory. Independent REA instances have independent queues. Whole
|
||||
operations have a **120-second** execution deadline after reaching the queue's
|
||||
front; upstream decompilation is limited to **90 seconds**.
|
||||
|
||||
Decoded manifest and method text have a **1 MiB** upstream byte budget and report
|
||||
truncation. Class-name pages use indexed slices of up to 4,096 names without changing the
|
||||
@@ -701,27 +309,6 @@ complete, case-sensitive search contract. A protocol frame above **8 MiB**, or c
|
||||
**32 MiB per operation**, fails with a diagnostic instead of returning an incomplete inventory
|
||||
as complete. These are memory-safety boundaries, not undocumented result caps.
|
||||
|
||||
A selected R8/ProGuard mapping is re-read and SHA-256 checked on every call, up
|
||||
to **64 MiB**. The serialized provider keeps one successful parsed index, keyed
|
||||
by byte length and digest with fixed parser options. Identical bytes reuse that
|
||||
index; changed bytes are reparsed, missing files still fail, and each response
|
||||
preserves the currently selected path. The index is released on provider idle
|
||||
retirement or close. Omitting `mapping_path` never applies the cached mapping.
|
||||
During replacement, the old index can coexist with the new parse until it
|
||||
succeeds; these transient allocations count toward process memory.
|
||||
|
||||
Before retaining records, the parser admits an estimated **128 MiB** index
|
||||
representation and **8 MiB** of expanded mapped claims per residual class,
|
||||
including shared header facts. The index allowance accounts for strings and
|
||||
containers; it is not a measured V8 heap cap. Parameter and metadata expansion
|
||||
is admitted before splitting or JSON decoding. These fixed resource bounds
|
||||
have no runtime override; refusal reports `resource_constraint`, the source
|
||||
line and mapping SHA-256. Ambiguous lookups count all candidates while keeping
|
||||
only the first eight in stable file order. A separate **8 MiB** combined claim
|
||||
budget applies to the actual query set, including outer and inner classes,
|
||||
before cloning the report. Claims that do not fit become explicit `unknown`
|
||||
entries; other admitted claims and all observed identities remain available.
|
||||
|
||||
## Real verification
|
||||
|
||||
Download the explicit public fixtures once; this installs no tools:
|
||||
@@ -733,14 +320,6 @@ export REA_ANDROID_TEST_APK="$PWD/_reference/apk-integration/ApiDemos-debug.apk"
|
||||
npm run verify:android
|
||||
```
|
||||
|
||||
The main lane and Windows Java-selection checks validate the selected JDK and
|
||||
engine archive through `validateVerifierJadxConfiguration`. This shared verifier
|
||||
boundary supplies the production validator's resource owner and waits for
|
||||
retained cleanup before proceeding. Failed closes retain their exact owners for
|
||||
retry; prerequisite failures must not be mistaken for completed bridge tests.
|
||||
Recovery checks match the typed Java or archive failure and its applicable
|
||||
remediation.
|
||||
|
||||
The lane also uses the selected full JDK's `jlink` to create a disposable JRE
|
||||
without the compiler module. It checks real Java startup failures, missing
|
||||
selectors, recovery after failures, and CLI cancellation with owned cleanup.
|
||||
@@ -765,7 +344,3 @@ containing node's unknown `dex_offset`. `direct_invokes_exhaustive: false`
|
||||
means instruction rows were bounded by the local bridge frame budget; `null`
|
||||
means the producer did not establish instruction coverage. Using-node totals
|
||||
and coverage do not imply complete instruction evidence.
|
||||
`direct_invokes_coverage` reports counts of using nodes with complete, partial,
|
||||
or unknown positions, plus the number of retained instruction sites. This
|
||||
aggregate makes position truncation visible without scanning every caller;
|
||||
older retained Evidence may omit it, which means the aggregate was unexamined.
|
||||
@@ -1,302 +0,0 @@
|
||||
# APK signature schemes
|
||||
|
||||
REA reads the signing of a selected local Android APK (or ZIP/AAB) statically,
|
||||
so an analyst can answer **who signed this, under which schemes, and whether the
|
||||
declared content digests match these bytes** before trusting or triaging it. The reader
|
||||
is in-process and needs no external engine: it parses the ZIP tail and the APK
|
||||
Signing Block, reads certificates with the platform X.509 parser, and
|
||||
recomputes the content digest. No Android code executes, and no device or
|
||||
`adb` is involved.
|
||||
|
||||
CLI and MCP share one workflow and return artifact-bound Evidence inline.
|
||||
|
||||
```sh
|
||||
rea inspect-apk-signature /path/app.apk
|
||||
```
|
||||
|
||||
## Selected facts and retained Evidence
|
||||
|
||||
MCP defaults to a summary with scheme presence, signer certificate fingerprints,
|
||||
row counts, posture and explicit coverage. `detail: "complete"` returns the full
|
||||
report. A selected page can answer a narrower question in the acquisition call:
|
||||
|
||||
```json
|
||||
{
|
||||
"path": "/path/app.apk",
|
||||
"view": {
|
||||
"collection": "signers",
|
||||
"schemes": ["v3"],
|
||||
"fields": ["certificate_sha256"],
|
||||
"limit": 32
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The returned `parent_evidence_id` identifies the retained complete report.
|
||||
Subsequent reads use `inspect_analysis_view`, including after the APK is deleted:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": {
|
||||
"kind": "retained-evidence",
|
||||
"evidence_id": "<returned parent_evidence_id>"
|
||||
},
|
||||
"view": {
|
||||
"kind": "apk-signature",
|
||||
"collection": "digests",
|
||||
"signer_ids": ["<returned signer id>"],
|
||||
"fields": ["signature_algorithm_id", "algorithm", "matches_apk"],
|
||||
"offset": 0,
|
||||
"limit": 32
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Collections are `signers`, `digests` and declared `lineage` nodes. `ids`,
|
||||
`signer_ids` and `schemes` filters intersect; omitted filters select all available
|
||||
records and `[]` selects none. Omitted `fields` returns every collection column;
|
||||
`fields: []` retains identity and relationship links. IDs include the complete
|
||||
APK digest, scheme and source ordinal; child records retain `signer_id` and
|
||||
`signer_index`, and declared certificates retain their raw-DER SHA-256 identity.
|
||||
Lineage pages expose `previous_id` when selected. Certificate fingerprints can
|
||||
be composed across versions without treating a declaration as verified identity.
|
||||
|
||||
Page coverage describes examined declared records. Exhausting a page does not
|
||||
establish certificate authenticity, platform acceptance, v1 entry coverage or a
|
||||
verified rotation. Partial decoding and its reason remain visible even when a
|
||||
lineage page contains no rows. A weak cache reuses validated immutable report
|
||||
facts; each read checks its parent subject, and new file inspections read again.
|
||||
|
||||
CLI defaults to complete delivery. A compact one-shot response requires a complete
|
||||
Evidence export:
|
||||
|
||||
```sh
|
||||
rea inspect-apk-signature /path/app.apk --detail summary \
|
||||
--view '{"collection":"signers","fields":["certificate_sha256"]}' \
|
||||
--evidence_output /path/app-signing-evidence.json --json
|
||||
```
|
||||
|
||||
The export contains the same complete parent used by MCP. Historical CLI pages
|
||||
use `inspect-analysis-view` with an `evidence-file` source record. The CLI and MCP
|
||||
share the application workflow, projection, retention and publication owners.
|
||||
|
||||
## What it reports
|
||||
|
||||
- **Schemes present:** v1 (JAR signing, from the central directory), and the
|
||||
APK Signing Block's v2, v3 and v3.1. v1 follows apksig's selection: entries
|
||||
under `META-INF/` at any depth ending in `.SF`, or in `.RSA`/`.DSA`/`.EC`
|
||||
(case-sensitive). v1 is `present` only when a signature block has its
|
||||
matching `.SF` file (`META-INF/x/CERT.RSA` pairs with `META-INF/x/CERT.SF`);
|
||||
`signature_files` and `block_files` list every candidate either way.
|
||||
- **Per v1 signer** (each block paired with its `.SF`): REA inflates the block
|
||||
and reads its PKCS#7 `SignedData`. `certificate` is the certificate the
|
||||
block's one `SignerInfo` names by `issuerAndSerialNumber` or
|
||||
`subjectKeyIdentifier` (not by position), with the same facts and raw-DER
|
||||
SHA-256 as a v2/v3 signer, so a v1-only APK can be fingerprinted and
|
||||
clustered with v2/v3-signed ones. apksig resolves only
|
||||
`issuerAndSerialNumber`, so its v1 verification fails for a block whose
|
||||
`SignerInfo` uses `subjectKeyIdentifier`: REA still reports the declared
|
||||
certificate (`identified_by: subject-key-identifier`), adds a limitation,
|
||||
and treats that signer as unknown in `v1_signers_match_block_signers`. `certificates` lists every certificate the
|
||||
block carries (SHA-256 and subject) and `signer_infos` gives each
|
||||
`SignerInfo`'s certificate and digest/signature algorithm OIDs. The PKCS#7
|
||||
`SignedData` envelope is read as BER, including constructed signature OCTET
|
||||
STRINGs; each embedded certificate is still interpreted as DER by the X.509
|
||||
owner. Issuer matching preserves the encoded Name and also admits equivalent
|
||||
ASCII PrintableString/UTF8String values for CN, C, L, ST, O and OU: case and
|
||||
repeated/edge spaces are normalized, RDN sequence order is preserved, and
|
||||
attribute order inside one multivalued RDN is ignored. Other name forms
|
||||
require exact encoded identity; semantic equivalence remains unestablished.
|
||||
`certificate` is `null` when the block has more than one `SignerInfo` (with
|
||||
nothing verified there is no single signer) or names a certificate it does
|
||||
not carry. A block that cannot be read (malformed ASN.1, an unsupported
|
||||
compression method, Zip64 sizes, a local header that disagrees with the
|
||||
central directory on the name, data-descriptor flag, CRC-32 or a size, as
|
||||
apksig refuses it, or over a bound) keeps its names and carries
|
||||
`decode_error`; it is never reported as an absent signer. apksig ignores
|
||||
the ZIP encryption flag (bit 0), so a block that sets it is read as stored:
|
||||
the PKCS#7 decoding decides (bytes that really are encrypted fail to
|
||||
decode), and a limitation names the block.
|
||||
apksig rejects the v1 signature of an archive whose central directory
|
||||
holds any name more than once ("Duplicate entry"). REA checks every entry:
|
||||
a limitation names the first few duplicates (JSON-quoted; a name over 256
|
||||
bytes is cut there and its stored length given) and counts the rest, and a
|
||||
signer whose block, `.SF` or `META-INF/MANIFEST.MF` name is duplicated
|
||||
(one signer per block name) carries `decode_error`
|
||||
`duplicate entry: <name>`, so a duplicated manifest marks every signer.
|
||||
A duplicate outside those names leaves the signers as declared.
|
||||
Names are compared as apksig compares them, as Java strings decoded from
|
||||
UTF-8: malformed bytes become U+FFFD, so `dup\xff` and `dup\xfe` are one
|
||||
name (apksigner: `Duplicate entry: dup�`). REA reproduces Java's
|
||||
replacement rather than WHATWG `TextDecoder`'s; the two differ for encoded
|
||||
surrogates (`ED A0..BF`), which Java replaces as one unit. This was checked
|
||||
against OpenJDK 25's `new String(bytes, UTF_8)` on about 300,000 malformed
|
||||
and well-formed sequences, including a leading byte-order mark, which Java
|
||||
keeps; other Java runtimes are not checked. The same
|
||||
decoding names v1 entries and compares a local header's name with the
|
||||
central directory's; as in apksig, a local name longer in bytes than the
|
||||
directory's is refused before decoding. apksig takes the record count from the classic
|
||||
end-of-central-directory record's 16-bit field and ignores a Zip64 count,
|
||||
so it reads only the first 65,535 records of a larger directory: the
|
||||
limitation then says so, counts as duplicates for apksig only names whose
|
||||
second copy lies within those records, and names separately those
|
||||
duplicated only past them. REA still reads v1 signing entries, and flags
|
||||
duplicated signing names, from every record, so past record 65,535 it
|
||||
reports signers apksig would not see.
|
||||
Bytes after the block's `ContentInfo` are ignored, as apksig's BER parser
|
||||
ignores them, and a limitation names the block and their count.
|
||||
- **Per signer:** the certificate's **SHA-256 fingerprint over the raw embedded
|
||||
DER** (the same value `apksigner --print-certs` prints), plus subject, issuer,
|
||||
serial, ISO 8601 validity and public-key algorithm when the X.509 parser can
|
||||
read the exact carried DER (each is `null` otherwise). The parser's returned
|
||||
DER must equal those bytes before readable facts are combined with their
|
||||
fingerprint; embedded PEM text and trailing data cannot select a different
|
||||
certificate. Raw signature algorithm IDs, declared
|
||||
digest hex and digest algorithms, signature lengths and public-key length; and for
|
||||
v3/v3.1 the signer's declared min/max SDK.
|
||||
- **Key rotation (v3/v3.1):** the declared proof-of-rotation lineage, oldest
|
||||
first. Each node carries its certificate and the capability flags **retained
|
||||
by that node's own certificate** after rotation (installed data, shared UID,
|
||||
permission, rollback, auth). A lineage attribute that cannot be decoded is
|
||||
reported with `decode_error`, never silently emptied.
|
||||
- **Source stamp:** whether a source-stamp block is present and the stamp
|
||||
certificate it declares (the V2 stamp block is preferred over V1, as apksig
|
||||
does). `certificate` is `null` when no stamp is present, or when the block
|
||||
could not be decoded and `decode_error` says why. Stamp signatures are not
|
||||
verified.
|
||||
- **Declared digest consistency:** for SHA-256/SHA-512 digests, REA recomputes
|
||||
the v2/v3 content digest and reports `matches_apk` per digest and
|
||||
`posture.self_consistent`. This only shows that the **declared** digest
|
||||
equals the digest of the current bytes. It is **not a signature
|
||||
verification**: anyone who changes the bytes can recompute the digest and
|
||||
write it into the signed data, and REA still reports `true` while
|
||||
`apksigner verify` rejects the APK. A changed byte with an unchanged
|
||||
declared digest reads `false`.
|
||||
- **Posture** (derived from the facts above):
|
||||
- `v1_only`: v1 is present and no v2/v3/v3.1 block is. The v1
|
||||
`X-Android-APK-Signed` marker is not read, so this cannot distinguish an
|
||||
APK signed only with v1 from one whose v2/v3 block was removed.
|
||||
- `schemes_agree_on_signer` (observed): every present block scheme presents
|
||||
the same _set_ of signer certificates. An extra signer in one scheme is
|
||||
disagreement; a signer without a certificate makes it `null` (unknown).
|
||||
A single present block scheme with known signers agrees with itself;
|
||||
v1 is compared separately below.
|
||||
- `signer_change_declared_by_rotation` (declared): the signers differ, but
|
||||
each appears in a lineage the APK declares. Read the two together: block
|
||||
schemes with different signers covered by a declared lineage show `false`
|
||||
/ `true`; differing signers without such a lineage show `false` / `false`.
|
||||
A lineage alone does not establish a change between block schemes. It is
|
||||
`null` when a signer's identity is unknown, or when the signers differ and
|
||||
a declared lineage could not be decoded.
|
||||
- `v1_signers_match_block_signers` (observed): the v1 signer certificates
|
||||
equal what the lowest block scheme that has signers (v2, else v3, else
|
||||
v3.1) expects v1 to carry. That is its signer set, except that a v3/v3.1
|
||||
signer declaring a rotation lineage expects the lineage's first
|
||||
(original) certificate, as apksig requires. `false` is notable: the JAR
|
||||
signature names a different key than the block signature. When v1 uses
|
||||
the lineage's original key and v3 its newer key, this comparison reads
|
||||
`true`. If v3 is the only present block scheme, agreement is `true` and
|
||||
declared inter-block signer change is `false` even though rotation is
|
||||
present. If v2 also uses the original key, agreement is `false` and the
|
||||
declared inter-block change is `true`. `null` when
|
||||
v1 or a populated block scheme is absent, any signer's identity is unknown (a
|
||||
`decode_error`, a `null` certificate or a `subjectKeyIdentifier`
|
||||
`SignerInfo`), or the lineage's original certificate cannot be
|
||||
identified. When identities or a lineage origin are unknown, limitations
|
||||
explain why; absence is shown in the scheme records.
|
||||
- `strongest_scheme`, `rotation_present`, `source_stamp_present`,
|
||||
`self_consistent`.
|
||||
|
||||
## What it does not claim
|
||||
|
||||
This is **declared identity and digest consistency, not trust**. No signature
|
||||
is cryptographically verified (including the v1 PKCS#7 signature and the
|
||||
`.SF`/`MANIFEST.MF` digests, so a v1 signer is reported even where `keytool`
|
||||
or `apksigner` reject the archive), certificate chains and revocation are not
|
||||
evaluated, and nothing here says an app is authentic. `self_consistent: true`
|
||||
means the declared digest equals the digest of the current bytes; it is not a
|
||||
signature verification, and it does not prove who made the bytes. Rotation-lineage node signatures are not verified either, so a lineage is
|
||||
what the APK _declares_: an attacker who re-signs an APK can declare any
|
||||
lineage, which is why `signer_change_declared_by_rotation` is kept apart from
|
||||
the observed agreement. SHA-256 identifies exact bytes, not authenticity.
|
||||
|
||||
Not yet read: v4 `.apk.idsig` (a sibling file), verity (`0x0421`/`0x0423`/`0x0425`) digests (reported with
|
||||
`matches_apk: null`), and the v1 `X-Android-APK-Signed` marker.
|
||||
|
||||
## Bounds and errors
|
||||
|
||||
The archive is read once, bounded at 512 MiB, without following symlinks and
|
||||
with change detection. ZIP tail offsets, the central-directory size and signing
|
||||
block spans used by the reader are checked against their containing sections,
|
||||
including the complete central-directory extent before ZIP end records. A process-wide queue permits
|
||||
one active whole-file inspection and eight pending scans across provider
|
||||
instances; pending scans retain no APK bytes and can be cancelled with their
|
||||
original reason. Further requests receive a capacity error. The active slot
|
||||
is held through parsing and cleanup. Signers per scheme, v1 signers and
|
||||
lineage nodes are capped at 32 each as resource admission limits, not format
|
||||
maxima (apksig separately accepts at most 10 v1 signers). Each v1 block is
|
||||
inflated to at most 1 MiB, and its BER is read within 32 levels of nesting and
|
||||
65,536 elements, with at most 32 certificates and 32 `SignerInfo`s; BER
|
||||
indefinite lengths and constructed OCTET STRING fields are accepted. Embedded
|
||||
X.509 certificates retain their DER requirement. The report's size is
|
||||
bounded from above by those counts before anything is materialised, and the exact
|
||||
serialized size of the report together with its limitations (which repeat
|
||||
archive names) is then checked against an 8 MiB output budget (the exact check
|
||||
is authoritative); an over-budget report is refused, not truncated.
|
||||
|
||||
Signing blocks admit at most 4096 ID/value pairs; at most 4096 signing-relevant
|
||||
META-INF entries are retained. These fixed counts, signer/lineage counts and the
|
||||
report byte budget return nonretryable `resource_constraint` errors naming the
|
||||
effective allowance. This release has no runtime override for them. Waiting for
|
||||
an active scan cannot raise a fixed count, and complete CLI JSON cannot raise the
|
||||
report budget. Occupied scan-queue capacity remains retryable after a scan finishes.
|
||||
|
||||
The whole-file reader allocates the selected snapshot and bounded read scratch;
|
||||
total process memory exceeds the selected file's size. It also computes the complete
|
||||
file SHA-256, so inspection is not header-only. Content digests are recomputed
|
||||
at most once per SHA-256/SHA-512 algorithm during one inspection. Input,
|
||||
inflation, decoded-record and serialized-output bounds keep work finite;
|
||||
resource refusals are distinct from malformed format errors.
|
||||
|
||||
If the stable read completes but closing its descriptor fails or is cancelled,
|
||||
the error retains `details.partial_observation` with the selected path, complete
|
||||
file SHA-256 and byte count. These facts describe the acquired file; they do not
|
||||
establish any decoded signing facts. Failed cleanup remains visible until the
|
||||
provider releases the descriptor on a cleanup retry.
|
||||
|
||||
A file that is not a ZIP is refused as an unsupported target. A damaged ZIP
|
||||
(local headers but no end-of-central-directory), a central directory that
|
||||
cannot be walked or whose declared record count does not account for all of
|
||||
its bytes, or a malformed signing block or signer is an input error naming the
|
||||
offset or scheme and reason. Zip64 tail damage includes a missing required
|
||||
locator, an impossible locator disk/count pair, a fixed end record that does
|
||||
not fit before its locator, or an incorrect end-record signature. These are
|
||||
input errors, not evidence of an unsupported format. Coherent multi-disk
|
||||
locator metadata and 64-bit offsets/counts/sizes beyond the reader's safe
|
||||
integer range remain unsupported. The locator offset is disk-relative, so
|
||||
multi-disk metadata is checked before applying local file bounds. This follows
|
||||
the record fields in [PKWARE APPNOTE 6.3.10, sections 4.3.14–4.3.15](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT);
|
||||
it does not establish full validation of Zip64 versions or extensible data.
|
||||
Signer framing includes signature records, the public-key field and matching
|
||||
signed/outer v3 SDK ranges. Suffix bytes beyond known
|
||||
fields are counted as uninterpreted, as Android readers may ignore extensions;
|
||||
their presence does not establish malformed signing.
|
||||
When the central directory is not immediately followed by
|
||||
the end-of-central-directory record (a declared-size mismatch or a Zip64
|
||||
record), the content digest is not recomputed and a limitation says so. A
|
||||
rotation attribute that cannot be decoded still counts as `rotation_present`
|
||||
(the attribute exists) and carries its `decode_error`.
|
||||
|
||||
Verified by the `verify:apk-signature` real lane. It builds a real minimal APK
|
||||
with `aapt2`, signs it with the SDK's `apksigner` (default v2+v3, v1-only with
|
||||
RSA and EC keys, v1+v2+v3 on a legacy target, a rotated key lineage with and
|
||||
without v1, rotation carried by v3.1, a source stamp, and a copy
|
||||
with one changed byte), and cross-checks REA against
|
||||
`apksigner verify --print-certs`, `apksigner lineage --print-certs` and
|
||||
`keytool -printcert -jarfile` through the CLI and MCP.
|
||||
|
||||
The real-toolchain CI lane uses pinned, checksum-verified Build Tools 36.1.0
|
||||
and platform 36 oracle files in its job workspace. It requires the existing
|
||||
host JDK and runs `npm run verify:apk-signature -- --require-tools`; absent
|
||||
oracles fail rather than silently establishing provider coverage. REA itself
|
||||
requires none of these external tools.
|
||||
@@ -8,7 +8,7 @@ installs Apktool or a Java runtime.
|
||||
|
||||
## Supply tools
|
||||
|
||||
Use Apktool 2.7.0 or newer (verified with 2.7.0 and 3.0.3 on Linux) and a Java runtime the
|
||||
Use Apktool 2.7.0 or newer (verified with 2.7.0) and a Java runtime the
|
||||
launcher can find. Select the launcher through the environment:
|
||||
|
||||
```sh
|
||||
@@ -24,30 +24,6 @@ missing launcher reports `capability_unavailable` with the
|
||||
`binary_session` uses the same host probe for tool availability; it does not
|
||||
validate a particular APK or guarantee that the selected input can be decoded.
|
||||
|
||||
Client inspection and decode results disclose the following reported-version
|
||||
advisory windows without refusing otherwise compatible launchers:
|
||||
|
||||
| Advisory | Reported version window | Host scope |
|
||||
| ----------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ---------- |
|
||||
| [CVE-2024-21633](https://github.com/iBotPeaches/Apktool/security/advisories/GHSA-2hqv-2xv4-5h5w) | Before 2.9.2 | All |
|
||||
| [GHSA-vgwr-4w3p-xmjv](https://github.com/iBotPeaches/Apktool/security/advisories/GHSA-vgwr-4w3p-xmjv) | Before 2.9.3 | Windows |
|
||||
| [CVE-2026-39973](https://github.com/iBotPeaches/Apktool/security/advisories/GHSA-m8mh-x359-vm8m) | 3.0.0 release line through versions before 3.0.2 | All |
|
||||
|
||||
The 2026 advisory names 3.0.1 and its 3.0.2 fix. Inclusion of 3.0.0 is
|
||||
source-derived: [tagged 3.0.0](https://github.com/iBotPeaches/Apktool/blob/f770b39e425af4fa6ac4ca1e9ea6c4824339c6d3/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/ResTypeSpec.java)
|
||||
passes type names through unchanged; the [3.0.2 patch](https://github.com/iBotPeaches/Apktool/commit/65dd8480dfcb63068562ffaa527f71bb0a9f772c)
|
||||
validates those names at their owner before they reach the output-path producer.
|
||||
Prereleases before the fixed release are conservatively disclosed; the exact
|
||||
`-dirty` downstream marker is compared as a release while preserving its spelling.
|
||||
This is version guidance, not verification of a particular prerelease or decoder.
|
||||
|
||||
A launcher version does not prove the decoder's contents, execution platform,
|
||||
downstream backports, or overall safety. Windows applicability uses the selected
|
||||
local host; an opaque wrapper may run another engine. A missing warning means
|
||||
only that none of these version/host windows matched. Verify the selected
|
||||
decoder's patches before decoding untrusted APKs; the compatibility floor above
|
||||
is not a security recommendation. REA does not fetch advisories at runtime.
|
||||
|
||||
On Windows, a `.bat` or `.cmd` launcher (such as Chocolatey's `apktool.bat`)
|
||||
runs through an explicit `cmd.exe /d /s /c` call. That call has no AutoRun,
|
||||
no delayed expansion, and quotes every argument. A launcher path that
|
||||
@@ -62,80 +38,30 @@ reports `capability_unavailable`.
|
||||
rea inspect-apktool-client
|
||||
rea decode-android-resources /targets/Example.apk
|
||||
rea decode-android-resources /targets/Example.apk --locale de
|
||||
rea decode-android-resources /targets/Example.apk --locales de --locales pt-BR
|
||||
rea decode-android-resources /targets/Example.apk --locales de --locales pt-BR --string-tables-only
|
||||
rea decode-android-resources /targets/Example.apk --no-strings
|
||||
rea decode-android-resources /targets/Example.apk --decode-timeout-ms 900000
|
||||
```
|
||||
|
||||
Equivalent MCP methods are `inspect_apktool_client` and
|
||||
`decode_android_resources`. The decode request takes `path`, an optional
|
||||
primary `locale` (project `res/values-<locale>/strings.xml` instead of the default
|
||||
table), `locales` (repeatable array of additional localized string tables to project
|
||||
from the same single decode pass), and `include_strings` (default true). Regional locales such as `pt-BR`
|
||||
select Android's `values-pt-rBR`. A three-digit region such as `es-419`
|
||||
selects `values-b+es+419`; `es-r419` is rejected because Android's legacy
|
||||
`-r` form requires a two-letter region. Android qualifiers such as `pt-rBR`
|
||||
and `b+zh+Hans+CN` may also be supplied directly.
|
||||
|
||||
Set `string_tables_only: true` (CLI `--string-tables-only`) to return string
|
||||
entries only inside `string_tables`, including when selecting a single table.
|
||||
The top-level `strings` array is then empty by request; it does not establish
|
||||
that the primary table is empty. Table status, counts and coverage still describe
|
||||
the examined source. This avoids repeating primary entries in multi-locale
|
||||
responses and charges the aggregate output budget for one copy per table.
|
||||
Omitting the option preserves existing responses. `include_strings: false`
|
||||
still suppresses all string projection, including in this mode.
|
||||
`locale` (project `res/values-<locale>/strings.xml` instead of the default
|
||||
table), and `include_strings` (default true). Regional locales such as `pt-BR`
|
||||
select Android's `values-pt-rBR`; Android qualifiers such as `pt-rBR` and
|
||||
`b+zh+Hans+CN` may also be supplied directly.
|
||||
|
||||
## Interpret the result
|
||||
|
||||
- **An owned APK snapshot is digested and decoded.** `target.sha256` and
|
||||
`target.bytes` bind the observation to the exact APK that was decoded.
|
||||
The Evidence subject carries the same snapshot digest and caller-selected
|
||||
path, including when the original file changes during decoding.
|
||||
A change during snapshot acquisition reports retryable `artifact_changed`;
|
||||
wait for a stable input before retrying. Host read permission failures report
|
||||
`access_denied`, while a missing or non-regular selected path reports
|
||||
`invalid_request` with the rejected `path` field.
|
||||
- **Metadata comes from apktool.yml as apktool printed it.** Version codes
|
||||
keep their own spelling (`'7'` versus `7`); `package_name` is read from
|
||||
the decoded manifest, not inferred.
|
||||
The `version: 2.9.0`-or-later header that Apktool 2.9+ and 3 emit selects
|
||||
their single-line string dialect: literal hashes, single quotes, empty
|
||||
strings, and the text `null` stay part of the value; producer backslashes
|
||||
and Unicode escapes decode once. Older/tagged documents retain YAML scalar
|
||||
semantics; double-quoted values also retain YAML string semantics.
|
||||
This distinction follows the released
|
||||
[2.9.0 metadata writer](https://github.com/iBotPeaches/Apktool/blob/b64e257c9b689eb9630ad0241f2fe4f4a749955b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/VersionInfo.java#L38-L42),
|
||||
[3.0.3 metadata writer](https://github.com/iBotPeaches/Apktool/blob/18b5e99cb56ff9451e8aa55b065dcf5bbd616975/brut.apktool/apktool-lib/src/main/java/brut/androlib/meta/VersionInfo.java#L49-L55)
|
||||
and [string escape owner](https://github.com/iBotPeaches/Apktool/blob/18b5e99cb56ff9451e8aa55b065dcf5bbd616975/brut.j.yaml/src/main/java/brut/yaml/YamlStringEscapeUtils.java#L55-L116).
|
||||
- **The manifest is the decoded text**, bounded at 512 KiB and reported as
|
||||
truncated when larger.
|
||||
- **Strings are projected verbatim.** Escaped entities stay escaped, and
|
||||
entries containing nested markup (`<b>`, `<xliff:g>`) are counted as
|
||||
unparsed rather than flattened — the limitation names the examined count.
|
||||
Malformed XML and a wrong document root are whole-document projection
|
||||
failures: the result is partial, the failure reason is disclosed, and entry
|
||||
counts remain unknown. They are not counted as one unsupported string.
|
||||
The default table is bounded to 2000 entries with partial coverage when more
|
||||
exist. `locales` lists observed regular `strings.xml` tables whose exact
|
||||
`values-<locale>` directory is selectable by the request's locale grammar.
|
||||
Locale suffixes allow up to 23 ASCII characters, derived from Apktool's
|
||||
language, script, region, and variant fields; for example,
|
||||
`b+sr+Latn+RS+REVISED` selects that exact table.
|
||||
Other table directories, including combined qualifiers such as
|
||||
`values-en-rUS-v21` and `values-mcc310-fr`, are named in limitations with
|
||||
their source spelling. They are not advertised as selectable locales;
|
||||
REA does not infer Android's runtime resource matching. Non-regular
|
||||
strings-table paths are disclosed with partial coverage. Discovery uses
|
||||
the same bounded traversal as the file count; filesystem failures remain
|
||||
failures rather than being interpreted as missing tables.
|
||||
When multiple tables are requested with `locales`, `string_tables` records
|
||||
each projected table with its locale, status (`present`, `missing`,
|
||||
`oversized`, `malformed`, or `skipped`), entry counts, and coverage,
|
||||
bounded at an aggregate budget of 10,000 strings and 32 MiB XML. When `locales`
|
||||
is omitted and `string_tables_only` is not true, legacy single-table payloads are preserved
|
||||
without adding `string_tables`.
|
||||
unparsed rather than flattened — the limitation names the count. The
|
||||
default table is bounded to 2000 entries with partial coverage when more
|
||||
exist; `locales` lists every `values-<locale>` that has a strings table.
|
||||
- **Smali is skipped by design** (`apktool d --no-src`): Java-source
|
||||
recovery belongs to the JADX family. Resource smali edits and rebuild are
|
||||
a follow-up lane with mutation authority.
|
||||
@@ -147,24 +73,17 @@ still suppresses all string projection, including in this mode.
|
||||
|
||||
## Resource and lifecycle limits
|
||||
|
||||
- The decode wall clock is `decode_timeout_ms` (CLI `--decode-timeout-ms`).
|
||||
Omitted means 600000 ms. The integer range is 1 to 2147483647. A change
|
||||
applies on the next decode. It does not raise the 4194304-byte (4 MiB)
|
||||
output capture budget. That capture budget has no runtime override. A
|
||||
stopped decode reports `provider_timeout` and names the effective
|
||||
milliseconds; an exceeded capture budget reports `resource_constraint`.
|
||||
- The version probe waits 20000 ms and retains 65536 bytes. This release has
|
||||
no runtime override for that probe. `decode_timeout_ms` does not raise it.
|
||||
- The decode runs with a 600 s deadline and a 4 MiB output capture budget;
|
||||
a stopped decode reports `provider_timeout` and exceeded budgets report
|
||||
`resource_constraint`.
|
||||
- Manifest reads are bounded to 512 KiB before decoding UTF-8. Strings XML
|
||||
reads are bounded to 8 MiB; larger documents are skipped with partial
|
||||
coverage and an explicit limit, rather than reported as missing.
|
||||
- `decoded_file_count` is capped at 100,000 files; the cap is reported in
|
||||
the limitations when hit.
|
||||
- An ordinary nonzero decode exit surfaces as `unsupported_target` naming
|
||||
the caller-selected APK, with Apktool's diagnostic and captured output.
|
||||
Launcher startup, ownership preparation, signal termination and incomplete
|
||||
output collection remain provider failures; they do not establish that the
|
||||
selected APK is unsupported.
|
||||
- Apktool's own decode failures (`AndrolibException`, directory traversal
|
||||
refusals) surface as `unsupported_target` with apktool's reason preserved
|
||||
verbatim.
|
||||
|
||||
## Real verification
|
||||
|
||||
@@ -175,16 +94,3 @@ locales — launcher version, on-disk digest agreement, apktool.yml metadata
|
||||
(1.2.3, SDK 24–34), manifest package agreement, default and `de` string
|
||||
tables projected. Parser goldens in the test suite come from the same
|
||||
decode. See `docs/testing.md` for the lane summary.
|
||||
|
||||
Multi-locale and table-only projection were also verified through real CLI and
|
||||
stdio MCP workflows on Linux with Apktool 3.0.3/JDK 21 against NewPipe 0.29.1,
|
||||
ApiDemos 6.0.18 and Appium Settings 8.2.1. Independent XML projection checks,
|
||||
exact facts/Evidence parity and bounded task costs are recorded in the
|
||||
[shared-decode and table-only checkpoint](https://github.com/morluto/rea/blob/42e1a17d6e6c0487f17be0bae248553c4caba7f7/experiments/analysis-efficiency/apktool-string-tables.md).
|
||||
This qualifies selected resource facts, not application execution or independent
|
||||
binary resource-decoder completeness.
|
||||
|
||||
If the selected APK changes while its private snapshot is copied, acquisition
|
||||
returns `artifact_changed`; request-schema correction cannot recover a stable
|
||||
snapshot. Launcher spawn permission denial is distinct from a missing launcher,
|
||||
and configuration diagnostics retain the selected command and system error.
|
||||
@@ -60,15 +60,6 @@ states that absence is unknown. Component arrays remain complete when candidate
|
||||
hypotheses are omitted; component arrays still include every component from
|
||||
the supplied inventory pages, and overall coverage is `partial` in that case.
|
||||
|
||||
Inventoried files whose bytes were not read, such as encrypted archive members,
|
||||
are listed in `unavailable_components` with their original path, format,
|
||||
encryption flag, limitations, and null content identity. The same application-root
|
||||
and AppleDouble attribution rules apply. These files make overall coverage
|
||||
`partial`; component absence is unknown even when `inventory_complete` is true.
|
||||
Directories have no file bytes and symlinks remain in `symlinks`, so neither is
|
||||
an unavailable file component. The additive field participates in `projection_id`,
|
||||
including when empty, so Apple projection identifiers change.
|
||||
|
||||
## Application roots
|
||||
|
||||
| Inventory root | Application root |
|
||||
@@ -77,15 +68,6 @@ including when empty, so Apple projection identifiers change.
|
||||
| `.app` directory | `.`, the inventoried bundle itself |
|
||||
| Directory, ZIP, or DMG containing apps | Each outermost `<Name>.app` followed by `Contents/` |
|
||||
|
||||
File and slice occurrences supply component endpoints. Directory tree identities
|
||||
remain in source inventory and bundle anatomy, and links remain in `symlinks`.
|
||||
|
||||
Root and layout discovery use known directories and ancestors implied by deeper
|
||||
inventory paths. A leaf file or symlink named `*.app`, `Contents`, or `Versions`
|
||||
does not establish that directory. Directory and authenticated-file identity are
|
||||
selected before applying anatomy case preferences, so a file cannot shadow a
|
||||
folded directory name or a directory shadow a metadata/signature file.
|
||||
|
||||
ZIP archives made with `ditto -c -k --keepParent` report `<Name>.app`. A DMG
|
||||
reports `<image>.dmg/<Volume>/<Name>.app`; DMG child inventory needs the native
|
||||
macOS mounting adapter. When a DMG has only its root identity, the projection is
|
||||
@@ -101,8 +83,8 @@ limitation reports how many inventoried entries were left out. IPA projection
|
||||
still lists every archive component, including `SwiftSupport/`.
|
||||
|
||||
`platforms` lists `macos` for application roots with `Contents/`. It lists `ios`
|
||||
for shallow application roots that have an `Info.plist` at their root. An
|
||||
application root with neither, such as an empty inventory, has no platform.
|
||||
for shallow application roots that have an `Info.plist`. An application root
|
||||
with neither, such as an empty inventory, has no platform.
|
||||
|
||||
## macOS bundle anatomy
|
||||
|
||||
@@ -149,11 +131,6 @@ directories rather than `Versions/Current`. A framework's `info_plist_path` is
|
||||
because `Versions/Current` decides which one applies) or when its single
|
||||
version directory has no `Resources/Info.plist`.
|
||||
|
||||
A `shallow` bundle's `info_plist_path` is its root `Info.plist`. Without one,
|
||||
it is `Resources/Info.plist`: a flattened macOS framework keeps its binary,
|
||||
`_CodeSignature/`, and `Resources/` directly in the `.framework`, like a single
|
||||
`Versions/<version>/` directory without the `Versions/` wrapper.
|
||||
|
||||
AppleDouble sidecar files (`._*` and `__MACOSX/`) are inventory facts, but they
|
||||
describe neighbouring files. They are excluded from roots, bundle roles, and the
|
||||
anatomy components.
|
||||
@@ -161,10 +138,6 @@ anatomy components.
|
||||
The projection does not read plists. Use `inspect_plist` on a bundle's
|
||||
`info_plist_path` or a launchd plist to read `CFBundleExecutable`, identifiers,
|
||||
`SMPrivilegedExecutables`, `BundleProgram`, URL schemes, and usage descriptions.
|
||||
MCP calls require an existing `open_binary` or `open_bundle_resources` session,
|
||||
including when `inspect_plist.path` is explicit. For a bundle without a resolved
|
||||
executable, open its resource session first. The CLI `rea inspect-plist <path>`
|
||||
opens the selected artifact for its one invocation.
|
||||
Use `inspect_signature` on an executable candidate for code-signing claims.
|
||||
|
||||
## Dylib load resolution
|
||||
@@ -189,21 +162,8 @@ rea trace-dylib-resolution MyApp.app --root Contents/MacOS/MyApp --architecture
|
||||
|
||||
Load commands are parsed in TypeScript from Mach-O and FAT headers, including
|
||||
the `dylib_use_command` encoding newer linkers emit for flags such as delayed
|
||||
initialization, and `LC_LAZY_LOAD_DYLIB_INFO` (`0x3a`). That command stores its
|
||||
install name in a linkedit payload rather than in the load command itself.
|
||||
Its flag and chain-start image offsets must lie in the slice VM range, which
|
||||
is dyld's one-page floor extended by every segment except `__LINKEDIT`,
|
||||
measured from the `__TEXT` vmaddr. Payload strings must start after the 24-byte
|
||||
header. The load-path NUL must occur before the payload's final byte, matching
|
||||
dyld's scan. Distinct payload bytes and payload-range lookups stay within the 16 MiB
|
||||
limit, and a file range already read is reused. String scanning and UTF-8
|
||||
checks count every reference, including repeats and overlaps, and stop at that
|
||||
same aggregate. A slice retains at most 1024 of these commands; further copies are
|
||||
unsupported instead of becoming one traced edge each. Symbol references,
|
||||
including repeats, stop at 65536 per slice. A larger table is unsupported
|
||||
instead of caching one string per offset.
|
||||
Nothing is executed and no Apple tool is required, so the tool also works off
|
||||
macOS.
|
||||
initialization. Nothing is executed and no Apple tool is required, so the tool
|
||||
also works off macOS.
|
||||
|
||||
Resolution follows dyld:
|
||||
|
||||
@@ -218,69 +178,34 @@ Resolution follows dyld:
|
||||
already loaded image reuses it.
|
||||
- Symlinks such as `Versions/Current` are followed inside the analyzed root.
|
||||
An escape through `..` or an absolute link is `escapes-target`.
|
||||
- Dyld path components are POSIX names. On Windows hosts, link text uses `\`
|
||||
as its separator, and a dyld component that contains `\` or `:` cannot name
|
||||
a file in the analyzed root, so its candidate is `absent`.
|
||||
|
||||
Each edge lists every candidate path tried, with its outcome: `resolved`,
|
||||
`absent`, `not-mach-o`, `malformed`, `unsupported` (recognized Mach-O outside
|
||||
parser coverage, such as big-endian images), `architecture-missing`,
|
||||
`not-loadable`, `platform-mismatch`, `outside-target`, `escapes-target`, or
|
||||
`undetermined`. `not-loadable` means a
|
||||
matching-architecture Mach-O image was found but it is not a dylib and cannot
|
||||
satisfy a dylib dependency. `platform-mismatch` means a matching-architecture
|
||||
dylib was found but its observed platform is incompatible with the process.
|
||||
These outcomes reject that candidate; dyld search may continue and resolve a
|
||||
later candidate. Candidate outcomes describe individual search paths, while
|
||||
`resolution.status` describes the final edge. For each selected process-root
|
||||
slice, candidate slices are matched by CPU type and dyld's subtype preference
|
||||
order, independently of FAT table order. An ARMv7s root slice prefers ARMv7s,
|
||||
then ARMv7; other known ARM32 subtypes use their compatibility rows from
|
||||
[Apple's dyld source](https://github.com/apple-oss-distributions/dyld/blob/dyld-852.2/src/dyld2.cpp#L2811-L3105).
|
||||
An `x86_64h` process also loads generic `x86_64`. Unknown ARM32 subtype policy
|
||||
remains undetermined when ARM32 candidate slices exist. This is a static model
|
||||
keyed to the selected root slice; it does not observe the target device's host
|
||||
CPU or prove which slice the device will execute. The edge's `resolution` is
|
||||
one of:
|
||||
`outside-target`, `escapes-target`, or `undetermined`. Slices follow dyld's
|
||||
graded architectures, so an `x86_64h` process also loads `x86_64`. The edge's `resolution` is one of:
|
||||
|
||||
| Status | Meaning |
|
||||
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `resolved` | An image inside the analyzed root loads, and no earlier candidate was undeterminable. |
|
||||
| `conditional` | An in-root image may load, but an earlier outside candidate, unmodeled environment input, or unknown platform compatibility can change which image dyld loads. |
|
||||
| `unresolved` | Every candidate is definitively absent or unusable inside the root. |
|
||||
| `undetermined` | No candidate resolves inside the root, and some candidate lies outside it or depends on the environment. |
|
||||
| Status | Meaning |
|
||||
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `resolved` | An image inside the analyzed root loads, and no earlier candidate was undeterminable. |
|
||||
| `conditional` | An image inside the root resolves, but an earlier candidate outside the root (for example `/usr/lib/swift`) would win if it exists on the running system. |
|
||||
| `unresolved` | Every candidate is definitively absent or unusable inside the root. |
|
||||
| `undetermined` | No candidate resolves inside the root, and some candidate lies outside it or depends on the environment. |
|
||||
|
||||
A `conditional` image is still traced, because it is an in-root candidate
|
||||
that may load. Every edge below it has `loader_conditional: true`, and a
|
||||
request that reuses it by install name is `conditional` too. An in-root dylib
|
||||
with no platform load command remains unknown unless the root is an executable
|
||||
with observed macOS platform and the candidate's CPU type is x86_64, i386, or arm64. In that macOS context,
|
||||
[Apple's dyld](https://github.com/apple-oss-distributions/dyld/blob/fd8d0c4d52320ebf64db34f3cb280310d905c5ae/mach_o/UnsafeHeader.cpp#L1137-L1145) supplies an implicit macOS platform to legacy dylibs on those CPU
|
||||
types. The candidate records `platform_basis: "macos-dyld-implicit"`; the
|
||||
reported slice fields `platform: null` and `platforms: []` remain the parsed
|
||||
facts. For other process contexts the platform remains unknown, coverage is
|
||||
partial, and the candidate is not treated as incompatible. If a later known
|
||||
candidate resolves, it remains the traced image with conditional resolution;
|
||||
the trace does not claim the unknown candidate's dependency graph. Findings on
|
||||
conditional edges say that they apply only if the conditional image loads.
|
||||
A `conditional` image is still traced, because it is the image the analyzed
|
||||
root supplies. Every edge below it has `loader_conditional: true`, and a
|
||||
request that reuses it by install name is `conditional` too. Findings on those
|
||||
edges say that they apply only if the conditional image loads.
|
||||
|
||||
Absolute install names that name a path beneath the analyzed root are checked
|
||||
within that root, with symlinks and `..` resolved by the same bounded tree
|
||||
resolver. Absolute paths outside the root, such as
|
||||
`/usr/lib/libSystem.B.dylib`, are `outside-target`; on macOS 11 and later most
|
||||
system libraries live in the dyld shared cache rather than on disk, so REA does
|
||||
not check them against the host. Mach-O path strings are not converted from
|
||||
`file:` URLs or looked up through unrelated host paths.
|
||||
Absolute install names such as `/usr/lib/libSystem.B.dylib` are
|
||||
`outside-target`. On macOS 11 and later most of them live in the dyld shared
|
||||
cache rather than on disk, so REA does not check them against the host.
|
||||
|
||||
`findings` are derived, not observed:
|
||||
|
||||
- `required-load-unresolved`, `weak-load-unresolved`, and `lazy-load-unresolved`.
|
||||
`LC_LAZY_LOAD_DYLIB` and `LC_LAZY_LOAD_DYLIB_INFO` dependencies are resolved
|
||||
but not traversed, because dyld loads them on first use; an unresolved one is
|
||||
not a launch failure. `LC_LAZY_LOAD_DYLIB_INFO` has no compatibility version,
|
||||
so those fields are null. Its may-be-missing flag is reported as `weak`, and
|
||||
an unresolved dependency with that flag is not a first-use failure: dyld
|
||||
continues if the image is absent.
|
||||
`LC_LAZY_LOAD_DYLIB` dependencies are resolved but not traversed, because dyld
|
||||
loads them on first use; an unresolved one is not a launch failure.
|
||||
- `earlier-rpath-candidate-absent`: a compatible Mach-O placed at an earlier
|
||||
modeled search path could take precedence, subject to unmodeled search inputs
|
||||
and code-signing library validation. These findings do not establish which
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
# Apple IPS crash reports
|
||||
|
||||
`inspect_apple_ips` / `inspect-apple-ips` answers whether one selected local
|
||||
Apple `.ips` file is a crash report (`bug_type` string `"309"`), which process,
|
||||
exception, and termination facts that report records, which threads and
|
||||
`usedImages` rows it lists, and which documented crash classes its strings
|
||||
match. It reads the file without executing a target and does not require Xcode,
|
||||
atos, a disassembler, or pwntools.
|
||||
|
||||
```bash
|
||||
rea inspect-apple-ips /absolute/path/to/report.ips --json
|
||||
```
|
||||
|
||||
The CLI resolves relative paths against the caller's working directory. MCP
|
||||
requires an absolute filesystem path and preserves that exact selection:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "inspect_apple_ips",
|
||||
"arguments": { "path": "/artifacts/report.ips" }
|
||||
}
|
||||
```
|
||||
|
||||
## Observations
|
||||
|
||||
The inline Evidence result contains the artifact path, SHA-256, and byte length.
|
||||
`metadata` is the first-line JSON object. `bug_type` stays a string. The crash
|
||||
body is parsed only when that string is `"309"`. Other types, including `"288"`
|
||||
stackshots, return no crash result.
|
||||
|
||||
`process`, `exception`, `termination`, `faulting_thread`, `threads`, and
|
||||
`images` reproduce documented body fields when present, including each image's
|
||||
`name` and each frame's `symbolLocation` (as `symbol_location`, the byte offset
|
||||
from the start of `recorded_symbol`). `termination` keeps the terminating
|
||||
process as `by_proc` and `by_pid` and its `flags`; `process.parent_pid` keeps
|
||||
`parentPid` next to `parent`; and `metadata` keeps `build_version` and
|
||||
`platform`. Integers keep their
|
||||
decimal literals and a hexadecimal form, including arm64e registers above
|
||||
2^53. Frame roles follow the recorded frame index: row zero of the resolved
|
||||
faulting thread is `faulting_instruction`, row zero of other threads is
|
||||
`current_instruction`, and later rows are `return_address`. Each runtime
|
||||
address uses that row's own image base and offset. The recorded `inline` flag
|
||||
does not supply a missing offset or authorize borrowing another row's address.
|
||||
When `faultingThread` is missing or does not resolve to a recorded thread
|
||||
object, row-zero roles are `unknown`; the recorded lookup remains available.
|
||||
A translated process records Rosetta's registers in a nested
|
||||
`threadState.rosetta` object; each one is reported as `rosetta.<name>` and
|
||||
counts toward the register budget. A [public ReportCrash `.ips` log](https://github.com/Mortennn/Dozer/issues/189)
|
||||
shows `rosetta.tmp2`, `tmp1`, and `tmp0` beside ordinary x86 registers. The
|
||||
test fixture quotes that register excerpt inside an authored report body; it
|
||||
does not generate a new Rosetta crash during the test.
|
||||
Observed JSON strings retain `""`; `null` means the field was missing or JSON
|
||||
`null`. This includes image paths, frame symbols, process text, and the older
|
||||
string `osVersion` form. Unlisted JSON keys and generic register names also
|
||||
retain empty spellings. An empty image architecture remains recorded as `""`
|
||||
while `uuid_unknown` stays true, since that spelling does not establish an
|
||||
architecture. An empty UUID is malformed, so its exact spelling appears in
|
||||
`coverage.type_mismatches` and its canonical UUID remains unknown.
|
||||
`process.os_version` keeps the producer's dictionary (`train`, `build`,
|
||||
`release_type`, `is_embedded`) or the older string form. Image UUIDs are uppercase 8-4-4-4-12; an all-zero UUID
|
||||
or a missing or empty `arch` is `uuid_unknown`. Image `source` letters keep their
|
||||
documented meaning (`P` process, `S` shared cache, `C` shared cache library,
|
||||
`K` kernel, `U` kernel cache, `T` kernel text exec, `A` absolute). Keys the
|
||||
documentation does not list remain in `unlisted_keys` or `unlisted_body_keys`.
|
||||
|
||||
Each frame's `image_index` refers to its entry in `images`. Image architecture,
|
||||
UUID and `uuid_unknown` are retained once on that image. Resolve the reference
|
||||
when using those facts; an absent or out-of-range index leaves the image
|
||||
unknown. This avoids repeating image text for every stack frame.
|
||||
|
||||
A present value whose JSON type differs from the documented one is never read
|
||||
as absent: it is listed in `coverage.type_mismatches` with its path, the
|
||||
expected type, and the observed type, and `coverage.status` becomes
|
||||
`partial`. A crash body without `threads` or `usedImages` is also `partial`.
|
||||
Malformed image UUID strings likewise make coverage partial; their exact spelling
|
||||
is retained in the mismatch's `recorded_value`, while the canonical image UUID
|
||||
remains unknown. Both 32 hexadecimal digits and canonical hyphenated UUIDs are accepted.
|
||||
Unresolved thread/image references also make coverage partial. A rejected
|
||||
integer lookup keeps its exact decimal literal in `recorded_integer`, including
|
||||
negative values and values above uint32. Valid uint32 lookup values remain in
|
||||
`faulting_thread` or `image_index` even when the referenced row is unavailable.
|
||||
|
||||
`classification` matches only documented SIGKILL codes (compared by their
|
||||
canonical hexadecimal form), `EXC_GUARD` subtypes,
|
||||
pointer-authentication suffixes, and dyld missing-library strings. A report
|
||||
that matches none of those rules is `unclassified`.
|
||||
|
||||
Opened Mach-O and dSYM UUID joins remain unobserved. Symbol names printed
|
||||
inside the report are recorded claims and are not verified. Linux ELF cores
|
||||
use the separate [recorded crash](recorded-crashes.md) workflow.
|
||||
|
||||
## Limits and failures
|
||||
|
||||
The selected file is limited to 1 MiB by default (16 MiB maximum). At most 256
|
||||
threads, 8192 frames, 1024 `usedImages` rows, and 16384 `threadState`
|
||||
register slots are examined by default (4096 / 65536 / 4096 / 262144
|
||||
maximum). Budgets are checked before each row is parsed, and body members the
|
||||
report does not interpret are syntax-checked without being kept. Fixed
|
||||
allowances bound JSON nesting (64 levels), unlisted key names per object
|
||||
(1024), and `termination.reasons` entries (256). Malformed metadata, a
|
||||
truncated crash body, invalid UTF-8, a NUL in the text, and exhausted budgets
|
||||
fail without partial success.
|
||||
|
||||
`max_integer_text_bytes` (`--max-integer-text-bytes` in the CLI) separately
|
||||
bounds retained decimal and hexadecimal integer text, including every derived
|
||||
runtime address and rejected lookup literal. Its default is 8 MiB of UTF-16 text storage and its maximum is
|
||||
64 MiB. Each appearance counts, even when an object could be shared in memory
|
||||
but would be serialized more than once. The parser reserves a conservative
|
||||
upper bound before numeric conversion or address expansion, then releases the
|
||||
unused reservation after each value. It reuses the parsed source integer when
|
||||
the same image base contributes to many frames.
|
||||
|
||||
Raise this per-call allowance on the next call when more room is needed; no
|
||||
restart is required. File-size and row budgets do not raise it. This allowance
|
||||
is separate from JSON wire size, MCP response limits and the client's receive
|
||||
buffer. An exhausted allowance reports a typed resource error without partial
|
||||
success or truncated integer facts.
|
||||
|
||||
```bash
|
||||
rea inspect-apple-ips /absolute/path/to/report.ips --max-integer-text-bytes 16777216 --json
|
||||
```
|
||||
|
||||
This workflow does not execute a target, launch atos, or search for dSYMs.
|
||||
+13
-59
@@ -1,18 +1,16 @@
|
||||
graph TD
|
||||
%% Selected layer owners and dependency/wiring paths, not a directory or import inventory.
|
||||
%% Current deep-provider candidates are wired in src/composition/binary.ts.
|
||||
subgraph "Entry Points"
|
||||
CLI["src/cli.ts + src/cli/<br/>One-shot CLI adapter<br/>setup, diagnostics, analysis, decompilation"]
|
||||
MAIN["src/main.ts + src/main/<br/>MCP server adapter: stdio default, streamable HTTP opt-in<br/>config, session, process lifetime"]
|
||||
DISPATCH["scripts/rea.mjs<br/>Executable dispatcher<br/>routes mcp / mcp --http / one-shot"]
|
||||
MAIN["src/main.ts + src/main/<br/>MCP stdio server adapter<br/>config, session, process lifetime"]
|
||||
DISPATCH["scripts/rea.mjs<br/>Executable dispatcher<br/>routes mcp / one-shot"]
|
||||
end
|
||||
|
||||
subgraph "Production Composition"
|
||||
COMPOSITION["src/composition/<br/>fresh typed provider and session factories<br/>one-shot binding and independent optional loading<br/>typed capability bindings: construction, availability and retryable lifetimes<br/>pure capability declarations feed provider catalogs"]
|
||||
COMPOSITION["src/composition/<br/>fresh typed provider and session factories<br/>one-shot binding and independent optional loading<br/>static WASM/EVM descriptors: named contracts, factories, retryable lifetimes"]
|
||||
end
|
||||
|
||||
subgraph "Adapters"
|
||||
SERVER["src/server/<br/>MCP request translation<br/>createServer, tool registration, toolResult<br/>http/: streamable HTTP listener and node:http bridge"]
|
||||
SERVER["src/server/<br/>MCP request translation<br/>createServer, tool registration, toolResult"]
|
||||
HOPPER_BRIDGE["bridge/hopper_bridge.py<br/>Runs inside Hopper<br/>adapts operations to Hopper Python API"]
|
||||
GHIDRA_BRIDGE["bridge/ghidra/ReaGhidraBridge.java<br/>Packaged HeadlessScript<br/>inventory, decompiler, references, CFG"]
|
||||
INSTALL["src/cli/ + src/application/Setup*<br/>Host installation adapters<br/>macOS: verified vendor DMG to ~/Applications<br/>Linux: verified vendor package + apt/dnf/pacman"]
|
||||
@@ -21,29 +19,21 @@ graph TD
|
||||
subgraph "Application Layer"
|
||||
APP["src/application/<br/>Shared CLI/MCP workflows<br/>enhanced analysis and investigations"]
|
||||
SESSION["SessionProviderRouter + BinarySession<br/>one immutable deep binding per target<br/>disjoint auxiliary routing"]
|
||||
REGISTRY["AnalysisProviderRegistry<br/>sorted deep candidates<br/>deterministic selection, no fallback<br/>ProviderInspectionScope: prerequisite observation lifetime"]
|
||||
REGISTRY["AnalysisProviderRegistry<br/>sorted deep candidates<br/>deterministic selection, no fallback"]
|
||||
RECORDS["src/application/investigation/<br/>composed InvestigationRecords + narrow ports<br/>EvidenceLedger and Unknown ownership"]
|
||||
LIFETIMES["CapabilityScope<br/>host-owned service close capabilities<br/>released peers removed; failed cleanup retained for retry"]
|
||||
SNAPSHOT["BinarySessionRecords + AnalysisSnapshotCache<br/>binary target/profile snapshots and session file ownership<br/>record coordination and post-commit notifications"]
|
||||
SNAPSHOT["BinarySessionRecords + AnalysisSnapshotCache<br/>binary target/profile snapshots<br/>compatibility facade and post-commit notifications"]
|
||||
JS_RECON["src/application/javascript/ + src/artifacts/javascript/<br/>JavaScript artifact reconstruction<br/>directory/ASAR reads<br/>AST-only bundle projection"]
|
||||
end
|
||||
|
||||
subgraph "Provider Layers (selected adapters)"
|
||||
PROVIDER_OWNERS["src/providers/<br/>shared deferred client owners<br/>exact cleanup retry and attributed failures"]
|
||||
subgraph "Provider Layers"
|
||||
PWNTOOLS["src/native/pwntools/ + src/native/pwndbg/<br/>BYO offline ELF/core adapters<br/>owned snapshots/Python and optional core-only GDB<br/>unchanged upstream parsers, ABI structures and mapping API"]
|
||||
WASM["src/wasm/<br/>caller-supplied WABT; immutable artifact inspection<br/>owned process supervision and retryable workspace cleanup"]
|
||||
APKSIG["src/apkSignature/<br/>in-process APK Signing Block + v1 reader<br/>bounded single read, no device or execution"]
|
||||
EVM["src/evm/<br/>unchanged EVMole/WASM in bounded owned worker<br/>explicit carrier parsing and inferred interface adapter"]
|
||||
HOPPER["src/hopper/<br/>Hopper launch & Unix-socket protocol<br/>BridgeLauncher, HopperClient, protocol"]
|
||||
GHIDRA["src/ghidra/<br/>Ghidra discovery, profile & function analysis<br/>snapshot, Unix/Windows transport, bounded lifecycle"]
|
||||
IDA["src/ida/<br/>Upstream MCP contract adaptation<br/>live GUI binding or owned headless database"]
|
||||
KUNA["src/kuna/<br/>BYO Kuna deep analysis candidate<br/>discovery reads metadata; version probe on target open"]
|
||||
IDA_MCP["External ida-pro-mcp<br/>GUI proxy or database supervisor<br/>upstream installation and engine lifecycle"]
|
||||
|
||||
KUNA["src/kuna/<br/>BYO Kuna decompiler, a deep analysis candidate<br/>discovery reads metadata only; version probe when a target opens"]
|
||||
AUX["src/artifacts/ + src/native/<br/>artifact inventory, bounded directory triage and extraction<br/>format readers and native capabilities"]
|
||||
|
||||
|
||||
AUX["src/artifacts/ + src/native/<br/>artifact inventory and extraction<br/>format readers and native capabilities"]
|
||||
BROWSER["src/browser/<br/>CDP / Playwright capture<br/>captured scripts and native module resolution<br/>execution/: coverage, listener and source attribution"]
|
||||
JS_CODEC["src/javascript/sourceMaps/<br/>pinned upstream map codec<br/>owned heap-limited Node process, complete point evidence"]
|
||||
INSPECTOR["src/inspector/<br/>passive V8 Inspector discovery and capture<br/>script and target locations"]
|
||||
@@ -55,19 +45,12 @@ graph TD
|
||||
FIRMWARE["src/firmware/<br/>Binwalk / Unblob adapters<br/>owned processes, report normalization, private extraction"]
|
||||
ANDROID["src/android/<br/>Static APK adapter<br/>owned SDK stdio, pinned headless JADX, normalization"]
|
||||
REFERENCE["src/reference/<br/>Filesystem reference-source reader<br/>bounded traversal, validation, errors"]
|
||||
|
||||
ANDROID_TOOLS["src/adb/ + src/apktool/ + src/jeb/<br/>BYO device, resource-decoding and JEB adapters<br/>owned adb and Apktool processes; JEB engine client over MCP"]
|
||||
CAPTURE_TOOLS["src/captures/ + src/pcap/ + src/signals/ + src/sqlite/<br/>BYO tshark, pcap, sigrok and SQLite child inspection<br/>selected captures, recordings and databases; owned processes"]
|
||||
NATIVE_READERS["src/go/ + src/rust/ + src/il2cpp/ + src/flutter/ + src/gpu/<br/>Bundled in-process binary metadata readers<br/>Go build info, Rust dependencies, IL2CPP, Dart build ID, GPU code objects<br/>src/compression/: shared codec-result helper for rust and gpu"]
|
||||
FORMAT_READERS["src/model/ + src/python/ + src/erlang/ + src/electron/<br/>Bundled in-process format readers<br/>GGUF and safetensors headers, Python .pyc, Erlang BEAM, Electron fuses"]
|
||||
RUNTIME["src/frida/ + src/cutter/<br/>Frida instrumentation sessions and live Cutter bridge client"]
|
||||
end
|
||||
|
||||
subgraph "Provider Process Foundation"
|
||||
PROCESS["src/process/<br/>owned process groups, private roots<br/>deadlines, bounded file reads and diagnostics, cleanup"]
|
||||
WINDOWS["src/windows/<br/>Windows host boundary adapters<br/>package loader, opaque handles, owned jobs and private runtimes"]
|
||||
WINDOWS_NATIVE["native/windows/src/<br/>Node-API implementation<br/>NTFS admission, protected DACLs and atomic Job Object assignment"]
|
||||
FILESYSTEM["src/filesystem/<br/>owned file and directory handles<br/>stable reads, staged output, caller paths"]
|
||||
end
|
||||
|
||||
subgraph "Contracts Layer"
|
||||
@@ -76,7 +59,7 @@ graph TD
|
||||
end
|
||||
|
||||
subgraph "Domain Layer"
|
||||
DOMAIN["src/domain/<br/>Pure side-effect-free modules<br/>format declarations, provider selection, Evidence, comparisons, application graphs"]
|
||||
DOMAIN["src/domain/<br/>Pure side-effect-free modules<br/>evidence, comparisons, application graphs"]
|
||||
end
|
||||
|
||||
subgraph "Configuration"
|
||||
@@ -87,8 +70,8 @@ graph TD
|
||||
TESTS["tests/<br/>Vitest suite<br/>fixtures/ holds fake launcher, bridge, and CDP seams"]
|
||||
end
|
||||
|
||||
subgraph "Development Evaluation"
|
||||
EVALUATION["tests/evaluation/model-evals/<br/>Agent release evaluation<br/>session transcript scoring"]
|
||||
subgraph "Evaluation"
|
||||
EVALUATION["src/evaluation/<br/>Agent release evaluation<br/>session transcript scoring"]
|
||||
end
|
||||
|
||||
DISPATCH --> CLI
|
||||
@@ -100,7 +83,6 @@ graph TD
|
||||
MAIN --> COMPOSITION
|
||||
CLI --> COMPOSITION
|
||||
SERVER --> COMPOSITION
|
||||
COMPOSITION --> LIFETIMES
|
||||
COMPOSITION --> SESSION
|
||||
APP --> FIRMWARE
|
||||
FIRMWARE --> PROCESS
|
||||
@@ -118,8 +100,6 @@ graph TD
|
||||
SERVER -->|narrow record ports| RECORDS
|
||||
RECORDS --> DOMAIN
|
||||
COMPOSITION --> WASM
|
||||
COMPOSITION --> APKSIG
|
||||
APKSIG --> DOMAIN
|
||||
WASM --> PROCESS
|
||||
WASM --> DOMAIN
|
||||
COMPOSITION --> PWNTOOLS
|
||||
@@ -128,7 +108,6 @@ graph TD
|
||||
COMPOSITION --> HOPPER
|
||||
COMPOSITION --> GHIDRA
|
||||
COMPOSITION --> IDA
|
||||
COMPOSITION --> KUNA
|
||||
COMPOSITION --> AUX
|
||||
IDA --> IDA_MCP
|
||||
IDA --> CONTRACTS
|
||||
@@ -151,19 +130,14 @@ graph TD
|
||||
OBJDUMP --> DOMAIN
|
||||
COMPOSITION --> RIZIN
|
||||
RIZIN --> DOMAIN
|
||||
COMPOSITION --> GDB
|
||||
CLI --> GDB
|
||||
SERVER --> GDB
|
||||
GDB --> PROCESS
|
||||
GDB --> DOMAIN
|
||||
HOPPER --> HOPPER_BRIDGE
|
||||
GHIDRA --> GHIDRA_BRIDGE
|
||||
HOPPER --> PROCESS
|
||||
GHIDRA --> PROCESS
|
||||
KUNA --> PROCESS
|
||||
GHIDRA --> PROVIDER_OWNERS
|
||||
APP --> PROVIDER_OWNERS
|
||||
PROVIDER_OWNERS --> PROCESS
|
||||
PROVIDER_OWNERS --> AUX
|
||||
PROVIDER_OWNERS --> DOMAIN
|
||||
GHIDRA --> WINDOWS
|
||||
PROCESS --> WINDOWS
|
||||
WINDOWS --> WINDOWS_NATIVE
|
||||
@@ -176,7 +150,6 @@ graph TD
|
||||
CONTRACTS --> DOMAIN
|
||||
HOPPER --> DOMAIN
|
||||
GHIDRA --> DOMAIN
|
||||
KUNA --> DOMAIN
|
||||
AUX --> DOMAIN
|
||||
BROWSER --> DOMAIN
|
||||
PROCESS --> DOMAIN
|
||||
@@ -193,22 +166,3 @@ graph TD
|
||||
APP -->|EVM interface port| EVM
|
||||
EVM --> PROCESS
|
||||
EVM --> DOMAIN
|
||||
COMPOSITION --> KUNA
|
||||
KUNA --> PROCESS
|
||||
KUNA --> FILESYSTEM
|
||||
KUNA --> DOMAIN
|
||||
COMPOSITION --> ANDROID_TOOLS
|
||||
ANDROID_TOOLS --> PROCESS
|
||||
ANDROID_TOOLS --> DOMAIN
|
||||
COMPOSITION --> CAPTURE_TOOLS
|
||||
CAPTURE_TOOLS --> PROCESS
|
||||
CAPTURE_TOOLS --> DOMAIN
|
||||
COMPOSITION --> NATIVE_READERS
|
||||
NATIVE_READERS --> DOMAIN
|
||||
COMPOSITION --> FORMAT_READERS
|
||||
FORMAT_READERS --> DOMAIN
|
||||
COMPOSITION --> RUNTIME
|
||||
RUNTIME --> DOMAIN
|
||||
PROCESS --> FILESYSTEM
|
||||
AUX --> FILESYSTEM
|
||||
APP --> FILESYSTEM
|
||||
@@ -1,118 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="263" viewBox="0 0 960 263" role="img" aria-labelledby="title desc">
|
||||
<title id="title">REA showcases</title>
|
||||
<desc id="desc">Three illustrations. A brick-breaker screen with a speaker on each side stands for the DX-Ball sound-pan showcase. A browser window joined by a wire to a clipboard stands for the Notion clipboard-bridge showcase. A ring of 24 dots with one orange dot around a dark centre stands for the TH04 bullet-ring showcase.</desc>
|
||||
<rect width="960" height="263" fill="#11111b"/>
|
||||
<rect x="43" y="16.5" width="262" height="235.5" fill="#181825"/>
|
||||
<rect x="354" y="14.5" width="258" height="237.5" fill="#181825"/>
|
||||
<rect x="664" y="16.5" width="260" height="235.5" fill="#181825"/>
|
||||
<g id="dx-ball">
|
||||
<rect x="108.5" y="54" width="140" height="160" fill="#000000" fill-opacity="0.45"/>
|
||||
<rect x="106.85" y="52.1" width="136" height="157" fill="#181825" stroke="#cdd6f4" stroke-width="3.7"/>
|
||||
<rect x="110.9" y="55.9" width="128.2" height="149.6" fill="none" stroke="#6c7086" stroke-width="2.2"/>
|
||||
<rect x="112.6" y="58.5" width="28.4" height="14.3" fill="#000000" fill-opacity="0.45"/>
|
||||
<rect x="145.6" y="58.5" width="28.4" height="14.3" fill="#000000" fill-opacity="0.45"/>
|
||||
<rect x="178.6" y="58.5" width="28.4" height="14.3" fill="#000000" fill-opacity="0.45"/>
|
||||
<rect x="211.6" y="58.5" width="28.4" height="14.3" fill="#000000" fill-opacity="0.45"/>
|
||||
<rect x="112.6" y="77.39999999999999" width="28.4" height="14.3" fill="#000000" fill-opacity="0.45"/>
|
||||
<rect x="145.6" y="77.39999999999999" width="28.4" height="14.3" fill="#000000" fill-opacity="0.45"/>
|
||||
<rect x="178.6" y="77.39999999999999" width="28.4" height="14.3" fill="#000000" fill-opacity="0.45"/>
|
||||
<rect x="211.6" y="77.39999999999999" width="28.4" height="14.3" fill="#000000" fill-opacity="0.45"/>
|
||||
<rect x="111.8" y="57.900000000000006" width="26.2" height="11.9" fill="#45475a" stroke="#cdd6f4" stroke-width="2.4"/>
|
||||
<rect x="144.79999999999998" y="57.900000000000006" width="26.2" height="11.9" fill="#45475a" stroke="#cdd6f4" stroke-width="2.4"/>
|
||||
<rect x="177.79999999999998" y="57.900000000000006" width="26.2" height="11.9" fill="#45475a" stroke="#cdd6f4" stroke-width="2.4"/>
|
||||
<rect x="210.79999999999998" y="57.900000000000006" width="26.2" height="11.9" fill="#45475a" stroke="#cdd6f4" stroke-width="2.4"/>
|
||||
<rect x="111.8" y="75.0" width="26.2" height="11.9" fill="#45475a" stroke="#cdd6f4" stroke-width="2.4"/>
|
||||
<rect x="144.79999999999998" y="75.0" width="26.2" height="11.9" fill="#45475a" stroke="#cdd6f4" stroke-width="2.4"/>
|
||||
<rect x="177.79999999999998" y="75.0" width="26.2" height="11.9" fill="#45475a" stroke="#cdd6f4" stroke-width="2.4"/>
|
||||
<rect x="210.79999999999998" y="75.0" width="26.2" height="11.9" fill="#45475a" stroke="#cdd6f4" stroke-width="2.4"/>
|
||||
<ellipse cx="173.4" cy="157.2" rx="8.4" ry="8.2" fill="#000000" fill-opacity="0.45"/>
|
||||
<ellipse cx="171.9" cy="154.5" rx="7.3" ry="7.2" fill="#585b70" stroke="#cdd6f4" stroke-width="2"/>
|
||||
<rect x="165.5" y="149.5" width="3.2" height="2.6" fill="#a6adc8"/>
|
||||
<rect x="145.5" y="195.2" width="57.6" height="12.4" fill="#585b70" stroke="#cdd6f4" stroke-width="2.2"/>
|
||||
<rect x="144.5" y="204" width="59.3" height="6.5" fill="#cdd6f4"/>
|
||||
<g transform="translate(0 0)">
|
||||
<path d="M61.5 117.5H69.6L78 108.5V141.2L69.6 132.8H61.5Z" fill="#585b70" stroke="#cdd6f4" stroke-width="2.3" stroke-linejoin="miter"/>
|
||||
<path d="M78.1 106V143" stroke="#cdd6f4" stroke-width="3.4" fill="none"/>
|
||||
<g fill="none" stroke="#89b4fa" stroke-width="2.2" stroke-linecap="butt" stroke-dasharray="2.6 1.4">
|
||||
<path d="M84.8 118.2Q87.6 125 84.8 131.8"/>
|
||||
<path d="M90.2 112.4Q94.5 125 90.2 137.6"/>
|
||||
<path d="M95.8 108.2Q101.5 125 95.8 141.8"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(190 0)">
|
||||
<path d="M61.5 117.5H69.6L78 108.5V141.2L69.6 132.8H61.5Z" fill="#585b70" stroke="#cdd6f4" stroke-width="2.3" stroke-linejoin="miter"/>
|
||||
<path d="M78.1 106V143" stroke="#cdd6f4" stroke-width="3.4" fill="none"/>
|
||||
<g fill="none" stroke="#89b4fa" stroke-width="2.2" stroke-linecap="butt" stroke-dasharray="2.6 1.4">
|
||||
<path d="M84.8 118.2Q87.6 125 84.8 131.8"/>
|
||||
<path d="M90.2 112.4Q94.5 125 90.2 137.6"/>
|
||||
<path d="M95.8 108.2Q101.5 125 95.8 141.8"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="clipboard-bridge">
|
||||
<rect x="390.3" y="83.6" width="100.6" height="82.6" fill="#000000" fill-opacity="0.45"/>
|
||||
<rect x="387.7" y="81.2" width="97.8" height="79.7" fill="#181825" stroke="#cdd6f4" stroke-width="2.9"/>
|
||||
<rect x="389.2" y="82.7" width="94.9" height="13" fill="#45475a"/>
|
||||
<path d="M387.7 97H485.5" stroke="#cdd6f4" stroke-width="3.1"/>
|
||||
<rect x="394.7" y="86.6" width="4.6" height="5" fill="#fab387"/>
|
||||
<rect x="402.7" y="86.6" width="4.6" height="5" fill="#f9e2af"/>
|
||||
<rect x="410.8" y="86.6" width="4.6" height="5" fill="#a6adc8"/>
|
||||
<rect x="398" y="107.4" width="10.6" height="9.4" fill="#7f849c"/>
|
||||
<rect x="412.4" y="107.9" width="16.8" height="6" fill="#7f849c"/>
|
||||
<rect x="434" y="107.9" width="42.4" height="6.2" fill="#7f849c"/>
|
||||
<rect x="412.2" y="117.3" width="64" height="2.6" fill="#6c7086"/>
|
||||
<rect x="412.2" y="124.9" width="55.3" height="3.6" fill="#6c7086"/>
|
||||
<rect x="412.2" y="133.2" width="50.3" height="3.8" fill="#6c7086"/>
|
||||
<rect x="412.2" y="142" width="13.2" height="3.4" fill="#6c7086"/>
|
||||
<rect x="430" y="142" width="14.4" height="3.4" fill="#6c7086"/>
|
||||
<rect x="449" y="142" width="27.6" height="3.2" fill="#313244"/>
|
||||
<rect x="412.2" y="150" width="12.6" height="2.9" fill="#6c7086"/>
|
||||
<path d="M455.2 162V187H476M497 187H508.2V163.2H523" fill="none" stroke="#89b4fa" stroke-width="3" stroke-linejoin="miter"/>
|
||||
<rect x="477.5" y="178.3" width="23" height="21" fill="#000000" fill-opacity="0.45"/>
|
||||
<path d="M484.7 173.2V177" stroke="#cdd6f4" stroke-width="2.2"/>
|
||||
<rect x="476.4" y="177.5" width="20.2" height="18.1" fill="#585b70" stroke="#cdd6f4" stroke-width="2.2"/>
|
||||
<rect x="481.4" y="178.6" width="4.6" height="16" fill="#313244"/>
|
||||
<rect x="477.2" y="191" width="4.6" height="4.4" fill="#f9e2af"/>
|
||||
<rect x="481.6" y="183.8" width="3.4" height="3" fill="#fab387"/>
|
||||
<rect x="525.4" y="114.6" width="60.6" height="75" fill="#000000" fill-opacity="0.45"/>
|
||||
<rect x="524.7" y="112.5" width="56.4" height="69.4" fill="#45475a" stroke="#cdd6f4" stroke-width="3"/>
|
||||
<rect x="531.6" y="118.6" width="42.3" height="53.4" fill="#1e1e2e"/>
|
||||
<rect x="531.6" y="172" width="42.3" height="8.4" fill="#45475a"/>
|
||||
<path d="M531.6 175.8H575" stroke="#cdd6f4" stroke-width="1.8" stroke-opacity="0.8"/>
|
||||
<path d="M547.4 103.3H556.6V106.2H559.6V109.2H564.9V120.3H540.1V109.2H545V106.2H547.4Z" fill="#585b70" stroke="#cdd6f4" stroke-width="2" stroke-linejoin="miter"/>
|
||||
<rect x="549.4" y="108.4" width="2.8" height="1.9" fill="#181825"/>
|
||||
<rect x="536.6" y="131.8" width="32.3" height="3.4" fill="#7f849c"/>
|
||||
<rect x="536.6" y="138.8" width="32.3" height="3.4" fill="#7f849c"/>
|
||||
<rect x="536.6" y="145.8" width="32.3" height="3.4" fill="#7f849c"/>
|
||||
<rect x="536.6" y="152.8" width="26.8" height="3.4" fill="#7f849c"/>
|
||||
<rect x="536.6" y="159.8" width="14.6" height="3.4" fill="#7f849c"/>
|
||||
</g>
|
||||
<g id="bullet-ring">
|
||||
<circle cx="783" cy="74.5" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="798" cy="72" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="812.5" cy="74.5" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="769.5" cy="81" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="826.5" cy="81" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="756.5" cy="90" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="839" cy="90" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="746.5" cy="103" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="849" cy="103" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="740.5" cy="117" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="854.5" cy="117" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="738" cy="134" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="856.5" cy="133.5" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="741" cy="149" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="854" cy="149" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="747.5" cy="163" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="848" cy="163" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="757.5" cy="175" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="770" cy="184" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="826.5" cy="184" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="838.5" cy="175" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="784" cy="191" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="798" cy="192.5" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="812" cy="190.5" r="3.8" fill="#89b4fa"/>
|
||||
<circle cx="833.5" cy="109" r="4.1" fill="#fab387"/>
|
||||
<circle cx="797.4" cy="133" r="10.9" fill="#cdd6f4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 8.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
@@ -1,118 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="263" viewBox="0 0 960 263" role="img" aria-labelledby="title desc">
|
||||
<title id="title">REA showcases</title>
|
||||
<desc id="desc">Three illustrations. A brick-breaker screen with a speaker on each side stands for the DX-Ball sound-pan showcase. A browser window joined by a wire to a clipboard stands for the Notion clipboard-bridge showcase. A ring of 24 dots with one orange dot around a dark centre stands for the TH04 bullet-ring showcase.</desc>
|
||||
<rect width="960" height="263" fill="#fafbfb"/>
|
||||
<rect x="43" y="16.5" width="262" height="235.5" fill="#f4f6f5"/>
|
||||
<rect x="354" y="14.5" width="258" height="237.5" fill="#f4f6f5"/>
|
||||
<rect x="664" y="16.5" width="260" height="235.5" fill="#f4f6f5"/>
|
||||
<g id="dx-ball">
|
||||
<rect x="108.5" y="54" width="140" height="160" fill="#000000" fill-opacity="0.27"/>
|
||||
<rect x="106.85" y="52.1" width="136" height="157" fill="#f4f6f5" stroke="#191a1d" stroke-width="3.7"/>
|
||||
<rect x="110.9" y="55.9" width="128.2" height="149.6" fill="none" stroke="#a5a6a9" stroke-width="2.2"/>
|
||||
<rect x="112.6" y="58.5" width="28.4" height="14.3" fill="#000000" fill-opacity="0.27"/>
|
||||
<rect x="145.6" y="58.5" width="28.4" height="14.3" fill="#000000" fill-opacity="0.27"/>
|
||||
<rect x="178.6" y="58.5" width="28.4" height="14.3" fill="#000000" fill-opacity="0.27"/>
|
||||
<rect x="211.6" y="58.5" width="28.4" height="14.3" fill="#000000" fill-opacity="0.27"/>
|
||||
<rect x="112.6" y="77.39999999999999" width="28.4" height="14.3" fill="#000000" fill-opacity="0.27"/>
|
||||
<rect x="145.6" y="77.39999999999999" width="28.4" height="14.3" fill="#000000" fill-opacity="0.27"/>
|
||||
<rect x="178.6" y="77.39999999999999" width="28.4" height="14.3" fill="#000000" fill-opacity="0.27"/>
|
||||
<rect x="211.6" y="77.39999999999999" width="28.4" height="14.3" fill="#000000" fill-opacity="0.27"/>
|
||||
<rect x="111.8" y="57.900000000000006" width="26.2" height="11.9" fill="#5a636d" stroke="#191a1d" stroke-width="2.4"/>
|
||||
<rect x="144.79999999999998" y="57.900000000000006" width="26.2" height="11.9" fill="#5a636d" stroke="#191a1d" stroke-width="2.4"/>
|
||||
<rect x="177.79999999999998" y="57.900000000000006" width="26.2" height="11.9" fill="#5a636d" stroke="#191a1d" stroke-width="2.4"/>
|
||||
<rect x="210.79999999999998" y="57.900000000000006" width="26.2" height="11.9" fill="#5a636d" stroke="#191a1d" stroke-width="2.4"/>
|
||||
<rect x="111.8" y="75.0" width="26.2" height="11.9" fill="#5a636d" stroke="#191a1d" stroke-width="2.4"/>
|
||||
<rect x="144.79999999999998" y="75.0" width="26.2" height="11.9" fill="#5a636d" stroke="#191a1d" stroke-width="2.4"/>
|
||||
<rect x="177.79999999999998" y="75.0" width="26.2" height="11.9" fill="#5a636d" stroke="#191a1d" stroke-width="2.4"/>
|
||||
<rect x="210.79999999999998" y="75.0" width="26.2" height="11.9" fill="#5a636d" stroke="#191a1d" stroke-width="2.4"/>
|
||||
<ellipse cx="173.4" cy="157.2" rx="8.4" ry="8.2" fill="#000000" fill-opacity="0.27"/>
|
||||
<ellipse cx="171.9" cy="154.5" rx="7.3" ry="7.2" fill="#4d5863" stroke="#191a1d" stroke-width="2"/>
|
||||
<rect x="165.5" y="149.5" width="3.2" height="2.6" fill="#aeb1b4"/>
|
||||
<rect x="145.5" y="195.2" width="57.6" height="12.4" fill="#525c65" stroke="#191a1d" stroke-width="2.2"/>
|
||||
<rect x="144.5" y="204" width="59.3" height="6.5" fill="#191a1d"/>
|
||||
<g transform="translate(0 0)">
|
||||
<path d="M61.5 117.5H69.6L78 108.5V141.2L69.6 132.8H61.5Z" fill="#56616b" stroke="#191a1d" stroke-width="2.3" stroke-linejoin="miter"/>
|
||||
<path d="M78.1 106V143" stroke="#191a1d" stroke-width="3.4" fill="none"/>
|
||||
<g fill="none" stroke="#274b77" stroke-width="2.2" stroke-linecap="butt" stroke-dasharray="2.6 1.4">
|
||||
<path d="M84.8 118.2Q87.6 125 84.8 131.8"/>
|
||||
<path d="M90.2 112.4Q94.5 125 90.2 137.6"/>
|
||||
<path d="M95.8 108.2Q101.5 125 95.8 141.8"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(190 0)">
|
||||
<path d="M61.5 117.5H69.6L78 108.5V141.2L69.6 132.8H61.5Z" fill="#56616b" stroke="#191a1d" stroke-width="2.3" stroke-linejoin="miter"/>
|
||||
<path d="M78.1 106V143" stroke="#191a1d" stroke-width="3.4" fill="none"/>
|
||||
<g fill="none" stroke="#274b77" stroke-width="2.2" stroke-linecap="butt" stroke-dasharray="2.6 1.4">
|
||||
<path d="M84.8 118.2Q87.6 125 84.8 131.8"/>
|
||||
<path d="M90.2 112.4Q94.5 125 90.2 137.6"/>
|
||||
<path d="M95.8 108.2Q101.5 125 95.8 141.8"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="clipboard-bridge">
|
||||
<rect x="390.3" y="83.6" width="100.6" height="82.6" fill="#000000" fill-opacity="0.27"/>
|
||||
<rect x="387.7" y="81.2" width="97.8" height="79.7" fill="#f4f6f5" stroke="#191a1d" stroke-width="2.9"/>
|
||||
<rect x="389.2" y="82.7" width="94.9" height="13" fill="#5a636d"/>
|
||||
<path d="M387.7 97H485.5" stroke="#191a1d" stroke-width="3.1"/>
|
||||
<rect x="394.7" y="86.6" width="4.6" height="5" fill="#c3693b"/>
|
||||
<rect x="402.7" y="86.6" width="4.6" height="5" fill="#be9a53"/>
|
||||
<rect x="410.8" y="86.6" width="4.6" height="5" fill="#b3b3b4"/>
|
||||
<rect x="398" y="107.4" width="10.6" height="9.4" fill="#666671"/>
|
||||
<rect x="412.4" y="107.9" width="16.8" height="6" fill="#666c74"/>
|
||||
<rect x="434" y="107.9" width="42.4" height="6.2" fill="#666c74"/>
|
||||
<rect x="412.2" y="117.3" width="64" height="2.6" fill="#7a7c82"/>
|
||||
<rect x="412.2" y="124.9" width="55.3" height="3.6" fill="#7a7c82"/>
|
||||
<rect x="412.2" y="133.2" width="50.3" height="3.8" fill="#6d7279"/>
|
||||
<rect x="412.2" y="142" width="13.2" height="3.4" fill="#6d7279"/>
|
||||
<rect x="430" y="142" width="14.4" height="3.4" fill="#6d7279"/>
|
||||
<rect x="449" y="142" width="27.6" height="3.2" fill="#e8e8e8"/>
|
||||
<rect x="412.2" y="150" width="12.6" height="2.9" fill="#6d7279"/>
|
||||
<path d="M455.2 162V187H476M497 187H508.2V163.2H523" fill="none" stroke="#274b77" stroke-width="3" stroke-linejoin="miter"/>
|
||||
<rect x="477.5" y="178.3" width="23" height="21" fill="#000000" fill-opacity="0.27"/>
|
||||
<path d="M484.7 173.2V177" stroke="#191a1d" stroke-width="2.2"/>
|
||||
<rect x="476.4" y="177.5" width="20.2" height="18.1" fill="#525c65" stroke="#191a1d" stroke-width="2.2"/>
|
||||
<rect x="481.4" y="178.6" width="4.6" height="16" fill="#323435"/>
|
||||
<rect x="477.2" y="191" width="4.6" height="4.4" fill="#be9a53"/>
|
||||
<rect x="481.6" y="183.8" width="3.4" height="3" fill="#c3693b"/>
|
||||
<rect x="525.4" y="114.6" width="60.6" height="75" fill="#000000" fill-opacity="0.27"/>
|
||||
<rect x="524.7" y="112.5" width="56.4" height="69.4" fill="#545e69" stroke="#191a1d" stroke-width="3"/>
|
||||
<rect x="531.6" y="118.6" width="42.3" height="53.4" fill="#ededec"/>
|
||||
<rect x="531.6" y="172" width="42.3" height="8.4" fill="#545e69"/>
|
||||
<path d="M531.6 175.8H575" stroke="#191a1d" stroke-width="1.8" stroke-opacity="0.8"/>
|
||||
<path d="M547.4 103.3H556.6V106.2H559.6V109.2H564.9V120.3H540.1V109.2H545V106.2H547.4Z" fill="#545e69" stroke="#191a1d" stroke-width="2" stroke-linejoin="miter"/>
|
||||
<rect x="549.4" y="108.4" width="2.8" height="1.9" fill="#f4f6f5"/>
|
||||
<rect x="536.6" y="131.8" width="32.3" height="3.4" fill="#706f74"/>
|
||||
<rect x="536.6" y="138.8" width="32.3" height="3.4" fill="#706f74"/>
|
||||
<rect x="536.6" y="145.8" width="32.3" height="3.4" fill="#706f74"/>
|
||||
<rect x="536.6" y="152.8" width="26.8" height="3.4" fill="#706f74"/>
|
||||
<rect x="536.6" y="159.8" width="14.6" height="3.4" fill="#706f74"/>
|
||||
</g>
|
||||
<g id="bullet-ring">
|
||||
<circle cx="783" cy="74.5" r="3.8" fill="#274b77"/>
|
||||
<circle cx="798" cy="72" r="3.8" fill="#274b77"/>
|
||||
<circle cx="812.5" cy="74.5" r="3.8" fill="#274b77"/>
|
||||
<circle cx="769.5" cy="81" r="3.8" fill="#274b77"/>
|
||||
<circle cx="826.5" cy="81" r="3.8" fill="#274b77"/>
|
||||
<circle cx="756.5" cy="90" r="3.8" fill="#274b77"/>
|
||||
<circle cx="839" cy="90" r="3.8" fill="#274b77"/>
|
||||
<circle cx="746.5" cy="103" r="3.8" fill="#274b77"/>
|
||||
<circle cx="849" cy="103" r="3.8" fill="#274b77"/>
|
||||
<circle cx="740.5" cy="117" r="3.8" fill="#274b77"/>
|
||||
<circle cx="854.5" cy="117" r="3.8" fill="#274b77"/>
|
||||
<circle cx="738" cy="134" r="3.8" fill="#274b77"/>
|
||||
<circle cx="856.5" cy="133.5" r="3.8" fill="#274b77"/>
|
||||
<circle cx="741" cy="149" r="3.8" fill="#274b77"/>
|
||||
<circle cx="854" cy="149" r="3.8" fill="#274b77"/>
|
||||
<circle cx="747.5" cy="163" r="3.8" fill="#274b77"/>
|
||||
<circle cx="848" cy="163" r="3.8" fill="#274b77"/>
|
||||
<circle cx="757.5" cy="175" r="3.8" fill="#274b77"/>
|
||||
<circle cx="770" cy="184" r="3.8" fill="#274b77"/>
|
||||
<circle cx="826.5" cy="184" r="3.8" fill="#274b77"/>
|
||||
<circle cx="838.5" cy="175" r="3.8" fill="#274b77"/>
|
||||
<circle cx="784" cy="191" r="3.8" fill="#274b77"/>
|
||||
<circle cx="798" cy="192.5" r="3.8" fill="#274b77"/>
|
||||
<circle cx="812" cy="190.5" r="3.8" fill="#274b77"/>
|
||||
<circle cx="833.5" cy="109" r="4.1" fill="#c3693b"/>
|
||||
<circle cx="797.4" cy="133" r="10.9" fill="#1c222a"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 8.7 KiB |
+4
-264
@@ -8,14 +8,9 @@ ordinary binary inspection use the same modular capability.
|
||||
|
||||
## Bring your own engine
|
||||
|
||||
Real verification covers **Linux x64** hosts and **EXEC/DYN/REL** objects in
|
||||
these profiles: ELF64 x86-64, ELF64 little-endian AArch64, ELF32 i386, ELF32
|
||||
little-endian ARM and ELF32 little- or big-endian MIPS. Other machines, classes
|
||||
and byte orders, including big-endian ARM, return `unsupported_target`. Other
|
||||
hosts, including Linux arm64, return `capability_unavailable` naming the
|
||||
detected host before any target is read. Provide an absolute
|
||||
Python executable whose environment already contains unchanged pwntools 4.15.0,
|
||||
pyelftools 0.33 and Unicorn 2.1.2:
|
||||
Initial real verification covers **Linux x64, ELF64 x86-64 little-endian
|
||||
EXEC/DYN/REL**. Provide an absolute Python executable whose environment already
|
||||
contains unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2:
|
||||
|
||||
```sh
|
||||
REA_PWNTOOLS_PYTHON=/absolute/isolated-env/bin/python \
|
||||
@@ -34,16 +29,6 @@ the selected object as a host process, or requests runtime library resolution. T
|
||||
uses isolated mode and an owned cache. Exact upstream profiles are recorded in
|
||||
[upstream provenance](https://github.com/morluto/rea/blob/main/third_party/pwntools/README.md).
|
||||
|
||||
The adapter skips pwntools kernel configuration and version discovery: those
|
||||
memory-content scans supply none of this report's fields. Ordinary `IKCFG_ST`
|
||||
data is not interpreted as a kernel-config request, and virtual zero-filled
|
||||
LOAD tails are reported from their headers without materializing them for that
|
||||
scan. GOT/PLT inference and the advertised static mitigation indicators still
|
||||
use the pinned upstream implementations. Header-table file ranges are checked
|
||||
before dependency seeks. Structural inventory and the convenience-map engine
|
||||
still parse the snapshot separately; eliminating that remaining duplication
|
||||
requires preserving the upstream GOT/PLT and mitigation facts.
|
||||
|
||||
## Interpreting results
|
||||
|
||||
- Artifact path, SHA-256 and size identify original selected bytes. All parsing
|
||||
@@ -105,12 +90,6 @@ one section or symbol, or a stable page without re-running the decoder. CLI
|
||||
one-shot invocations pass portable inline Evidence JSON; an MCP session uses
|
||||
the retained `evidence_id`. Page `limit` is required and bounded by the
|
||||
measured MCP stdio budget documented on the tool contract.
|
||||
An empty retained section or symbol collection has no valid item index. Its page
|
||||
view can still return an empty page.
|
||||
Malformed authenticated layout input remains rejected with the owning schema's
|
||||
safe field diagnostics under `source.normalized_result`, including the field
|
||||
path and type or value reason. Evidence authentication and decoder validation
|
||||
remain separate boundaries.
|
||||
|
||||
Over MCP, `inspect_binary_layout` with `"detail": "summary"` retains the
|
||||
complete layout Evidence in the session and returns only its summary view.
|
||||
@@ -181,41 +160,11 @@ launch builder. It is a configuration fact, not measured memory use or proof of
|
||||
the effective JVM limit; JVM environment options may affect that limit.
|
||||
When a Windows script wrapper selects the JVM, this value remains unknown.
|
||||
Historical stdout and stderr remain available, but classification uses only the
|
||||
failing operation's `failure_output`. Retained stdout and stderr share an 8 MiB
|
||||
budget, but every drained byte is scanned for `java.lang.OutOfMemoryError`:
|
||||
`failure_output.observed_oome` records the latest confirmed token's bounded line per stream from
|
||||
the failing operation, with its byte offset, even after the budget is spent. These observations own OOME detection; retained text that ends at a class-name prefix cannot confirm a complete token. `line_complete` distinguishes a known line end from an incomplete trailing context; `invalid_utf8` marks replacement-decoded context.
|
||||
Incomplete operation output without an observed OOME keeps the original failure code and explains that a memory cause cannot be ruled out. Incompleteness alone does not confirm a memory constraint or justify heap-specific advice. Token context stays within the failing operation's checkpoint; a class-name prefix in a different identifier is not OOME evidence. A trailing bare token requires an observed stream EOF to establish its boundary.
|
||||
Output draining waits at most one second,
|
||||
failing operation's `failure_output`. Output draining waits at most one second,
|
||||
subject to earlier cancellation or the startup deadline. `output_incomplete`
|
||||
marks a snapshot taken before stream closure; cleanup ownership remains with
|
||||
the session until release succeeds.
|
||||
|
||||
A kernel or container OOM killer ends Ghidra with `SIGKILL` and prints no
|
||||
`OutOfMemoryError`. When a process or protocol failure records
|
||||
`exit_signal: "SIGKILL"` and no `stop_requested`, REA reports the signal
|
||||
instead of generic `rea doctor` advice. The process supervisor records
|
||||
`stop_requested: true` once REA begins stopping Ghidra, before it sends any
|
||||
signal, so REA's own SIGKILL is never reported as an outside kill.
|
||||
|
||||
On Linux with cgroup v2, REA also reads its own cgroup's `memory.events` before
|
||||
launch and again after the kill, locating the group through the cgroup2 mount
|
||||
in `/proc/self/mountinfo`. `memory_cgroup` records the group path,
|
||||
`oom_kill_before`, `oom_kill_after` and `memory_max`. If the count rose, REA
|
||||
returns `resource_constraint` with `resource: "memory"` and `cgroup_memory_max`
|
||||
in `reported_limits`. The count covers every process in REA's cgroup and its
|
||||
child groups, which can include processes outside REA's process tree when the
|
||||
group is shared, so a rise shows that the kernel OOM killer ended a process in
|
||||
that cgroup or a child group, not which process it chose. `memory_max` is the
|
||||
limit in bytes, `"max"` when the group sets none, or `null` when it could not be
|
||||
read; a limit on a parent group is not read. The limit also covers Ghidra's
|
||||
native decompiler processes, whose number follows the available CPUs, so the
|
||||
advice does not recommend a larger Java heap. When the session had already
|
||||
completed an operation, the advice keeps the instruction to reopen the target
|
||||
with `open_binary`. Without a cgroup v2 memory group, or when the count did not
|
||||
rise, the failure stays `execution_failure` and names the signal as a possible
|
||||
out-of-memory kill.
|
||||
|
||||
## Portable PE resources
|
||||
|
||||
`inspect_pe_resources` / `inspect-pe-resources` inspects an explicit PE32 or
|
||||
@@ -239,14 +188,6 @@ reported. Directory headers, entries, data entries and payloads retain original
|
||||
file offsets and lengths. Each opaque payload has its own SHA-256; code page and
|
||||
reserved data-entry fields are preserved.
|
||||
|
||||
Every `directories[]` row retains its identity `path` and the matching incoming
|
||||
`entry_locations`: both are empty for the root, then contain the type and
|
||||
optionally the name. Empty branches keep these identities even when they have
|
||||
no payload leaves. Shared directory bytes produce a row for each incoming path,
|
||||
with the original directory range and each path's own entry locations. These
|
||||
rows are included in the metadata budget. A present empty root remains distinct
|
||||
from an absent resource data directory (`directory: null`, `directories: []`).
|
||||
|
||||
RT_GROUP_ICON records expose original image descriptors and RT_ICON candidates.
|
||||
A reference resolves only when an icon with the same numeric ID and language
|
||||
exists. Missing references, other-language candidates and declared-size mismatch
|
||||
@@ -269,204 +210,3 @@ Large Evidence envelopes may require a caller-configured MCP transport receive
|
||||
buffer because envelope copies add overhead. Requests honor cancellation during
|
||||
snapshot reading, directory traversal and payload hashing. The operation reads
|
||||
regular files only and rejects symbolic links.
|
||||
|
||||
## Byte entropy triage
|
||||
|
||||
`inspect_byte_entropy` / `inspect-byte-entropy` profiles any local regular file
|
||||
without an analysis engine, execution, extraction or network access.
|
||||
|
||||
```sh
|
||||
rea inspect-byte-entropy ./firmware.bin --block-size 4096 --json
|
||||
rea inspect-byte-entropy ./firmware.bin --offset 4096 --length 65536 --json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "inspect_byte_entropy",
|
||||
"arguments": { "path": "/artifacts/firmware.bin", "detail": "summary" }
|
||||
}
|
||||
```
|
||||
|
||||
Complete Evidence includes `artifact` (original path, byte size and SHA-256),
|
||||
`block_size`, `window`, `file_entropy`, `thresholds`, `blocks`, `runs` and
|
||||
`limitations`. For a 4096-byte zero-filled file, the entropy fields are:
|
||||
|
||||
```json
|
||||
{
|
||||
"block_size": 4096,
|
||||
"window": { "offset": 0, "length": 4096 },
|
||||
"file_entropy": 0,
|
||||
"thresholds": { "high": 7.2, "low": 1 },
|
||||
"blocks": [{ "offset": 0, "length": 4096, "entropy": 0 }],
|
||||
"runs": [{ "offset": 0, "length": 4096, "classification": "low" }]
|
||||
}
|
||||
```
|
||||
|
||||
Offsets and lengths are decimal byte counts. Blocks begin at the requested
|
||||
`offset` (default zero); omitted `length` extends to EOF. Explicit windows past
|
||||
EOF fail rather than silently clipping. The final block may be shorter.
|
||||
`block_size` must be a power of two from 256 through 65536, default 4096. A
|
||||
window may produce at most 32,768 block rows; larger requests fail before the
|
||||
file scan with a capacity error. Choose a shorter window or larger block size.
|
||||
SHA-256 and `file_entropy` always cover the entire file, including when a window
|
||||
is selected. Empty files and empty windows are accepted; empty entropy is zero
|
||||
by convention. The tool reads the size the file reports. A file that reports size
|
||||
0, such as a Linux `/proc` entry, returns no blocks with a limitation that the
|
||||
result does not show the file is empty. File identity, size and timestamps are checked across the read;
|
||||
detected changes produce an error rather than Evidence.
|
||||
|
||||
Shannon entropy is reported in bits per byte from 0 to 8, rounded to four decimal
|
||||
places. Adjacent blocks with reported entropy at least 7.2 form `high` runs;
|
||||
blocks at most 1.0 form `low` runs. Other blocks interrupt runs. Entropy does not
|
||||
prove encryption, and this measure cannot distinguish encrypted from compressed
|
||||
data. Small blocks can underestimate entropy; the result is a triage clue.
|
||||
|
||||
MCP `detail: "summary"` retains complete Evidence in the session and returns
|
||||
file entropy, block/run counts, thresholds, the selected window, and the ten
|
||||
longest runs (ties ordered by file offset). Use its `parent_evidence_id` to
|
||||
retrieve complete Evidence, or request its summary with `inspect_analysis_view`.
|
||||
The CLI returns complete Evidence through the same service. Summary detail
|
||||
requires a session capable of retaining Evidence.
|
||||
|
||||
Reads use one block-sized buffer and a 256-entry histogram. Complete output
|
||||
can become very large because it includes one row per selected block, and
|
||||
summary delivery still retains that full list. Use a shorter length or a
|
||||
larger block size to reduce it. Full Evidence and its serialization require
|
||||
memory proportional to the number of selected blocks. The 32,768-row ceiling bounds the actual retained block objects and
|
||||
their serialized representation: a cap-sized run below produced 1,514,877
|
||||
serialized bytes and increased V8 heap use by 21.2 MiB on the measured host.
|
||||
The cap therefore bounds this output independently of file size; it is not a
|
||||
portable process-memory guarantee. Narrow the window or increase block size to
|
||||
reduce retained rows further.
|
||||
|
||||
Local service measurement (macOS arm64, Node 24.18.0, 100,000,000-byte file,
|
||||
4096-byte blocks, file already in the OS cache): first process run took 1.113 s
|
||||
wall / 1.148 s CPU with 145.3 MiB peak RSS; a second run in the same process took
|
||||
1.015 s / 1.035 s CPU, with cumulative peak RSS 155.8 MiB. Each read 100,000,000
|
||||
bytes and produced 24,415 blocks. Measurements include Evidence construction,
|
||||
summary projection and JSON serialization, with no child processes. Complete
|
||||
Evidence serialized to 1,122,303 bytes and its summary to 2,705 bytes. A
|
||||
cap-sized 134,217,728-byte sparse file with 4096-byte blocks produced 32,768
|
||||
rows and 1,514,877 serialized bytes; V8 heap use increased by 21.2 MiB and RSS
|
||||
by 18.1 MiB in that process. This separate measurement used the same macOS
|
||||
arm64 host and Node version. Cold disk I/O, token counts and an agent
|
||||
investigation were not measured; there was no previous equivalent tool to
|
||||
benchmark. These figures do not establish a portable peak-memory bound,
|
||||
constant memory for retained Evidence or faster agent task completion.
|
||||
|
||||
## Portable native exports
|
||||
|
||||
`inspect_native_exports` / `inspect-native-exports` reads the export table of an
|
||||
explicit PE or ELF image on any supported host, without an active target,
|
||||
native engine, `objdump` or Windows SDK:
|
||||
|
||||
```sh
|
||||
rea inspect-native-exports ./runtimes/linux-x64/native/libexample.so --json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "inspect_native_exports",
|
||||
"arguments": { "path": "/artifacts/native/example.dll" }
|
||||
}
|
||||
```
|
||||
|
||||
The complete Evidence identifies the stable artifact snapshot by path, byte
|
||||
count and SHA-256. Complete delivery remains the default. For a small initial
|
||||
MCP reply, select `detail: "summary"`; this retains the complete Evidence and
|
||||
returns artifact identity, table coverage and row counts. Use its
|
||||
`normalized_result.parent_evidence_id` with `inspect_analysis_view`:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": {
|
||||
"kind": "retained-evidence",
|
||||
"evidence_id": "ev_<parent digest>"
|
||||
},
|
||||
"view": {
|
||||
"kind": "native-exports",
|
||||
"collection": "exports",
|
||||
"names": ["selected_name"],
|
||||
"fields": ["name"],
|
||||
"offset": 0,
|
||||
"limit": 32
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Names match exactly and preserve every same-name version or alias. Optional
|
||||
`indices` selects original row positions, intersecting a name filter when both
|
||||
are supplied; empty filters select nothing. `source_index` always remains and,
|
||||
with the parent ID, identifies the original export or issue row. Fields default
|
||||
to the complete row; an empty field list returns only its identity. ELF `name`
|
||||
selection also returns raw-byte and encoding qualifications. Fields from the
|
||||
other format are rejected. `collection: "issues"` pages the complete diagnostic
|
||||
records, with optional original indices. Offset and limit default to 0 and 64
|
||||
and apply to matching rows in original order.
|
||||
|
||||
View `coverage` describes the selected page; `summary.coverage` or
|
||||
`exports.coverage` preserves source-table coverage. An exhausted or empty page
|
||||
does not make a partial source complete. Retained views never reopen the file.
|
||||
The CLI uses the same projector and requires a durable bundle for a summary:
|
||||
|
||||
```sh
|
||||
rea inspect-native-exports ./libexample.so --detail summary --evidence_output ./exports.bundle.json --json
|
||||
# Pass the summary's parent ID and the same view as JSON to inspect-analysis-view,
|
||||
# together with --evidence-bundle ./exports.bundle.json.
|
||||
```
|
||||
|
||||
- PE rows reproduce the export directory: each used export-address-table slot
|
||||
with its biased `ordinal` (`OrdinalBase` plus the slot index), one row per
|
||||
name (aliases share an ordinal), `address`
|
||||
as the preferred-base virtual address, or `forwarder` text when the slot
|
||||
forwards to another DLL. Unused zero-address slots are omitted;
|
||||
ordinal-only exports have a null `name`. The name-ordinal table stores an
|
||||
unbiased 16-bit EAT index; reported biased ordinals may exceed 16 bits. The
|
||||
directory's DLL name, ordinal base and timestamp are reported, and an image
|
||||
without an export directory is a complete empty inventory.
|
||||
- ELF rows list defined `.dynsym` symbols with global, weak or GNU-unique
|
||||
binding and default or protected visibility. Each row keeps `value`
|
||||
(`st_value` as stored; bit 0 marks Thumb code on 32-bit ARM), `size`, `type`,
|
||||
`binding` and `visibility` (`"default"` or `"protected"`; `"unknown"` in older
|
||||
historical Evidence where visibility was not recorded). ELF string tables do
|
||||
not declare a text encoding. Names that
|
||||
contain non-ASCII bytes are decoded as UTF-8 when those bytes form valid
|
||||
UTF-8; `name_utf8_decodable` records whether the non-ASCII name bytes are
|
||||
valid UTF-8 and `name_bytes_base64` retains those exact bytes (`null` for
|
||||
ASCII names). When they are not valid UTF-8, `name` uses a byte-preserving
|
||||
Latin-1 mapping and the raw bytes remain available. Older retained results
|
||||
report null for the newly added encoding fields. `version` is null for an
|
||||
unversioned symbol. A versioned
|
||||
symbol reports its GNU index, a name resolved from `.gnu.version_d` or
|
||||
`.gnu.version_r`, the corresponding `name_bytes_base64` and
|
||||
`name_utf8_decodable` facts, and the `hidden` flag from `.gnu.version`.
|
||||
Version names use the same UTF-8-when-valid, otherwise byte-preserving
|
||||
Latin-1 display mapping as symbol names; ELF does not declare either
|
||||
string's text encoding. Older retained results report null for the added
|
||||
version-name encoding fields. `"unknown"`
|
||||
means the symbol-version table could not be read. A hidden version is not
|
||||
the default version that a by-name `dlsym` lookup selects.
|
||||
Definition names take precedence when both tables name the same index.
|
||||
|
||||
Unreadable names, out-of-range name ordinals and unreadable forwarders are
|
||||
listed in `issues`, their entries are not reported, and coverage is `partial`.
|
||||
PE export names are bounded null-terminated byte strings and may continue
|
||||
across adjacent file-backed section mappings when the virtual bytes are
|
||||
unambiguous. Gaps, zero-fill and conflicting mappings remain unreadable names.
|
||||
Unresolved symbol versions keep their rows: the version name is null when no
|
||||
definition or requirement names the index, and `version` is
|
||||
`"unknown"` when the version table cannot be read; coverage is likewise
|
||||
`partial`. A dynamic symbol table without its null entry is malformed. An ELF
|
||||
without section headers is `unsupported`. With section headers, a missing
|
||||
`SHT_DYNSYM` section is also `unsupported` when a `PT_DYNAMIC` segment is
|
||||
observed, including a directory containing only the inactive null section.
|
||||
Enumerating dynamic symbols from that segment is not implemented. A present
|
||||
section directory with neither a dynamic symbol table nor a dynamic segment
|
||||
reports a complete empty inventory. Mach-O exports are reported by
|
||||
`inspect_macho`.
|
||||
|
||||
`max_file_bytes` defaults to 256 MiB (maximum 512 MiB; CLI `--max-file-bytes`).
|
||||
Export directories in PE headers are read like those in sections. An export or
|
||||
dynamic symbol table above 262144 entries, more than 262144 rows, or more than
|
||||
16 MiB of retained names fails without partial success. Export rows do not establish that a symbol is reachable, safe to call
|
||||
or loaded at runtime.
|
||||
+35
-332
@@ -12,51 +12,17 @@ For request/response bodies and traffic debugging, see
|
||||
[Fiddler Everywhere captures and MCP](web-network-captures.md#reading-fiddler-captures-saz).
|
||||
Inspect native SAZ or HAR 1.2 offline with REA, or connect Everywhere's separate MCP server.
|
||||
|
||||
HTTP discovery and browser WebSocket inputs require an explicit port. An
|
||||
explicit default `:80` remains valid and is retained in the canonical endpoint;
|
||||
omitting the port remains invalid. HTTP `/json/list` discovery preserves usable
|
||||
siblings when optional metadata is malformed: attachment becomes unknown and
|
||||
row/field diagnostics appear in `limitations`. Target identity and socket
|
||||
authority are validated independently before selection and connection.
|
||||
|
||||
## Shipped surfaces
|
||||
|
||||
- `list_browser_targets` / `rea list-browser-targets` discovers all eligible page targets at the endpoint by default, with an optional exact-origin filter.
|
||||
- `inspect_web_page` / `rea inspect-web-page` captures DOM structure, accessibility nodes, scripts, resources, safe response/DOM metadata, attach-window network and console metadata, WebSocket frame sizes, workers, quota, and optionally selected storage key names, redacted storage-content fingerprints, or script sources.
|
||||
- `analyze_web_bundle` / `rea analyze-web-bundle` parses selected script artifacts without execution and derives chunk edges, route and endpoint candidates, vendor fingerprints, static WebMCP declarations, and optionally source-map/original-source evidence.
|
||||
- `observe_web_session` / `rea observe-web-session` arms an observation window of the requested duration for an external user action and records ordered reload, SPA navigation, redirect, failure, lifecycle, and target-termination metadata.
|
||||
- `discover_webmcp_tools` / `rea discover-webmcp-tools` uses the experimental CDP WebMCP domain to return observed in-scope registrations and their complete observed `input_schema` declaration and canonical `input_schema_sha256` digest inline. Declarations are untrusted JSON data, including declared types, required fields, enums, examples and reference URLs. REA does not resolve schema references. An omitted optional `inputSchema` is a valid parameterless registration, represented by null schema and digest. These fields replace `input_schema_shape`. Registrations with the same name and URL in distinct observed frames retain separate identities. `tool_key` includes the observed frame ID; it is not a stable identity across navigation or browser sessions. Same-frame registration updates replace that owner only, and removal leaves other owners intact. Declared `readOnly`, `untrustedContent`, `consequential`, and `autosubmit` annotation hints are reported per registration: `true`/`false` are declared values, `null` means the declaration did not provide the hint in that capture, and a missing `consequential` key means the hint was not recorded, either in a historical capture or because the observed annotation was invalid. Consult discovery completeness and exclusions to distinguish these cases. Malformed annotation values or registration stacks retain the registration with incomplete coverage and an `invalid_protocol_value` exclusion. Other malformed protocol payloads can fail discovery explicitly. REA never exposes `WebMCP.invokeTool`.
|
||||
|
||||
- `discover_webmcp_tools` / `rea discover-webmcp-tools` uses the experimental CDP WebMCP domain to return every in-scope registration and its complete observed `input_schema` declaration and canonical `input_schema_sha256` digest inline. Declarations are untrusted JSON data, including declared types, required fields, enums, examples and reference URLs. REA does not resolve schema references. These fields replace `input_schema_shape`. Registrations with the same name and URL in distinct observed frames retain separate identities. `tool_key` includes the observed frame ID; it is not a stable identity across navigation or browser sessions. Same-frame registration updates replace that owner only, and removal leaves other owners intact. Declared `readOnly`, `untrustedContent`, `consequential`, and `autosubmit` annotation hints are reported per registration: `true`/`false` are declared values, `null` means the declaration did not provide the hint in that capture, and a missing `consequential` key means the hint was not recorded, either in a historical capture or because the observed annotation was invalid. Consult discovery completeness and exclusions to distinguish these cases. Malformed annotation values retain the registration with incomplete coverage and an `invalid_protocol_value` exclusion. Other malformed protocol payloads can fail discovery explicitly. REA never exposes `WebMCP.invokeTool`.
|
||||
- `compare_web_captures` / `rea compare-web-captures` compares passive captures or exact-step-aligned browser scenarios. Scenario results commit reproducible literal normalization and expose alignment failures plus artifact-level action, screenshot, DOM, accessibility, URL, history, storage, and event differences. Missing or truncated evidence is never treated as equivalence.
|
||||
|
||||
For a compact route query, select `detail: "summary"` and a
|
||||
`view: {kind: "javascript-routes", collection: "routes", limit: 20}`. The
|
||||
shared [route inventory](javascript-route-inventory.md) preserves source IDs,
|
||||
precise locations, partial coverage, and the complete parent Evidence for
|
||||
subsequent filtered pages. Static candidates never establish complete application
|
||||
coverage or reachability.
|
||||
|
||||
For `analyze_web_bundle`, each `webmcp_declarations` entry's
|
||||
`schema_property_names` is a source-derived projection of statically known own
|
||||
property keys, not schema validation. `schema_property_names_coverage` reports
|
||||
how complete that projection is: `complete` means a literal properties object
|
||||
was fully enumerated (including an empty literal); `partial` means a literal
|
||||
object was available but some computed or spread membership was unresolved,
|
||||
and the name array retains keys that were known; `unknown` means the schema or
|
||||
properties owner could not be enumerated as an object literal, including
|
||||
explicit non-object values; `not_present` means no effective schema or own
|
||||
`properties` member was present after the `inputSchema ?? input_schema` alias
|
||||
selection. `not_present` describes this projection only: it does not validate a
|
||||
schema or rule out properties expressed through other schema constructs. Older
|
||||
observations may omit the coverage field; treat omission as `unknown` rather
|
||||
than inferring completeness from the array.
|
||||
|
||||
Passive DOM comparison checks complete node inventories and complete frame
|
||||
parent/child structure independently. Node value lengths are comparable only
|
||||
when both nonempty snapshots declare the same `node_value_length_unit`.
|
||||
Missing or different units leave length comparison unknown; independent node
|
||||
or frame structural differences can still prove change. Empty inventories have
|
||||
no value lengths to compare. Frame IDs and sibling enumeration order
|
||||
parent/child structure independently. Frame IDs and sibling enumeration order
|
||||
are capture-local and do not establish a change; duplicate IDs, missing parents,
|
||||
and cyclic ancestry remain unknown. A complete frame difference can still prove
|
||||
change when DOM nodes are incomplete, and vice versa. Partial DOM or metadata
|
||||
@@ -65,57 +31,6 @@ from a protocol omission. Rejected DOM facts also mark derived URL and agent-hin
|
||||
metadata incomplete, while valid facts from other nodes and documents remain
|
||||
available in the capture.
|
||||
|
||||
Page network and console coverage starts independently at the first observed
|
||||
event from that domain or its successful enable acknowledgment. The capture's
|
||||
lifecycle start is earlier and does not establish domain readiness. Events
|
||||
received before the enable reply remain part of the observation.
|
||||
|
||||
Worker output is a final target-inventory snapshot. `attached` describes any
|
||||
debugging client, not an REA-owned observing session. The selected page-session
|
||||
collector does not collect worker-session requests, console events, or scripts,
|
||||
and workers that disappeared before the snapshot may be missing; worker
|
||||
coverage is therefore attach-limited. Missing ownership metadata is reported as
|
||||
`unattributed_target` with unavailable worker attribution, separately
|
||||
from a proven foreign owner excluded as `out_of_target_scope`. A shared origin
|
||||
alone establishes neither relationship.
|
||||
|
||||
Passive comparison keeps lookup identity, cross-capture correspondence, content
|
||||
evidence, and inventory coverage separate:
|
||||
|
||||
| Observation | Comparison outcome |
|
||||
| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
|
||||
| Renamed WebMCP tool keys/frame IDs with one matching name, owner origin, and frame URL | Compare the registration content; local IDs do not prove change |
|
||||
| Duplicate WebMCP owners with the same name, origin, and frame URL | Unknown correspondence; complete group count differences still prove change |
|
||||
| Matching script inventory keys with retained source digests | Compare the retained SHA-256 values even without a producer hash |
|
||||
| One corresponding script source is unavailable, or neither source nor producer hash is available | Source equality remains unknown; inventory changes and other proven differences survive |
|
||||
| A partial HTTP response group is a subset of the complete peer | Unknown; omitted repeated responses can explain the difference |
|
||||
| An observed HTTP response fingerprint is absent from the complete peer group | Changed; omissions cannot explain that retained response |
|
||||
| A WebSocket connection or frame inventory is incomplete | Do not align retained payloads by position |
|
||||
|
||||
Storage fingerprints decode supported CDP primitive/JSON values before hashing.
|
||||
Debugger descriptions, previews, and remote references do not represent storage
|
||||
content. Actual JSON properties with those names remain content. Reference-only
|
||||
objects, unsupported values, and opaque cookie partition identities remain
|
||||
incomplete. Previously exported fingerprints are not recomputed; recapture both
|
||||
sides when comparing captures produced across this decoder change.
|
||||
IndexedDB schema fingerprints with `projection_version: 2` include the observed
|
||||
database version and object-store definitions. A missing, malformed, or imprecise
|
||||
version leaves the schema fingerprint incomplete while retaining observed store
|
||||
facts. Historical fingerprints without this marker covered object stores only:
|
||||
matching historical hashes cannot establish unchanged database versions, and a
|
||||
historical hash is not compared with a version-bearing hash. Recapture both
|
||||
sides for a complete IndexedDB version comparison; other proven storage changes
|
||||
remain reportable.
|
||||
|
||||
Script comparison groups observations by reported URL and compares unordered
|
||||
content samples. Producer inventory keys include optional metadata, so a changed
|
||||
key cannot by itself prove script removal or addition. Missing module, length,
|
||||
language or source-hash facts remain unknown; shared known values and complete
|
||||
retained source digests can still prove a group change. Partial inventories use
|
||||
the same omission rule as repeated HTTP samples. WebMCP dropped-tool counts also
|
||||
prevent equality, and two incomplete owner inventories cannot prove tool
|
||||
correspondence merely from their different retained registrations.
|
||||
|
||||
- `capture_web_screenshot` / `rea capture-web-screenshot` returns a content-addressed visible-viewport PNG.
|
||||
- `compare_web_screenshots` / `rea compare-web-screenshots` performs bounded local PNG pixel comparison without OCR or external services.
|
||||
- The CLI comparison commands accept each capture, screenshot artifact or normalization policy as inline JSON or a JSON file path. Use files for screenshot artifacts and scenario captures, whose embedded PNG bytes can exceed the host's command-line length limit.
|
||||
@@ -159,11 +74,6 @@ preserve these authority and lifecycle differences. They share one exclusion
|
||||
vocabulary (`BrowserExclusionReason`): every denial reason maps onto it, even
|
||||
where a wire schema keeps a narrower historical bucket.
|
||||
|
||||
Passive page and Inspector capture enable the `Debugger` domain. A target's
|
||||
`debugger` statement can then pause execution; these observation paths do not
|
||||
suppress pauses with `Debugger.setSkipAllPauses` or resume the target. Their
|
||||
lack of explicit pause commands does not guarantee uninterrupted execution.
|
||||
|
||||
CDP attachment is not a guarantee that the browser preserves all state.
|
||||
Chromium can clear another client's `navigator.platform` emulation when REA's
|
||||
page session disconnects, even if that session sent no emulation commands.
|
||||
@@ -199,11 +109,14 @@ bundle, for example
|
||||
`"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"`. Fedora installs
|
||||
Chromium as `chromium-browser`.
|
||||
|
||||
REA attaches through the browser's HTTP discovery endpoint on a loopback port,
|
||||
or, for some passive tools, through a
|
||||
[browser socket](#browsers-without-http-discovery). Connection failures report
|
||||
`endpoint_unreachable`; a reachable HTTP service that returns a non-200 or
|
||||
non-JSON discovery response reports `invalid_endpoint_response`.
|
||||
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
|
||||
@@ -255,9 +168,7 @@ file for you.
|
||||
the origins you are investigating.
|
||||
|
||||
A browser driven only through `--remote-debugging-pipe` or a vendor's private
|
||||
automation channel has neither endpoint, and REA cannot attach to it. To
|
||||
inspect such a page, reproduce it in a Chromium-family browser started with
|
||||
`--remote-debugging-port`.
|
||||
automation channel has neither endpoint, and REA cannot attach to it.
|
||||
|
||||
## Request boundary
|
||||
|
||||
@@ -290,11 +201,6 @@ rea inspect-web-page http://127.0.0.1:9222 TARGET_ID \
|
||||
--json
|
||||
```
|
||||
|
||||
Passive observation honors the requested duration using cancellable waits within
|
||||
the runtime's timer range. Long windows stay pending across successive waits;
|
||||
caller cancellation, target termination or leaving the allowed origins ends
|
||||
collection early. `window_elapsed` identifies completion of the requested wait.
|
||||
|
||||
Static bundle analysis and a user-driven observation window are separate operations:
|
||||
|
||||
```bash
|
||||
@@ -334,11 +240,7 @@ read as registration state only after domain availability is confirmed.
|
||||
Discovery is attach-only: the command needs a reachable loopback CDP endpoint
|
||||
and a target ID from `list_browser_targets`, and it never navigates, reloads,
|
||||
or evaluates the page. Observe the page in the state you want to measure;
|
||||
registered tools reflect what the protocol delivered. If an in-scope subframe
|
||||
already exists when discovery attaches, `webmcp_tools` remains attach-limited:
|
||||
Chromium replay may omit its older registrations. Later live registrations
|
||||
retain their frame ownership but do not establish recovery of those missing
|
||||
descriptors. REA does not reload, evaluate, or invoke tools to fill that gap.
|
||||
registered tools reflect the document as loaded.
|
||||
|
||||
Accessibility names/descriptions, console primitive text, JSON body shapes, WebSocket shapes, script content, and storage key names are selected independently:
|
||||
|
||||
@@ -380,102 +282,22 @@ are reported in the result.
|
||||
Call `list_browser_targets` first because target IDs are browser-instance-specific. REA rechecks the selected target's current type and origin immediately before attaching. Without an origin filter, discovery lists all eligible pages; an individual capture defaults to the selected page's current origin.
|
||||
|
||||
REA establishes a main-frame origin boundary before enabling Runtime, Debugger, or Network observation, rechecks it before document capture, and validates it again afterward. If the main frame navigates during final capture, REA discards the mixed result and returns `target_changed`.
|
||||
An observed navigation also takes precedence over failures from stale
|
||||
document reads, such as a script ID invalidated by reload; cancellation remains
|
||||
cancellation. Retry after navigation settles, or refresh target discovery if
|
||||
the selected page was replaced. Origin-scope refusals from page inspection,
|
||||
runtime observation, screenshots, and WebMCP discovery explain the permitted
|
||||
HTTP(S) origin scope and direct the caller to select a permitted document or
|
||||
explicitly revise `allowed_origins`. Page capture also preserves an observed
|
||||
main-frame scope violation when a later document read fails. An unresolved
|
||||
initial main frame does not establish an origin violation; its bounded startup
|
||||
retries retain a distinct refusal.
|
||||
|
||||
Screenshot capture requires a valid main-frame identity. Malformed identity
|
||||
produces a protocol failure; a readable child frame cannot substitute for the
|
||||
main frame.
|
||||
|
||||
The web-session timeline uses CDP monotonic seconds where the event producer
|
||||
provides a comparable timestamp; absent or invalid timestamps are `null` rather
|
||||
than a synthetic zero. Events buffered while REA samples the window boundary are
|
||||
retained only when their timestamp places them after that boundary; older events
|
||||
are excluded. A buffered event without a comparable timestamp may remain as
|
||||
contextual evidence with `timestamp: null`, accompanied by a limitation stating
|
||||
that it does not establish activity in the selected window. Main-frame scope
|
||||
enforcement still applies to contextual navigation events. Network redirect
|
||||
events retain their paired prior-hop response context when the timestamped
|
||||
next-request event falls inside the window. A redirect row's optional
|
||||
`redirect_response` contains the prior response `url`, CDP-reported `status`,
|
||||
and `scope`. The URL has transport credentials removed; URL and status are
|
||||
withheld unless its origin is approved. Missing or malformed response URLs have
|
||||
null scope and add `invalid_protocol_value` timeline coverage; a valid URL with an
|
||||
unsupported protocol has `unsupported` scope. Outside-policy and unsupported
|
||||
responses mark the timeline as policy-filtered with `disallowed_origin` or
|
||||
`unsupported_url`. Missing status remains null rather than zero. A supplied
|
||||
non-numeric status is also null and records `invalid_protocol_value` coverage;
|
||||
the other valid response facts remain available. Historical rows may omit this
|
||||
field; omission does not establish that no prior response existed. Response
|
||||
headers and bodies are discarded. The row timestamp belongs to the next request,
|
||||
not an exact prior response time. The pre-arm timeline buffer is bounded
|
||||
by original CDP message bytes. If clock sampling completes after that budget is
|
||||
exhausted, the operation returns `payload_limit` instead of a partial window.
|
||||
Cancellation and clock-request failures retain their own outcomes. Session
|
||||
`load_failed` rows require a previously observed main-frame `Document` request;
|
||||
subresources, child-frame requests and failures without admitted request ownership
|
||||
are excluded. Redirects reuse their request identity, and completed/failed requests
|
||||
release it. Active request identity metadata uses the existing 8 MiB retained-event
|
||||
allowance; exhaustion fails the observation explicitly instead of guessing scope.
|
||||
|
||||
## Data minimization
|
||||
|
||||
Response metadata parses repeated CDP header field lines independently. The
|
||||
Link projection retains their declarations in order. `csp.directives` lists the
|
||||
directives of every CSP policy in order, across field lines and comma-separated
|
||||
policies, without recording policy boundaries, so a directive name can repeat
|
||||
across policies. As in CSP3, a repeated name within one policy is ignored.
|
||||
Single-valued policy fields combine their lines as one comma-separated value,
|
||||
as RFC 9110 specifies. Each link keeps its target in `href` and
|
||||
its relationship context in `context_url` with a separate `context_scope`;
|
||||
absent `anchor` uses the response URL, while a relative `anchor` resolves
|
||||
against that URL. Anchored links remain in the projection, but a `link_rel`
|
||||
agent hint is emitted only when the resolved context matches the response URL
|
||||
before userinfo credentials are removed. Following [RFC 8288](https://www.rfc-editor.org/rfc/rfc8288.html),
|
||||
the `rel`, `anchor`, and `type` parameters use their first occurrence, and
|
||||
quoted-pair escapes are decoded as specified by [RFC 9110 §5.6.4](https://www.rfc-editor.org/rfc/rfc9110.html#section-5.6.4).
|
||||
The context fields are optional when reading older saved captures. Commas
|
||||
inside URI targets or quoted parameters remain part of those values.
|
||||
Well-known-resource hints require an exact supported pathname; path case is
|
||||
preserved because a case variant may identify a different HTTP resource.
|
||||
COOP and COEP projections retain the exact declared token after validating the
|
||||
complete Structured Field Item, including parameters such as `report-to`.
|
||||
These are declarations, not an assessment of the document's effective policy;
|
||||
absent, malformed or non-token items project as `null`.
|
||||
|
||||
Console entries, WebSocket frame summaries, and selected request/response JSON
|
||||
shape outputs share one 8 MiB retained-observation allowance per capture. Shape
|
||||
inference admits expanded path bytes before allocating those paths, including
|
||||
escaped and Unicode property names. Replacing or removing a request's shapes
|
||||
refunds their retained charges. A refused body shape preserves network metadata
|
||||
and any captured peer shape with `resource_budget_exhausted` coverage; a refused
|
||||
WebSocket frame is counted as dropped rather than reclassified as non-JSON text.
|
||||
The allowance has no runtime override. Protocol messages and JSON input parsing
|
||||
remain subject to their separate transport admission; this output allowance does
|
||||
not bound the whole process heap.
|
||||
|
||||
REA retains local observation data and removes only URL userinfo credentials, explicitly declared scenario secrets, and values from structured credential/storage fields:
|
||||
|
||||
- URL observations preserve the complete local URL, including query values, fragments, duplicate parameters, and their order. Only username/password userinfo is removed; malformed original URL strings remain available with `origin: null`.
|
||||
- DOM snapshots retain node types, node names, value lengths, and attribute names, but not text or attribute values. `dom.node_value_length_unit` is `utf16-code-units` for web observations and applies to every node's `node_value_length`; it counts JavaScript string code units, not Unicode code points or UTF-8 bytes. Electron observations declare `utf8-bytes` instead. Historical captures without the unit leave it unknown; reading them does not infer a unit or convert their stored lengths.
|
||||
- Supported DOM URL attributes are projected separately as resolved destinations. Empty `action` on HTML or XHTML forms and empty `formaction` on HTML or XHTML submit controls resolve to the document URL; other relative URL attributes resolve against the captured document base.
|
||||
- Accessibility structure and reported roles are retained by default, while names and descriptions require `include_accessibility_text: true`. The browser protocol may omit an individual role; that role remains unknown for comparison. Other observed accessibility differences can still establish a change.
|
||||
- DOM snapshots retain node types, node names, value lengths, and attribute names, but not text or attribute values.
|
||||
- Accessibility structure and roles are retained by default, while names and descriptions require `include_accessibility_text: true`.
|
||||
- Network observations retain method, status, MIME type, size, type, initiator stack location, and complete URLs with userinfo credentials removed. Redirects reported by CDP are attached as ordered `redirects` on the one request ID, with the prior request URL, CDP response URL, method/type, response status/MIME/encoded length, request timestamp, and the next `requestWillBeSent` event timestamp. CDP delivers `redirectResponse` with that next request event, so the latter is an event boundary rather than an exact response timestamp. Redirect response headers and bodies are discarded; hops outside the selected origins are discarded with the request chain. Headers are discarded after an allowlisted projection of length/encoding, structured CSP/Link/policy fields, and untrusted agent hints. Cookies and authorization headers are never retained.
|
||||
- Request/response bodies are not requested or parsed by default. When selected, only allowed-origin JSON media types are read, converted immediately to complete value-free typed paths, types and observation counts, and discarded. Each path segment is either `{ "kind": "property", "name": "raw property name" }` or `{ "kind": "array-element" }`; array-element types include primitives and nested arrays. Literal `*` properties remain distinct from array elements. `Network.getResponseBody` is never sent unless JSON body shape capture is selected. A JSON-typed request body that CDP reports through `hasPostData` but omits from `postData`, or that cannot be parsed as JSON, makes `json_body_shapes` unavailable. That request's `body_shapes.status` is `partial`, not `included`, when only its response shape was captured. Base64 response bytes must be valid UTF-8 before JSON interpretation; invalid bytes leave the response shape unavailable and preserve any observed request shape and network metadata. Producer-decoded strings and genuine U+FFFD characters retain their reported meaning. Comparison uses body shapes only when both captures selected them, and compares request/response shapes only where both observations provide those facts. Unavailable or truncated selected coverage makes equivalence unknown; observed differences still report changed.
|
||||
- Request/response bodies are not requested or parsed by default. When selected, only allowed-origin JSON media types are read, converted immediately to complete value-free typed paths, types and observation counts, and discarded. Each path segment is either `{ "kind": "property", "name": "raw property name" }` or `{ "kind": "array-element" }`; array-element types include primitives and nested arrays. Literal `*` properties remain distinct from array elements. `Network.getResponseBody` is never sent unless JSON body shape capture is selected. Comparison uses body shapes only when both captures selected them, and compares request/response shapes only where both observations provide those facts. Unavailable or truncated selected coverage makes equivalence unknown; observed differences still report changed.
|
||||
For normalized passive inputs with complete request inventories on both sides,
|
||||
request additions and removals remain observable even when selected body shapes,
|
||||
response metadata, or WebSocket coverage are unavailable. HTTP request and
|
||||
WebSocket inventory coverage are evaluated separately; complete HTTP coverage
|
||||
cannot prove a missing WebSocket connection. A missing request in a limited inventory cannot prove absence; a retained
|
||||
request absent from a complete peer can prove a change. This comparison rule does not make live CDP's
|
||||
cannot prove a missing WebSocket connection. Limited request inventory still
|
||||
prevents request-absence claims. This comparison rule does not make live CDP's
|
||||
attach-limited inventory complete or establish body-shape equality.
|
||||
An unavailable HTTP resource type also leaves request identity incomplete:
|
||||
observations with the same method and URL remain possible matches across types,
|
||||
@@ -490,19 +312,15 @@ REA retains local observation data and removes only URL userinfo credentials, ex
|
||||
uses `websocket_connections` and `websocket_frames`. Drops in other event kinds
|
||||
and the aggregate total do not by themselves limit HTTP inventory. Proven
|
||||
modifications remain reportable despite incomplete inventory.
|
||||
Repeated HTTP samples retain multiplicity. A partial group that is contained
|
||||
in a complete peer cannot prove modification, even when the omitted response
|
||||
had different content. A retained sample absent from the complete peer can
|
||||
prove modification. Two partial groups cannot establish response
|
||||
correspondence from their different retained samples alone.
|
||||
Repeated identical HTTP content samples establish multiplicity changes only
|
||||
with complete request inventories; limited inventories retain known content
|
||||
comparisons without interpreting duplicate sample counts as changes.
|
||||
WebSocket stream inventories and connection counts likewise require complete
|
||||
WebSocket coverage. Retained frame positions are compared only with complete
|
||||
connection/frame inventories; matching direction/opcode sequences alone do
|
||||
not prove alignment after drops. Redirect resource types use only shared known metadata:
|
||||
WebSocket coverage. Redirect resource types use only shared known metadata:
|
||||
an omitted type leaves equality unknown rather than proving a modification.
|
||||
- Console observations with a stack source retain call type, argument types, the CDP `Runtime.Timestamp` in milliseconds since Unix epoch, and source location. An absent or invalid timestamp is `null` and makes `console_events` incomplete; an observed zero remains zero. Argument types are listed by CDP argument position. An absent or non-array argument inventory is `null`, and a malformed argument keeps its position as `unknown`; either makes `console_events` incomplete, while an observed empty inventory remains `[]`. A malformed stack or top stack frame is not replaced by its caller: the console event is excluded, and a request initiator location or WebMCP `registration_source` is `null`; each is reported as `invalid_protocol_value`. Source locations are CDP's zero-based lines and UTF-16 columns. A negative column, which Chrome reports on the first line of some inline scripts, is kept as reported and does not locate a column in the source URL; an invalid line or a non-integer column is `null` (WebMCP `registration_source` is `null`) and is reported as `invalid_protocol_value`. This epoch-millisecond clock is separate from the monotonic-second timestamps in `observe-web-session`. When console text capture is selected, already-delivered primitive values are retained verbatim with their original `argument_index`; objects, getters, and remote properties are never expanded. Text capture is `unavailable` when the argument inventory is unknown.
|
||||
- WebSocket observations retain direction, opcode, and payload byte length. A missing or malformed CDP opcode is returned as `null`, with opcode-dependent payload byte length and shape also `null`, and marks `websocket_frames` incomplete. Missing or wrong-typed payload data is not treated as empty; its byte length is `null`, and a text payload shape is unknown. Numeric opcode `0` and an observed empty payload remain distinct from missing data. Opcode is nullable in the public inspection schema, so clients must handle unknown protocol values. When shape capture is selected, text messages are classified as text or complete value-free JSON shape; binary bytes, hashes, prefixes, and raw payloads are never retained.
|
||||
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` selection. Cookie fingerprints cover cookies applicable to the current main-frame URL, including its path, rather than every cookie path on that origin. Other storage remains scoped to the origin. Cache fingerprints hash all decoded response bytes, including bodies above 64 KiB, subject to the [64 MiB per-message CDP limit](#request-boundary). Missing or undecodable cache bodies and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
|
||||
- Console observations with a stack source retain call type, argument types, timestamp, and source location. When console text capture is selected, already-delivered primitive values are retained verbatim; objects, getters, and remote properties are never expanded.
|
||||
- WebSocket observations retain direction, opcode, and payload byte length. When shape capture is selected, text frames are classified as text or complete value-free JSON shape; binary bytes, hashes, prefixes, and raw frames are never retained.
|
||||
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` selection. Cookie fingerprints cover cookies applicable to the current main-frame URL, including its path, rather than every cookie path on that origin. Other storage remains scoped to the origin. Cache bodies above 64 KiB and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
|
||||
- Unavailable or malformed storage inventories mark `storage_keys` unavailable, even without fingerprints. Comparisons suppress inferred key additions and removals while preserving modifications proven by complete fingerprints on both sides. Previously exported captures are not retroactively corrected; recapture them to obtain these coverage markers.
|
||||
- Script metadata is included only when CDP supplies a URL on an allowed origin. Stable keys exclude transient CDP script IDs, and exact transient raw URLs are used only during script/resource reconciliation. URL-less scripts are excluded because their origin cannot be established. When CDP supplies an execution-context association, the accepted script retains its authorized frame ID for later attribution. Script metadata admission bounds retained identity and source-map strings to 8 MiB and 50,000 records. When a source-map URL cannot fit, the script identity remains and source-map coverage reports `resource_budget_exhausted`; rejected script observations are counted without retaining their IDs. Source content is omitted unless explicitly requested and is returned inline as a self-verifying artifact with its SHA-256 digest.
|
||||
|
||||
@@ -510,11 +328,6 @@ Frames, resources, scripts, events, and workers outside the selected origin scop
|
||||
Retained workers include validated opener-target and parent-frame IDs when CDP
|
||||
provides them; these relationships do not expand the exact-origin boundary or
|
||||
prove static module ownership.
|
||||
Workers without either relationship are withheld with an `unattributed_target`
|
||||
exclusion and `workers` in `unavailable_sections`: a same-origin URL does not
|
||||
establish which page owns a worker. A supplied relationship to another target or
|
||||
frame remains `out_of_target_scope`. Missing ownership also remains unknown when
|
||||
the worker URL is empty; withheld target details are not returned.
|
||||
|
||||
## Completeness and limits
|
||||
|
||||
@@ -529,57 +342,6 @@ validation and resource bounds. Omitted script metadata makes its affected
|
||||
section explicitly truncated.
|
||||
Disallowed-origin entries are filtered before retention.
|
||||
|
||||
Script metadata and selected source reads use one fixed admitted-script snapshot.
|
||||
New in-scope script IDs still retained when the final CDP read completes are
|
||||
counted in `scripts.total` and `completeness.dropped_events.scripts`, with
|
||||
`scripts` marked truncated; they do not extend the source-read pass. Repeated
|
||||
late events for the same ID count once. The original snapshot is not replaced
|
||||
or reduced by later updates to an existing ID. These omissions do not imply
|
||||
that a source byte or time budget was exhausted.
|
||||
|
||||
Selected script-source collection shares a 32 MiB aggregate UTF-8 retention
|
||||
budget and the `command_timeout_ms` deadline (default 20000 ms) across the
|
||||
capture. The byte budget has no runtime override. An oversized source is
|
||||
omitted while later smaller sources may still fit. When the deadline expires,
|
||||
remaining source reads are skipped. Script metadata, per-item omission reasons,
|
||||
and `script_sources` completeness remain available. A single CDP response can
|
||||
still be materialized up to the separate 64 MiB transport limit before source
|
||||
admission; the retention budget does not bound that transient allocation.
|
||||
|
||||
Bundle parsing and semantic inspection run sequentially in an owned child,
|
||||
without executing captured code. These are separate phases. `max_heap_mb`
|
||||
defaults to 1024 MiB V8 old-space (range 128..16384). `analysis_timeout_ms`
|
||||
defaults to 30000 per script, including startup and transfer.
|
||||
`total_analysis_timeout_ms` defaults to 300000 across script analysis. A
|
||||
per-script deadline above the effective total is rejected. A failed or
|
||||
timed-out script becomes an explicit unknown while completed findings and later
|
||||
scripts remain available. Total-deadline expiry returns a typed timeout with
|
||||
partial Evidence and identifies unstarted scripts. `source_map_fetch_timeout_ms`
|
||||
defaults to 30000 and is an item limitation, not that analysis timeout. These
|
||||
controls are also available as CLI flags with hyphenated names. Worker protocol
|
||||
and result byte budgets have no runtime override.
|
||||
|
||||
One web-bundle capture/analysis is admitted per REA process at a time. Completed
|
||||
script results share an 8 MiB serialized transfer budget, with host heap
|
||||
reservation checked before decoding and validating another result.
|
||||
When the host reservation is what reaches zero, the failure names that heap
|
||||
headroom and the remaining fixed budget. When the 8 MiB allowance itself is
|
||||
exhausted, the failure names that fixed budget and says it has no runtime
|
||||
override. Either stop ends further script launches and returns a typed
|
||||
resource failure with completed facts and explicit unstarted-script unknowns.
|
||||
Worker control frames are limited to 64 KiB. Cleanup must confirm child termination
|
||||
before releasing its private files; failed cleanup retains its exact owner and
|
||||
blocks replacement until retry succeeds. Cancellation retains completed bundle
|
||||
facts, including cancellation during final cleanup.
|
||||
|
||||
The worker heap setting is not a process-family RSS limit. Node enforces V8
|
||||
old-space flags; Bun ignores them. Windows Bun instead uses an owned process
|
||||
commit limit with a 512 MiB runtime reserve; other Bun hosts have no enforced
|
||||
memory limit. An allocation failure before that Windows limit is reported as
|
||||
reached still names the configured limit and says it was not reached. These
|
||||
bounds do not establish an aggregate host-memory limit across other REA
|
||||
operations or external processes.
|
||||
|
||||
Resource-tree inventories distinguish a valid empty `resources` array from a
|
||||
missing or malformed collection. Invalid resource entries are omitted with
|
||||
incomplete coverage, while valid resources from that frame and other frames
|
||||
@@ -591,31 +353,13 @@ An explicitly empty MIME type is valid. The optional content size may be absent;
|
||||
an invalid supplied size remains unknown with incomplete resource coverage while
|
||||
the resource's other facts are retained. Zero and nonnegative fractional sizes
|
||||
are preserved.
|
||||
`resource_key` remains the exact observation identity used by script
|
||||
reconciliation. Capture comparison groups resources by URL, origin, type and
|
||||
MIME type, then compares known sizes within a group. If a matched group contains
|
||||
an unknown size, comparison reports unknown rather than treating the observation
|
||||
as a resource addition or removal; complete inventory coverage still establishes
|
||||
when an entire group is absent.
|
||||
|
||||
CDP discovery has a 10-second response deadline. HTTP discovery documents
|
||||
use an 8 MiB budget. Browser-socket discovery (`Browser.getVersion`,
|
||||
`Target.getTargets`) uses the 64 MiB protocol message budget. Neither budget
|
||||
nor the discovery deadline has a runtime override. `list_browser_targets` accepts no
|
||||
`command_timeout_ms`: HTTP discovery and browser-socket discovery
|
||||
(`Browser.getVersion`, `Target.getTargets`) stay on that fixed deadline, and
|
||||
the socket closes without a later command. Other CDP tools use
|
||||
`command_timeout_ms` (default 20000) for each WebSocket handshake and command
|
||||
response, separate from the caller-selected observation window, discovery, and
|
||||
the 5000 ms cleanup grace. A browser-level socket's handshake and the following
|
||||
`Target.attachToTarget` each receive that full deadline; they do not share one
|
||||
budget. A stalled handshake or command reports
|
||||
`provider_timeout` and names `command_timeout_ms` only when that operation
|
||||
accepts it; caller cancellation still reports cancellation. WebSocket messages
|
||||
remain subject to the [64 MiB limit](#request-boundary), which has no runtime
|
||||
override. REA does
|
||||
not impose a message-count or pending-command quota. On cancellation, it closes
|
||||
only its own WebSocket,
|
||||
CDP discovery has a 10-second deadline. Each WebSocket handshake and command
|
||||
response has its own 20-second deadline, separate from the caller-selected
|
||||
observation window. A stalled handshake or command reports `provider_timeout`;
|
||||
caller cancellation still reports cancellation. WebSocket messages remain subject
|
||||
to the [64 MiB limit](#request-boundary). REA does not impose a message-count or
|
||||
pending-command quota. On cancellation, it closes only its own WebSocket,
|
||||
waiting up to one second before terminating it. Malformed unsolicited events
|
||||
and malformed replies to pending commands poison the connection so later
|
||||
commands fail closed. A correlated reply's session ID must match the command's
|
||||
@@ -624,63 +368,22 @@ 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.
|
||||
|
||||
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 uses `source_map_fetch_timeout_ms` (default 30000) and a 64 MiB aggregate streamed-response budget across redirects. The byte budget has no runtime override. A fetch overrun is an item limitation, not a thrown analysis timeout. 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`.
|
||||
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`.
|
||||
|
||||
A fully read response that fails UTF-8, JSON, mapping, or structural validation
|
||||
retains `artifact: null`, empty original-source, module-edge, and mapping
|
||||
arrays, and an explicit failure limitation. Invalid content is `invalid`;
|
||||
decoder or structural capacity refusal is `fetch_failed`. Its optional
|
||||
`response_identity` records the complete body's SHA-256 digest, byte count,
|
||||
and `fetch-response-body-bytes` representation without retaining malformed
|
||||
text or charging that text to the 32 MiB output budget. These are bytes
|
||||
delivered by Fetch after HTTP content decoding and before UTF-8 decoding, not
|
||||
HTTP wire bytes. The identity is absent when the body was not fully read, and
|
||||
older observations may omit it. A response whose text cannot fit the existing
|
||||
output budget is likewise `fetch_failed` with text omitted and completed-body
|
||||
identity; JSON parsing and source-map decoding are not attempted. Successful and partial maps keep their
|
||||
full text artifacts. Their original text, including an initial BOM or XSSI
|
||||
line, remains distinct from the parsing view that removes that framing.
|
||||
Collection reads the body once and does not raise its response or output limits.
|
||||
|
||||
When fetching is selected, source-map `excluded` facts preserve the capture
|
||||
ledger's reasons and counts for declarations rejected before download. No
|
||||
disallowed URL details are retained or fetched. `requested` and `processed`
|
||||
count the admitted fetch requests; exclusions are counted separately. Excluded
|
||||
declarations make the collection unavailable, or partial when maps were retained.
|
||||
An empty collection with no declarations remains `not_requested`.
|
||||
|
||||
If the caller cancels while later source maps are being fetched, the failure's
|
||||
partial Evidence retains the captured page and every completed map item. Its
|
||||
`normalized_result.kind` is `web-bundle-acquisition` and
|
||||
`analysis_status` is `not_performed`: no script-analysis result is claimed.
|
||||
`source_maps.requested` remains the admitted request count,
|
||||
`source_maps.processed` counts completed items, and any unprocessed requests
|
||||
have an explicit unknown-results limitation. Capture exclusions remain in the
|
||||
same collection. The cancellation error keeps the caller's exact reason;
|
||||
an already observed response resource or cleanup failure keeps its own error
|
||||
category and cleanup owner.
|
||||
|
||||
Source-map items retain the script's `declared_url` separately from the observed
|
||||
`effective_url`. Successful maps use the latter as their source-resolution base.
|
||||
Failed fetches retain the last response URL observed within the approved origins;
|
||||
`effective_url` is `null` when no response was observed. Query parameters are
|
||||
source-map items retain the script's `declared_url` separately from the observed
|
||||
`effective_url`. successful maps use the latter as their source-resolution base.
|
||||
failed fetches retain the last response URL observed within the approved origins;
|
||||
`effective_url` is `null` when no response was observed. query parameters are
|
||||
preserved, and both URLs count toward the inline evidence budget.
|
||||
Current fetches emit a string or null; historical observations may omit
|
||||
`effective_url` without changing their stored content or Evidence identity.
|
||||
An observed URL must fit before its response body is read. If it cannot fit,
|
||||
remaining map results are explicitly unknown; failed cancellation retains its
|
||||
cleanup context and resource constraint.
|
||||
Indexed maps whose decoded mappings cross the next section's starting position
|
||||
are invalid under [ECMA-426](https://tc39.es/ecma426/2024/), rather than complete
|
||||
maps with clipped mapping rows. Fetch collection and source tracing share this
|
||||
validation.
|
||||
|
||||
## Non-goals and threat model
|
||||
|
||||
The browser provider deliberately does not expose generic CDP commands, `Runtime.evaluate`, WebMCP invocation, navigation, input, downloads, page closure, or browser closure. Screenshot capture is the sole pixel surface and returns an image only when requested. REA disables the domains it enabled, detaches the target session, and closes only its own WebSocket.
|
||||
Discovery and attachment errors remain primary if closing REA's socket also
|
||||
fails. The cleanup failure is reported alongside the original command failure;
|
||||
it cannot replace the operation diagnostic.
|
||||
|
||||
This feature is not a browser sandbox or network containment mechanism. The attached page and browser continue running with their existing privileges and may make external requests independently of REA. CDP gives deep access to the selected browser profile, so use a dedicated profile, include only origins you intend to inspect, and treat other same-user processes as outside this boundary.
|
||||
|
||||
|
||||
@@ -10,70 +10,22 @@ liveness deadlines. `INPUT_JSON` may be inline JSON or a JSON file path.
|
||||
The minimal request contains only browser launch/connect selection, `start_url`,
|
||||
and at least one `actions` entry. Launch `headless` defaults to `true` and can
|
||||
be set to `false` where the host supports a visible browser. Navigations follow
|
||||
the destinations named by the scenario and the page's ordinary browser requests,
|
||||
and [request steps](#request-steps) send only to the start URL's origin;
|
||||
there is no separate origin allowlist. Request steps use a temporary dispatch
|
||||
gate for cancellation; ordinary page requests retain their browser behavior. Environment
|
||||
the destinations named by the scenario and the page's ordinary browser requests;
|
||||
there is no separate origin allowlist or request-interception layer. Environment
|
||||
settings (including service workers blocked by default), empty storage, and a
|
||||
final sanitized URL capture are supplied by default. Set
|
||||
`environment.service_workers` to `allow` when the application requires them.
|
||||
The connected service-worker policy owner retains its exact CDP transport
|
||||
through discovery, attachment and restoration. Failed admission preserves its
|
||||
primary diagnostic when socket cleanup also fails; later policy cleanup retries
|
||||
that retained transport and clears cleanup uncertainty only after close succeeds.
|
||||
For attached browsers, `block` rejects captures when the selected page or its
|
||||
frames are controlled by a service worker or already have a registration,
|
||||
including after navigation. Policy checks bracket each artifact capture, including
|
||||
the final step. If an external worker takes control while artifacts are being
|
||||
captured, REA rejects the capture and retains the completed artifacts in
|
||||
`details.diagnostics.collected_artifacts`; they do not establish policy-compliant
|
||||
completion. Existing caller registrations are never removed.
|
||||
including after navigation. Existing caller registrations are never removed.
|
||||
Navigation can run destination code before a newly discovered worker is rejected;
|
||||
that rejection does not roll back browser effects. REA removes its owned
|
||||
future-document scripts, restores live frame methods, then disconnects.
|
||||
Add secret declarations, storage seeds, and additional artifact or event
|
||||
capture only when needed. Missing environment-backed secrets fail as input errors before browser acquisition, naming the declaration and required variable. An explicitly set empty string is a resolved value. Action, secret, and storage counts are not capped.
|
||||
Duration, action, and navigation timeouts remain separate from CDP command
|
||||
deadlines. `command_timeout_ms` (default 20000; omit it to keep that default
|
||||
and leave it out of Evidence) bounds launch startup, the connect-over-CDP
|
||||
handshake, target lookup, environment commands, and storage CDP commands,
|
||||
including replies from attached child targets. It also bounds the initial
|
||||
`start_url` navigation through the load event; a redirect or replacement
|
||||
navigation cannot leave startup waiting indefinitely. Set it in the scenario JSON.
|
||||
`capture-browser-scenario` has no `--command-timeout-ms` option.
|
||||
It does not change action `timeout_ms`, `wait_for_timeout.duration_ms`, or
|
||||
action navigation. Service-worker restoration keeps a fixed 5000 ms grace and does
|
||||
not inherit a raised `command_timeout_ms`. CDP discovery stays on its own
|
||||
fixed deadline. When service workers are blocked, the registration check
|
||||
before discovery has its own fixed 10000 ms deadline; `command_timeout_ms`
|
||||
does not apply because discovery has not started. The discovery deadline
|
||||
covers endpoint discovery only. The following WebSocket handshake and
|
||||
`Target.attachToTarget` each use `command_timeout_ms`. `capture.limits` controls retained event and network-content
|
||||
budgets; it does not change those liveness deadlines.
|
||||
|
||||
## Validate a scenario before running it
|
||||
|
||||
`validate-browser-scenario INPUT_JSON` checks one scenario against the same
|
||||
`browserScenarioSchema` refinement that `capture_browser_scenario` admits, and
|
||||
stops there. `INPUT_JSON` may be inline JSON, a JSON file path, or `-` for
|
||||
standard input.
|
||||
|
||||
```bash
|
||||
rea validate-browser-scenario ./browser-scenario.json --json
|
||||
```
|
||||
|
||||
A valid scenario reports `{"valid":true,"actions":<count>,"secrets":<count>}`
|
||||
and exits 0. An invalid one exits 1 and reports the same field paths capture
|
||||
would report, so a correction made here is the correction capture needs:
|
||||
duplicate or reserved step IDs, duplicate secret identities and
|
||||
environment-variable names, undeclared secret references, request-step
|
||||
violations, and unknown fields.
|
||||
|
||||
Validation performs no browser discovery, launch, attachment, or navigation and
|
||||
constructs no provider. It does not read secret environment variables, so a
|
||||
scenario whose declared variables are unset still validates; the missing-value
|
||||
failure belongs to capture. A schema-valid scenario does not prove that the
|
||||
executable or target is available on the host.
|
||||
capture only when needed. Action, secret, and storage counts are not capped.
|
||||
Duration, action, and navigation timeouts remain fixed provider-owned liveness
|
||||
limits. `capture.limits` controls retained event and network-content budgets;
|
||||
it does not change those liveness deadlines.
|
||||
|
||||
## Minimal launch example
|
||||
|
||||
@@ -155,11 +107,6 @@ Through MCP, pass the same object as the `capture_browser_scenario` arguments.
|
||||
The final step's DOM and accessibility artifacts retain the page state at that
|
||||
capture time inline. REA still launches and cleans up the browser profile.
|
||||
|
||||
Action `timeout_ms` and `wait_for_timeout.duration_ms` accept integers from 1
|
||||
through 2,147,483,647 milliseconds, matching the underlying timer representation.
|
||||
Larger values receive an input error before browser launch. Omit `timeout_ms`
|
||||
to keep an action pending until completion or request cancellation.
|
||||
|
||||
The click's `timeout_ms` covers the click action. A non-navigating `fetch` or
|
||||
delayed render can finish later, so use `wait_for` to observe a page-specific
|
||||
ready condition before final capture. A page that updates synchronously can
|
||||
@@ -197,24 +144,6 @@ distinction: it closes and deletes provider-owned profiles, but only disconnects
|
||||
from an external CDP browser. Real-browser verification checks both outcomes and
|
||||
confirms that an attached external browser remains alive.
|
||||
|
||||
CDP connections leave Playwright's default-context overrides disabled. The
|
||||
selected page receives the declared scenario environment and temporary focus
|
||||
emulation, so actions can complete while that page is in the background.
|
||||
Its environment owner clears these overrides before disconnecting; other pages
|
||||
keep their media settings. Connecting does not create a bootstrap page.
|
||||
|
||||
That cleanup requires REA to remain running. Abrupt termination, including
|
||||
Windows process-tree termination or POSIX `SIGKILL`, can leave the launch
|
||||
profile and Playwright artifacts behind. A profile can retain cookies, history,
|
||||
cache and origin storage. Later REA runs currently do not reclaim these paths.
|
||||
Playwright 1.58.2 can also leave an artifacts directory when launch preparation
|
||||
fails before a browser process exists, such as for a missing executable.
|
||||
POSIX wrappers can request awaited cancellation with `SIGINT` or `SIGTERM`;
|
||||
Windows process termination is not an equivalent graceful-cancellation channel.
|
||||
Do not identify owned artifacts by directory prefix or creation time alone.
|
||||
See [#4659](https://github.com/morluto/rea/issues/4659) for the remaining recovery
|
||||
and wrapper-cancellation work.
|
||||
|
||||
Keeping that browser alive does not guarantee its emulation state survives.
|
||||
Chromium can clear caller-owned `navigator.platform` emulation when a page CDP
|
||||
session disconnects, including sessions that sent no emulation commands. REA
|
||||
@@ -236,9 +165,7 @@ language environments.
|
||||
HTTP(S) URLs may include ordinary query values and fragments directly. Use
|
||||
ordered structured query entries when a value needs to reference a declared
|
||||
environment-backed secret. Form, storage, and cookie values are either literal
|
||||
strings or declared secret references; request step headers can also read a
|
||||
cookie or local storage value from the page, and request steps and `goto`
|
||||
destinations can send [saved values](#saved-values). Raw URL userinfo credentials are rejected.
|
||||
strings or declared secret references. Raw URL userinfo credentials are rejected.
|
||||
Captured credential-header
|
||||
values are not retained, and declared secret values are redacted automatically.
|
||||
A secret may be declared solely to
|
||||
@@ -255,9 +182,8 @@ validation.
|
||||
|
||||
Cookies are installed before the initial navigation. Local storage entries are
|
||||
seeded once per browser storage partition; session storage entries are seeded
|
||||
once per origin in each instrumented top-level browsing context. Seeding occurs
|
||||
before the first document scripts for the selected page and its instrumented
|
||||
frames. An origin first reached by a later action
|
||||
once per origin in each top-level browsing context. Seeding occurs before the
|
||||
first document scripts for that area. An origin first reached by a later action
|
||||
receives its seed on that first visit. Application updates, deletions, and
|
||||
empty values then persist across later navigations in the same area; a new
|
||||
scenario starts with its own seeds. Partitioned frames can receive separate
|
||||
@@ -265,24 +191,11 @@ local storage seeds even when they share an origin. The pre-script mechanism
|
||||
uses CDP debugger instrumentation. The observer resumes only pauses whose top
|
||||
frame belongs to its private seed script; page breakpoints and `debugger`
|
||||
statements remain under the attached debugger client's control.
|
||||
If Chromium leaves the private isolated world's origin empty, REA looks up
|
||||
that exact frame's reported security origin before selecting a seed. It does
|
||||
not infer the origin from a frame URL or serialized storage key. Unresolved
|
||||
frame identity or security origin fails initialization rather than silently
|
||||
running with an unknown initial state.
|
||||
Popup discovery does not establish that its first script was paused by the
|
||||
selected page's storage instrumentation. When non-cookie seeds are declared
|
||||
and a popup is discovered, the capture retains completed observations and
|
||||
marks `storage` coverage missing, with equality ineligible. This applies even
|
||||
if popup lifecycle events were not selected. Popup first-script storage
|
||||
initialization is not currently guaranteed.
|
||||
|
||||
The result is Evidence with an initial state followed by one record per
|
||||
declared action. Each step reports action status, elapsed time, sanitized URLs,
|
||||
event bounds, and independently typed capture states for screenshot, DOM,
|
||||
accessibility, URL, history, and storage. Actions after a failed or cancelled
|
||||
action are not run and are recorded as `cancelled` with a not-executed error.
|
||||
Console, page-error, network,
|
||||
accessibility, URL, history, and storage. Console, page-error, network,
|
||||
WebSocket, frame, worker, popup, and cancelled-download events are attributed to
|
||||
steps. Retained events and requested artifacts are returned inline; omitted
|
||||
events are counted by kind and identify affected network transactions.
|
||||
@@ -349,263 +262,6 @@ external browser without closing it. The request itself defines the operation;
|
||||
there is no additional REA grant step. The browser and host still enforce their
|
||||
own access and process rules.
|
||||
|
||||
## Request steps
|
||||
|
||||
A `request` step sends one HTTP request from the page with the page's own
|
||||
`fetch`, so the browser attaches the session's cookies and the request is
|
||||
recorded in the step's network events like any request the application makes.
|
||||
Use it to record an application's API, including writes, when driving the UI
|
||||
for each call would be slower or less exact than calling the endpoint the UI
|
||||
calls. It is the only action that changes application state without a user
|
||||
gesture, so run scenarios with write requests only against an instance whose
|
||||
data you may change. This scenario creates one item:
|
||||
|
||||
```json
|
||||
{
|
||||
"browser": {
|
||||
"mode": "launch",
|
||||
"executable_path": "/absolute/path/to/chromium"
|
||||
},
|
||||
"start_url": { "url": "http://127.0.0.1:3000" },
|
||||
"actions": [
|
||||
{
|
||||
"step_id": "create-item",
|
||||
"action": "request",
|
||||
"method": "POST",
|
||||
"path": "/api/items",
|
||||
"query": [
|
||||
{ "name": "notify", "value": { "source": "literal", "value": "false" } }
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"name": "X-CSRFToken",
|
||||
"value": { "source": "cookie", "name": "csrftoken" }
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": {
|
||||
"source": "local_storage",
|
||||
"key": "auth",
|
||||
"json_pointer": "/token"
|
||||
},
|
||||
"prefix": "Bearer "
|
||||
}
|
||||
],
|
||||
"json": { "title": "Scratch item 1" },
|
||||
"expected_status": [201],
|
||||
"timeout_ms": 10000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `method` is `GET`, `HEAD`, `POST`, `PUT`, `PATCH` or `DELETE`. `json` is the
|
||||
body, sent with `Content-Type: application/json` unless a header sets one; a
|
||||
`GET` or `HEAD` step cannot carry it.
|
||||
- `path` starts with one `/` and must resolve to the start URL's origin, which
|
||||
validation checks before the browser starts. When the step runs, the page must
|
||||
be on that origin, checked again in the page as the request is sent; add a
|
||||
`goto` step first if an earlier action left it. A query in `path` is sent as
|
||||
written (a fragment is not sent), and `query` entries are appended after it
|
||||
in order and may reference declared secrets.
|
||||
- Request steps follow no redirects: a redirect answer fails the step, and
|
||||
nothing is sent to the location it names, though a write the redirect
|
||||
answered may already have been applied. `expected_status` cannot name a 3xx
|
||||
status. Manual redirects can be opaque to the page and may not produce a
|
||||
response event; their status and `Location` can therefore be unavailable even
|
||||
when network events and header values were selected. Such captures report
|
||||
incomplete event coverage.
|
||||
- Header values are literals, declared secrets, a cookie the browser holds for
|
||||
the request URL (`{"source": "cookie", "name"}`, HttpOnly cookies included,
|
||||
whose value the page's scripts can then see while the request runs), or a
|
||||
local storage entry on the start origin (`{"source": "local_storage",
|
||||
"key"}`, with an optional `json_pointer` to a string or number in a stored
|
||||
JSON value). `prefix` is sent before the value. A value read from the page is
|
||||
redacted like a declared secret, as `[REDACTED:page:<step_id>.<header>]`,
|
||||
from records made after the step reads it; a response recorded earlier, such
|
||||
as the login response that returned a token, keeps the value. A value read
|
||||
again keeps its first marker, and a value shorter than 8 characters is not
|
||||
redacted, since redacting it would rewrite ids, ports and hosts throughout
|
||||
the capture. A value is redacted as stored and as `fetch` sends it, without
|
||||
surrounding spaces and tabs. A missing cookie, key or pointer fails the step.
|
||||
Reading a page value raises the estimated cost of redacting every later
|
||||
body, so the bodies kept within `max_network_body_allocation_bytes` (1 MiB
|
||||
by default) get smaller: after a 32-character `X-CSRFToken` value, a body over
|
||||
about 47 KiB is kept as `unavailable`. Raise `capture.limits` when the
|
||||
application's responses are larger.
|
||||
- A header can send Basic credentials, `{"source": "basic_credentials", "parts":
|
||||
[username, password]}`: the base64 of `username:password`, after a `prefix`
|
||||
such as `"Basic "` or with none, as some APIs take encoded credentials in
|
||||
their own header. Each part is a literal, a declared secret, a saved value, a
|
||||
cookie or a local storage entry, so a header a frontend builds from a login
|
||||
token can be sent too; the password is never a literal other than empty, since
|
||||
Evidence records literals as written, and a username with a colon fails the
|
||||
step, since Basic authentication ends the username at its first colon. The
|
||||
encoded value is redacted however short, as
|
||||
`[REDACTED:page:<step_id>.<header>]`, and parts read from the page are
|
||||
redacted as page values are.
|
||||
- Validation refuses a header name given twice in a step, header names a
|
||||
page's `fetch` cannot set, such as `Cookie`, `Origin`, `Host` or `Sec-*`, and
|
||||
`User-Agent`, which Chrome drops; it refuses line breaks and other ASCII
|
||||
control characters in literal values and prefixes, and the step fails before sending
|
||||
when a secret or page value holds them.
|
||||
- The step completes when the whole response has been read. With
|
||||
`expected_status`, any other status fails the step and the later actions are
|
||||
recorded as `cancelled`; without it, any response completes the step. A
|
||||
network error fails the step. `timeout_ms` covers reading the header values
|
||||
and the request: a step whose time runs out, or that is cancelled, before
|
||||
its request is sent sends nothing, but a request already sent can still reach
|
||||
the application after the step fails.
|
||||
An absolute deadline also covers browser evaluation queued behind page work.
|
||||
A temporary route checks cancellation again at network dispatch and is removed
|
||||
when the request evaluation settles. A page controlled by a service worker
|
||||
is refused because its fetch can bypass that dispatch gate; use the default
|
||||
blocked service-worker policy for request steps.
|
||||
|
||||
The request runs in the page's main world, so an application that wraps
|
||||
`fetch` sees it, and it goes through the same network observation as other
|
||||
page requests: select network content (bodies or header values) to record it.
|
||||
Captures with request steps state in `limitations` that the steps sent their
|
||||
declared requests through the page's fetch. A scenario without request steps
|
||||
is unchanged.
|
||||
|
||||
## Saved values
|
||||
|
||||
A step can keep a string or number from a JSON response, by name, and a later
|
||||
step can send it, so a scenario can read back, edit and delete an object whose
|
||||
id the application assigns:
|
||||
|
||||
```json
|
||||
{
|
||||
"browser": {
|
||||
"mode": "launch",
|
||||
"executable_path": "/absolute/path/to/chromium"
|
||||
},
|
||||
"start_url": { "url": "http://127.0.0.1:3000/items" },
|
||||
"actions": [
|
||||
{
|
||||
"step_id": "save-item-1",
|
||||
"action": "click",
|
||||
"timeout_ms": 10000,
|
||||
"locator": { "kind": "role", "role": "button", "name": "Save" },
|
||||
"save_response": {
|
||||
"method": "POST",
|
||||
"path": "/api/items",
|
||||
"save": [{ "name": "item_1", "json_pointer": "/id" }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": "open-item-1",
|
||||
"action": "goto",
|
||||
"timeout_ms": 10000,
|
||||
"destination": { "url": "http://127.0.0.1:3000/items/{saved:item_1}" },
|
||||
"wait_until": "load"
|
||||
},
|
||||
{
|
||||
"step_id": "copy-item-1",
|
||||
"action": "request",
|
||||
"timeout_ms": 10000,
|
||||
"method": "POST",
|
||||
"path": "/api/items/{saved:item_1}/copies",
|
||||
"json": {
|
||||
"source_id": "{saved:item_1}",
|
||||
"title": "Copy of {saved:item_1}"
|
||||
},
|
||||
"expected_status": [201]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- A `request` step saves with `save: [{name, json_pointer, secret}]`, from its
|
||||
own response. A `click` or `press` step saves with `save_response: {method,
|
||||
path, save}`, from a newly observed matching response while the step waits.
|
||||
The recorder watches before the action and excludes requests already running,
|
||||
such as an earlier autosave. Method/path matching does not prove that the UI
|
||||
caused the request: use a path that distinguishes the intended operation from
|
||||
background polling. Multiple observed matches before binding fail as ambiguous;
|
||||
requests starting after this observation window cannot be ruled out. It matches the method and a path on the
|
||||
start origin as the browser spells it, so a path may name `My List` or
|
||||
`My%20List` (`{*}` matches one segment, and the query is ignored), and fails
|
||||
the step unless a 2xx JSON response arrives before its `timeout_ms`; one still
|
||||
being read when the step times out or is cancelled saves nothing. A pointer
|
||||
that names no string or number fails the step, so later steps never send a
|
||||
missing value, as does a response over 4,000,000 characters: a request step
|
||||
stops reading and aborts its fetch there. A UI step reads a body only when
|
||||
its decoded size is bounded at 12,000,000 bytes or less: its received size,
|
||||
or the declared Content-Length of one served from cache, times 1032 for each
|
||||
gzip or deflate coding, the most DEFLATE expands. Other codings and unknown
|
||||
or zero sizes are refused before materializing the body; decoded text over
|
||||
the character limit also fails. The UI action may already have applied.
|
||||
- `{saved:name}` sends a saved value in a request step's `path`, including its
|
||||
query, and in a `goto` destination's `url`, percent-encoded; a destination's
|
||||
structured `query` values are sent as declared. A saved value that makes a
|
||||
path segment `.` or `..`, alone or with the text beside it (including `%2e`),
|
||||
fails the step, since the URL would drop it. In a request step's `json` body,
|
||||
a string that is only `{saved:name}` becomes the saved value with the JSON
|
||||
type it was saved with, so a saved number id is sent as a number, and
|
||||
`{saved:name}` inside a longer string is filled as text. A header value or
|
||||
Basic credentials part sends one as `{"source": "saved", "name"}`. Other
|
||||
braces, such as `{id}`, are sent as written, and so is a percent-encoded
|
||||
`%7Bsaved:name%7D` anywhere but a `goto` URL, whose parsed form spells the
|
||||
braces that way.
|
||||
- Validation refuses a saved value that no earlier step saves. A later save of
|
||||
the same name replaces the value. An integer past 2^53, which JSON parsing
|
||||
would round to another number, and a number that is not finite, such as
|
||||
`1e400`, negative zero, or a value that underflows to zero, such as `1e-400`, fail the step
|
||||
that saves it.
|
||||
- A scenario keeps at most 4,000,000 characters of saved values at once,
|
||||
counting distinct retained redaction spellings, including trimmed variants,
|
||||
of values saved as secret. Replacing a binding releases its live value
|
||||
charge, while secret spellings remain protected for the scenario lifetime.
|
||||
A save past the budget fails before changing the binding.
|
||||
- A value saved with `secret: true` is redacted however short, as a declared
|
||||
secret is, as `[REDACTED:page:<step_id>.<name>]`. The selected source
|
||||
response is protected before retaining its body or header values, including
|
||||
when only headers were requested. If that body cannot be read safely, its
|
||||
body and header values remain unavailable. Before publishing the capture,
|
||||
the selected source transaction's earlier URL, request headers and request
|
||||
body are also protected using the admitted values. Unresolved source values
|
||||
withhold that transaction's affected metadata/content with an explicit
|
||||
limitation. Before publication, retained events are rechecked
|
||||
with the final marked-response values, including immediate follow-up requests
|
||||
and their response bodies, console/page errors, WebSocket text and event URL/name fields. Learned redaction facts are independent of source-identity readiness. Unknown source values withhold only the selected
|
||||
source transaction. Other saved values, such as ids, stay visible, since
|
||||
environments model them. The environment build warns about saved values
|
||||
named like credentials that are not marked secret
|
||||
(`credential-named-saved-values`).
|
||||
- Page-derived cookie/local-storage values and constructed Basic credentials
|
||||
remain redacted only from observations made after they were read/built.
|
||||
They do not retroactively rewrite earlier facts. The final event projection
|
||||
borrows only declared and response-marked patterns; promotion of an already
|
||||
held page value to an explicit response secret preserves its first marker.
|
||||
Projection state is session-local, references admitted pattern/byte owners,
|
||||
and is invalidated on admission or response classification changes.
|
||||
- Screenshots are withheld as `missing` from the start when the scenario plans
|
||||
any response save marked `secret: true`, page-derived or Basic authentication
|
||||
header values, or whenever nonempty sensitive values
|
||||
are already known. Raw pixels cannot be safely text-redacted, so the provider
|
||||
does not request pixels in those cases. This also protects earlier steps and
|
||||
partial failures without retaining an image to scrub later. Other requested
|
||||
artifacts remain available; affected screenshot/aggregate completeness is
|
||||
partial and is not eligible for equality.
|
||||
- Saved URLs and expanded request JSON are admitted before building their expanded strings or
|
||||
serializing. Its allowance is the smaller of Node's string limit and
|
||||
one sixteenth of current V8 available heap bytes, allowing for the expanded
|
||||
tree, serialization, Playwright transport and browser-side copies. Size
|
||||
accounting includes JSON escaping and repeated references; each distinct
|
||||
saved value's size is measured once per request. An oversized request fails
|
||||
before browser dispatch; the complete request envelope, including repeated
|
||||
header values, is checked as well. URL encoding uses a conservative
|
||||
nine-characters-per-UTF-16-unit allowance before encoding a saved value.
|
||||
Reduce the payload/repetitions, or raise the parent
|
||||
Node heap and restart; this is a conservative allocation estimate, not a
|
||||
measurement or guarantee of browser/process-family memory.
|
||||
- Evidence records a request step's empty `save` list as absent, so a scenario
|
||||
that saves nothing records the same parameters and `scenario_sha256` as
|
||||
before saved values existed, and REA versions without saved values read its
|
||||
captures.
|
||||
|
||||
## Network evidence
|
||||
|
||||
`capture.events: ["network"]` retains request, response, completion, failure,
|
||||
@@ -646,18 +302,6 @@ and unfinished-request metadata. Select content independently:
|
||||
All three options default to `false`. Any selected network content also enables
|
||||
network metadata observation; listing `network` in `events` is then optional.
|
||||
Each observed request object receives a capture-scoped `transaction_id`.
|
||||
Each `request` event carries `observed_at`, the wall-clock time REA observed the
|
||||
request start. Playwright reports the browser's own start only once a request
|
||||
ends, so each `request-finished` or `request-failed` event carries `started_at`,
|
||||
the browser's wall-clock start, and `elapsed_ms`, its time from that start to
|
||||
the end of the response, when the browser reports them. The environment views
|
||||
time scenario records from these, as HAR entries are timed: the browser's start
|
||||
where it reported one, else `observed_at`, which trails it by up to tens of
|
||||
milliseconds while REA reads other responses. A duration is retained only from a
|
||||
unique terminal event that reports it; missing or ambiguous durations stay
|
||||
omitted, while an explicitly reported zero remains zero. Captures made before these fields existed lack them and
|
||||
are read in record order. REA versions before them refuse captures that carry
|
||||
them. Scenario comparisons ignore all three (`ignore_network_request_timing`).
|
||||
Requests sharing a URL remain distinct. Redirects link to the preceding
|
||||
transaction only when it was observed; a null predecessor does not establish
|
||||
that no earlier hop existed. Request events require both transaction fields;
|
||||
@@ -691,16 +335,8 @@ Proxy-Authorization, Cookie, and Set-Cookie values are null with
|
||||
and their standard URI/form or JSON-escaped representations are replaced without
|
||||
decoding unrelated binary bytes. Other encodings are not interpreted. Digests describe the
|
||||
retained representation after redaction.
|
||||
The selected body-byte limit also bounds that retained representation after
|
||||
secret expansion, before base64 serialization. Credential header values are
|
||||
discarded before retained-header allocation accounting; their names and null
|
||||
projections still count. The transport/API has already materialized incoming
|
||||
bodies and headers, so these retained limits do not bound producer-side input
|
||||
allocation.
|
||||
|
||||
Only completed responses are read. The producer-observed terminal state is
|
||||
recorded independently of whether its event metadata fit the retention budget. An omitted completion event cannot turn a
|
||||
finished transaction into an unfinished one. When the final step ends, observation
|
||||
Only completed responses are read. At the end of the final step, observation
|
||||
stops; requests still in flight emit `request-unfinished`, and their observed
|
||||
responses have explicitly unavailable bodies. Already-admitted content reads
|
||||
drain before browser cleanup. At most 16 reads run at once; queued work is
|
||||
@@ -720,24 +356,6 @@ observed) and excludes the sequence reference from normalization.
|
||||
Playwright scenario capture does not supply initiator stacks; passive CDP
|
||||
observation retains its own initiator evidence.
|
||||
|
||||
## Storage value fingerprints
|
||||
|
||||
New storage snapshots include `fingerprint_version: 2`. Non-secret, well-formed
|
||||
JavaScript strings retain their SHA-256 over UTF-8. Strings with unpaired UTF-16
|
||||
surrogates use SHA-256 over a raw `0xff` byte followed by their exact UTF-16LE
|
||||
code units; this prefix cannot occur in UTF-8. Declared secrets remain
|
||||
`redacted-secret` with a null digest, without a second or legacy digest.
|
||||
|
||||
A missing version identifies legacy UTF-8 fingerprints, which replaced unpaired
|
||||
surrogates and cannot establish value identity. Storage containing any legacy
|
||||
hashed value is conservatively compared as `unknown`, including old-to-old
|
||||
comparisons; recapture both sides to compare reliably. This also conservatively
|
||||
withholds some otherwise provable legacy differences. Empty or entirely
|
||||
secret-redacted storage keeps its existing comparison semantics. The version is
|
||||
checked before normalization and excluded from content comparison: changing
|
||||
encoding metadata alone does not establish a website change. Existing captures
|
||||
and their completeness records are not rewritten.
|
||||
|
||||
## Scenario comparison
|
||||
|
||||
`compare_web_captures` and `compare-web-captures` also accept two complete
|
||||
@@ -755,8 +373,8 @@ and compared as an unordered collection after caller rules apply, because
|
||||
concurrent responses arrive in any order and can land in a later step; a
|
||||
redirect retains the complete predecessor ancestry, including each hop's own
|
||||
request, response outcome, and selected network content. Each transaction's
|
||||
capture-local IDs, sequence/index fields, content source-sequence anchor and
|
||||
request times (`observed_at`, `started_at`, `elapsed_ms`) are removed. Caller rules normalize retained strings before hashing. The
|
||||
capture-local IDs, sequence/index fields, and content source-sequence anchor
|
||||
are removed. Caller rules normalize retained strings before hashing. The
|
||||
`preserve_redirect_transaction_ancestry` built-in rule fingerprints each hop
|
||||
as canonical SHA-256 of `{ events, redirected_from }`: `events` is the sorted
|
||||
list of its normalized event SHA-256 digests, and `redirected_from` is its
|
||||
@@ -770,18 +388,6 @@ sorted canonically before application, making the same policy reproducible
|
||||
regardless of input order. Rules operate only on already-redacted durable
|
||||
capture fields and are preserved in the result.
|
||||
|
||||
Cookies, local storage, and session storage are compared as unordered inventories
|
||||
after caller literal rules apply. Enumeration order alone does not establish a
|
||||
storage change. Canonical ordering uses the complete entry, preserving cookie
|
||||
scope/flags, value fingerprints, and duplicate entries even when redaction or
|
||||
literal rules make names collide. Captured storage arrays retain their original
|
||||
order. The `compare_storage_entries_as_unordered_collection` built-in rule
|
||||
participates in the normalization commitment; new storage comparison digests
|
||||
therefore differ from earlier order-sensitive comparisons. The
|
||||
`ignore_network_request_timing` built-in rule likewise joins the commitment, so
|
||||
normalization digests differ from comparisons made before it, though captures
|
||||
with and without request times compare alike.
|
||||
|
||||
Each aligned step reports changed, unchanged, or unknown. Changed and unknown
|
||||
action, screenshot, DOM, accessibility, URL, history, storage, and event
|
||||
artifacts carry their normalized before/after digests and capture states.
|
||||
@@ -794,67 +400,7 @@ selection. Captures with different selections report event evidence as unknown
|
||||
with a recovery instruction, even when no selected family emitted an event.
|
||||
Captures written before this field existed omit it, so their event evidence is
|
||||
also unknown.
|
||||
Retained-capture readers admit that omission without deriving a selection from
|
||||
observed events; other artifact families remain comparable. Current capture
|
||||
producers still require and publish the selected event families. Manifest,
|
||||
environment and script-export readers share the retained-capture admission
|
||||
rules and preserve the same missing fact.
|
||||
|
||||
Network normalization also commits the removal of capture-local transaction IDs,
|
||||
attribution to the requesting step, and unordered transaction comparison. These
|
||||
built-in rule identities participate in the normalization digest.
|
||||
|
||||
## Summary delivery and retained views
|
||||
|
||||
`capture_browser_scenario` accepts `detail: "summary"` to retain the complete
|
||||
capture in the MCP session and return its summary. The default remains
|
||||
`complete`. Delivery detail does not change scenario parameters or identity.
|
||||
|
||||
The summary reports each step's status, error, URL and captured/missing/truncated
|
||||
section names. It omits `not_requested` sections and repeated `before_url` values.
|
||||
The accessibility tree comes from the first unsuccessful step, or the final
|
||||
step on success; when that step captured no tree, its nearest earlier captured
|
||||
tree is identified explicitly. Event and network counts cover retained records,
|
||||
with capture omissions reported separately.
|
||||
|
||||
For the one-shot CLI, save the complete Evidence before selecting a summary:
|
||||
|
||||
```bash
|
||||
rea capture-browser-scenario scenario.json --detail summary --evidence_output /absolute/path/scenario-evidence.json --json
|
||||
```
|
||||
|
||||
Use the returned `normalized_result.parent_evidence_id` with
|
||||
`inspect_analysis_view` source `{kind: "retained-evidence", evidence_id: "..."}`.
|
||||
For a CLI bundle, use an `evidence-file` source with its absolute path and the
|
||||
parent Evidence ID. Views read the recorded capture; they never replay actions.
|
||||
|
||||
- `{kind: "summary"}` returns the same overview.
|
||||
- `{kind: "browser-scenario", collection: "steps", step_indices: [2], artifacts: ["accessibility"], offset: 0, limit: 1}` returns selected recorded steps and artifacts.
|
||||
- `{kind: "browser-scenario", collection: "events", event_kinds: ["response"], transaction_ids: ["request-2"], offset: 0, limit: 20}` pages selected events with original item indices.
|
||||
|
||||
Omitted filters select all; empty arrays select none. Pagination counts matching
|
||||
retained rows. Exhausting a page does not establish capture completeness or
|
||||
current application state.
|
||||
|
||||
Transaction pages use `{kind: "browser-scenario", collection: "transactions", limit: 20}`.
|
||||
Their stable identity is the parent Evidence ID plus `transaction_id`; each row
|
||||
keeps original event indices. Metadata events are returned by default. Select
|
||||
`event_kinds: ["network-content"]` for recorded bodies and header values, or
|
||||
`event_kinds: []` for identity and indices alone. A `step_indices` filter selects
|
||||
a whole transaction when any retained event belongs to a selected step; this
|
||||
is receipt attribution, not proof that the action caused the request.
|
||||
|
||||
For a large retained tree, request `{kind: "browser-scenario", collection: "text",
|
||||
step_index: 10, artifact: "accessibility", offset: 0, limit: 2048}`. `dom` is also
|
||||
supported. Offsets and limits count UTF-16 code units. The full artifact digest
|
||||
and byte size remain attached to each page. Missing, truncated and unrequested
|
||||
text stay explicit; source truncation counts are preserved without treating an
|
||||
exhausted page as a complete capture.
|
||||
|
||||
Network summary counts include every retained transaction identity, including
|
||||
those whose request event was dropped. Missing request events, conflicting
|
||||
metadata and observed request failures have separate counts. `transactions` is
|
||||
`null` when network observation was unrequested or its legacy selection is
|
||||
unknown and there are no retained transactions; an observed empty inventory is
|
||||
`0`. Full capture omissions remain available and each page carries source
|
||||
completeness, dropped-event counts and omitted-transaction accounting.
|
||||
@@ -1,62 +0,0 @@
|
||||
# Bun standalone executables
|
||||
|
||||
`inspect-artifact` inventories embedded Bun module graphs in Mach-O `__BUN,__bun`,
|
||||
PE `.bun`, ELF `.bun`, and appended trailers. `extract-artifact` writes their stored
|
||||
module contents through the existing safe output tree. Neither operation executes
|
||||
the executable or decompiles bytecode.
|
||||
|
||||
```sh
|
||||
rea inspect-artifact ./hello --json
|
||||
rea extract-artifact ./hello --json
|
||||
```
|
||||
|
||||
In MCP, open the executable with `open_binary`, then call `inspect_artifact`.
|
||||
Use `detail: "summary"` for large graphs and `inspect_analysis_view` to page retained
|
||||
Evidence. The root metadata's `entry_point_logical_path` is the reader-owned
|
||||
location of its entry module. Use it as an inventory `logical_paths` filter,
|
||||
then read selected node fields with the returned content identity. This also
|
||||
works for names mapped to indexed extraction paths. The complete Evidence stays
|
||||
retained; no source-name normalization is needed in the caller.
|
||||
|
||||
The root reports the carrier, layout, module count, entry point, raw flags and
|
||||
compile exec-argv. Each module retains its source name, loader and encoding values,
|
||||
entry-point status, sizes and SHA-256. Sourcemaps, bytecode and module-info regions
|
||||
are separate children. Source names such as `/$bunfs/root/main.js` remain in
|
||||
metadata; extraction uses a relative path. Unsafe or non-UTF-8 names receive an
|
||||
indexed path and retain their source spelling or bytes. Virtual names deeper
|
||||
than 256 logical path parts also use an indexed path with an explicit limitation,
|
||||
preventing unbounded path-tree work while retaining the source fact. Duplicate names remain
|
||||
separate occurrences; ordinary extraction collision rules apply.
|
||||
|
||||
The recognized record shapes are the 36-byte Bun 1.3.3 layout and the 52-byte
|
||||
Bun 1.3.9 layout. These labels identify structural layouts, not a detected compiler
|
||||
version. Bun does not store an explicit revision identifier: unknown table sizes
|
||||
and ambiguous known layouts are unsupported. A future layout that is byte-for-byte
|
||||
compatible cannot be distinguished. Invalid pointers and truncated claimed sections
|
||||
are malformed. An absent trailer without a named section does not establish a Bun
|
||||
graph. Unknown enum values remain raw, with a null name.
|
||||
|
||||
Sourcemaps are stored Bun regions, not necessarily standalone JSON source maps.
|
||||
Extracted contents retain their original encoding. Use JavaScript analysis only on
|
||||
compatible source files; bytecode is retained as evidence, not decoded.
|
||||
|
||||
Module parsing reads exact metadata ranges rather than loading stored asset
|
||||
bodies or whole pages around small pointers. A cache retains at most 256 KiB
|
||||
and 1,024 ranges for one reader preparation. Cumulative uncached metadata reads
|
||||
are limited to 256 MiB, checked before allocation or I/O. The module table,
|
||||
decoded strings and combined logical path tree have separate admission checks;
|
||||
at most 250,000 distinct path prefixes are admitted before inventory construction.
|
||||
Stored bodies are range-streamed for hashing and extraction. Header/suffix observations are reused
|
||||
only for the same admitted file state, with selected-path checks before publication.
|
||||
|
||||
See [limits and recovery](design/limits-and-recovery.md) for admission ceilings.
|
||||
Synthetic tests cover the carriers and malformed declarations. Run
|
||||
`node scripts/verify-bun-standalone.mjs` after `npm run build:cached` for local Bun
|
||||
verification, and add `--docker` to compile a Linux fixture with `oven/bun:1.4.0`.
|
||||
Bun and Docker are fixture producers only and are not installed by REA.
|
||||
|
||||
Layout references: [Bun 1.3.3 serializer](https://github.com/oven-sh/bun/blob/bun-v1.3.3/src/StandaloneModuleGraph.zig)
|
||||
and [Bun 1.3.9 serializer](https://github.com/oven-sh/bun/blob/bun-v1.3.9/src/StandaloneModuleGraph.zig).
|
||||
|
||||
The current 52-byte record order can also be checked in the
|
||||
[Bun 1.4.4 Rust serializer](https://github.com/oven-sh/bun/blob/bun-v1.4.4/src/standalone_graph/StandaloneModuleGraph.rs).
|
||||
@@ -1,107 +0,0 @@
|
||||
# Capability composition and ownership
|
||||
|
||||
REA keeps domain contracts and shared workflows separate from provider adapters
|
||||
and CLI/MCP translation. A capability can have several provider identities; a
|
||||
provider can contribute operations to several capabilities. Those associations
|
||||
are source facts, while host availability is an observation.
|
||||
|
||||
## Source declarations and runtime bindings
|
||||
|
||||
`src/composition/capabilityDeclarations.ts` contains static provider identities,
|
||||
named contract-group references and precise operation subsets. Complete groups
|
||||
refer to `TOOL_CONTRACT_GROUPS` in the existing contract owner; family definitions
|
||||
and provider catalogs read the same arrays, so a new group operation is included
|
||||
without updating a copied provider list. Metadata imports only the group ID type
|
||||
and pure metadata leaves; it must not load factories, schemas, engines or
|
||||
generated catalogs. Catalog generation resolves groups and subsets against the
|
||||
canonical contracts and rejects unknown groups/operations and conflicting
|
||||
identities. New provider provenance must come from the implementation
|
||||
that actually produces the Evidence, including format-specific capture adapters
|
||||
and derived workflows.
|
||||
|
||||
`src/composition/capabilityServices.ts` binds typed service factories to those
|
||||
capability keys. Each runtime entry owns construction, cleanup and any available
|
||||
host/configuration facts or live probes. A service entry requires a close
|
||||
capability; entries adapting managers retain their existing `closeAll` operation.
|
||||
The host maps entries without erasing their individual service/port types. There
|
||||
is no generic JSON operation dispatcher.
|
||||
|
||||
These two declarations have different loading needs. Static facts can be read
|
||||
without loading a schema or provider graph. The full runtime composition is
|
||||
materialized only when the MCP runtime needs it. One-shot CLI adapters import the
|
||||
selected factory rather than constructing a complete capability host. CLI and MCP
|
||||
reuse the same provider/service factories and application workflows, while
|
||||
keeping their respective argument translation and lifetime.
|
||||
|
||||
## Resource lifetime
|
||||
|
||||
`CapabilityScope` registers a module's actual close capability when its instance
|
||||
is composed. There is no independent server provider-close list to update.
|
||||
Concurrent shutdown calls join the same cleanup attempt. A successful peer is
|
||||
removed from pending cleanup; failed peers remain owned and are retried on the
|
||||
next close. Synchronous throws and rejections without a reason remain failures,
|
||||
and cannot prevent other peers from attempting cleanup. Shutdown seals further
|
||||
ownership registration.
|
||||
|
||||
The host distinguishes these lifetimes:
|
||||
|
||||
- Host-owned service instances are released by that host.
|
||||
- Caller-owned optional observations remain borrowed by SDK server instances;
|
||||
their process-level owner closes them, including discarded discovery probes.
|
||||
- CLI invocations use their existing finalizers to join cleanup and preserve the
|
||||
original observation, cancellation and cleanup errors.
|
||||
- Binary engines, bundle readers and investigation records retain their existing
|
||||
owners. The `BinarySession` transition/drain barrier still coordinates target
|
||||
changes, reset and disposal. A module refactor must not create independent
|
||||
barriers that allow late work to repopulate reset records.
|
||||
|
||||
An application workflow may borrow a shared resource scope, such as byte entropy
|
||||
using the host's artifact scope. Its wrapper does not acquire a second ownership
|
||||
claim over the borrowed scope.
|
||||
|
||||
## Availability and registration
|
||||
|
||||
Module bindings provide host support, selected configuration and precisely typed
|
||||
live availability probes through `CapabilityAvailabilityFacts`. Constructors do
|
||||
not run these probes or launch engines. Session discovery invokes the existing
|
||||
provider-owned probes only for advertised families; live failures remain fresh
|
||||
observations. Caller policy, selected environments and injected providers retain
|
||||
their existing precedence.
|
||||
|
||||
MCP registration remains in the MCP adapter. It consumes composed services,
|
||||
record ports, admission and logging; it must not add fallback constructors that
|
||||
can escape the host owner. Named contracts bind handlers and schemas. The complete
|
||||
tool-family registrar map is checked by TypeScript, and catalog order never
|
||||
selects a handler or schema.
|
||||
|
||||
## Extending a capability
|
||||
|
||||
1. Add or reuse the domain contract and application workflow, with explicit
|
||||
Evidence provenance, coverage, unknowns and caller choices.
|
||||
2. Put concrete construction in `src/composition/` and keep its default factory
|
||||
lazy. Reference adapter-owned pure identity metadata from both the provider
|
||||
and its static capability declaration.
|
||||
3. Add the runtime binding if a host-owned service is needed. Bind cleanup and
|
||||
availability/probes there, preserving borrowed versus owned resources.
|
||||
4. Add the CLI/MCP argument adapters and their named registrations. A one-shot
|
||||
command uses the shared selected factory and existing finalization helpers.
|
||||
5. Run source-boundary, discovery, SDK-schema and relevant public workflow checks.
|
||||
Exercise partial cleanup, retry, cancellation and caller-owned peers at the
|
||||
applicable production seam; use actual applications/providers for routing and
|
||||
complete Evidence verification. Recording ports do not prove real providers.
|
||||
|
||||
The constructor boundary guard covers the centralized Frida, Cutter, GDB and
|
||||
Rizin constructors without rejecting legitimate type-only injected interfaces.
|
||||
Other settled pure/provider boundaries continue checking type imports too. The
|
||||
shared parser records module-edge erasure once. Whole-declaration type imports
|
||||
are erased; inline type bindings retain their runtime module edge under
|
||||
`verbatimModuleSyntax`. Static dynamic imports remain runtime edges too.
|
||||
Tests belong with their semantic owner and must be
|
||||
found exactly once; provider roots need the corresponding adapter discovery lane.
|
||||
|
||||
This is an internal composition contract. External plugin distribution, automatic
|
||||
provider installation and a universal provider interface are separate concerns.
|
||||
|
||||
The [initial verification report](verification/capability-composition-2026-11-01.md)
|
||||
records the executed public workflows, cleanup boundaries, resource measurements
|
||||
and remaining provider qualification gaps for this composition increment.
|
||||
Loaded 100 of 6225 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user