Compare commits

...
Author SHA1 Message Date
Cursor Agent 2f2110a8ba feat(clients): register Grok Build and Grok Bot during setup
Detect both clients and install the local REA MCP registration the same
way setup already handles the other agents. Grok Build edits only the
REA tables in its TOML config, sets startup_timeout_sec to 30, and
removes rea from disabled_mcp_servers. Grok Bot writes mcpServers.rea
into mcp.json. Setup does not call the account API.

Uninstall keeps comments in an inline table whose only server was REA.
2026-10-08 21:33:27 +00:00
morluto 4458128a84 fix(ghidra): make qualified function renames idempotent 2026-10-09 05:27:39 +08:00
morluto 34f7c4fd66 fix(docs): validate translated links to canonical product facts 2026-10-09 05:15:23 +08:00
morluto 67252b23b9 perf(javascript): index configuration and module lookups 2026-10-09 05:15:23 +08:00
morluto 02d0702761 fix(electron): bound hook retention and report dropped coverage 2026-10-09 05:15:23 +08:00
morluto e714fb027a fix(inspector): bound retained metadata and preserve cancellation 2026-10-09 05:15:23 +08:00
morluto f983c718c3 fix(browser): bound aggregate source-map decoding before expansion 2026-10-09 05:15:23 +08:00
morluto 3d450a29fc fix(application): budget bridge candidate projection by bytes 2026-10-09 05:15:23 +08:00
morluto 73b09f4c5c fix(managed): bind inspection evidence to artifact identity 2026-10-09 05:15:23 +08:00
morluto dbcf8c3391 fix(apple): budget Interface Builder decoding and projection 2026-10-09 05:15:23 +08:00
morluto f2e9c491ea fix(native): share command supervision and retain failure output 2026-10-09 05:15:23 +08:00
morluto 4a45c14742 fix(firmware): reject malformed UTF-8 producer reports 2026-10-09 05:15:23 +08:00
morluto 2b86ea9c7c docs: require partial native facts at provider boundaries 2026-10-09 05:12:36 +08:00
morluto f201b04fbb fix(hopper)!: isolate regex matching from the native API thread
BREAKING CHANGE: Hopper regex mode uses ECMAScript Unicode syntax instead of Python re. Matching runs in a cancellable worker with a five-second deadline; literal mode keeps Unicode casefold matching.
2026-10-09 05:12:36 +08:00
morluto 246170238e fix(hopper): preserve partial native call and string evidence 2026-10-09 05:12:36 +08:00
morluto 0a49d5bfb9 fix(hopper): load verified FAT64 slices with native Mach-O semantics 2026-10-09 05:12:36 +08:00
morluto 458771cb58 fix(hopper): preserve launcher evidence on readiness timeout 2026-10-09 05:12:36 +08:00
morluto 7d7b3f961a fix(hopper): preserve typed string values and verify real searches 2026-10-09 05:12:36 +08:00
morluto 35f1ddd6bc fix(hopper): resolve FAT loaders and original file coordinates 2026-10-09 05:12:36 +08:00
morluto b2d5bc1109 fix(hopper): validate annotation destinations and symbol extents 2026-10-09 05:12:36 +08:00
morluto 704fa7699c fix(ghidra): preserve source failures and snapshot ownership 2026-10-09 05:10:36 +08:00
morluto 2f59287f1a feat(website): add GitHub link with live star count 2026-10-09 04:55:51 +08:00
morluto d83f0983d5 docs: align translated READMEs and website links with English 2026-10-09 04:55:43 +08:00
morluto 1136ea8810 test(session): preserve live binding after cancelled switches 2026-10-09 04:43:19 +08:00
morluto e44cb45908 fix(session): serialize snapshot lifecycle with admitted requests 2026-10-09 04:37:43 +08:00
morluto 8d39eae6b9 chore(website): configure Cloudflare hosting for rea.tools 2026-10-09 04:29:57 +08:00
morluto 09d7b9bab5 test(process): cover argv rewrites in native ownership inspection 2026-10-09 04:12:49 +08:00
morluto 9a70fb482c test(process): distinguish native startup from producer verification 2026-10-09 04:12:49 +08:00
morluto f591e37a1e test(artifacts): retire simulated mounts after detach failures 2026-10-09 04:12:49 +08:00
morluto a0784c0d2c test(process): retire native caches at worker teardown 2026-10-09 04:12:49 +08:00
morluto 96d3cafb07 fix(browser): bind correlated replies to their selected session 2026-10-09 04:12:49 +08:00
morluto ee043e2cbf fix(verify): report final success only after resource cleanup 2026-10-09 04:12:49 +08:00
morluto 3eb7dbdc16 fix(verify): isolate package archives and published canary caches 2026-10-09 04:12:49 +08:00
morluto 90b06db6c0 test(process): expose unavailable capture and unexpected teardown failures 2026-10-09 04:12:49 +08:00
morluto 6b92002597 fix(browser): preserve command failures and reject malformed replies 2026-10-09 04:12:48 +08:00
morluto bc4aa46d7c fix(ghidra): recover regex stack exhaustion without losing annotations 2026-10-09 03:54:11 +08:00
morluto 549effa056 fix(ghidra): resolve exact external function entries 2026-10-09 03:54:02 +08:00
morluto dcae3c6c14 fix(ghidra): reject address truncation before reads and edits 2026-10-09 03:53:28 +08:00
morluto 84a17d5519 fix(ghidra): preserve function references and snapshot boundaries
Collect references from the complete owned function body and exact entry,
including instruction-interior destinations and embedded-data sources.
Bind the corrected semantics to the analysis profile so legacy snapshots
cannot replay incomplete results. Preserve snapshot mismatch reasons and
recovery advice through CLI and MCP without disturbing live sessions.

Return exact entry candidates for ambiguous function names. Expand the
real-provider regression workflow and replace redundant parser assertions
with malformed-output coverage through the pinned MCP client.

Verified with Ghidra 12.1.4 on macOS arm64: full host-native lane, real
CLI/MCP boundary probes, 30 focused tests, and typecheck/lint.
2026-10-09 03:19:47 +08:00
morluto 017297c40c test(browser): reject unmodeled CDP commands in protocol fixtures 2026-10-09 03:13:53 +08:00
morluto ffd9611308 test: consolidate failure guards and trim unused fixture APIs 2026-10-09 03:13:43 +08:00
morluto 81ae838567 refactor(config): prune unused format wrappers and launcher export 2026-10-09 03:13:27 +08:00
YsopwnableandGitHub d1c904bac7 fix(build): exclude the test-only generated catalog from packages (#1116) 2026-10-09 03:02:07 +08:00
morluto 8f9da25164 test(mcp): serialize real process capture and report unavailable authority 2026-10-09 02:58:03 +08:00
morluto fe48e779c2 test(setup): scope removal regression to its fixture client 2026-10-09 02:58:03 +08:00
morluto 9a5b077899 test(process): retain cancellation checks under unverified host cleanup 2026-10-09 02:58:03 +08:00
morluto 9588482bfe docs(testing): prioritize real workflows and distinct regressions 2026-10-09 02:58:03 +08:00
morluto 0e0b386dc3 test: classify application scenarios by their actual boundaries 2026-10-09 02:58:03 +08:00
morluto a1b7d40bb0 test: remove duplicate result assertions before throwing guards 2026-10-09 02:58:03 +08:00
morluto 5945915f91 test: prune redundant cases and promote configuration lifecycle coverage 2026-10-09 02:58:03 +08:00
morluto bef234d6ce fix(ghidra): preserve Unicode boundaries without breaking sessions
Validate native annotation text before mutation and escape surrogate code units in Java bridge JSON replies. Reject unsupported text with field and index diagnostics while preserving supported Unicode and line endings.

Probe real CLI/MCP annotation, transport, mapping, rollback and cleanup behavior; replace seven redundant unit tests with E2E and SDK boundary coverage.
2026-10-09 02:50:33 +08:00
morluto 64717ad7da test(hopper): cover real boundary failures and CLI parity 2026-10-09 02:44:28 +08:00
morluto d830cb79b3 fix(analysis): resolve procedure identities and preserve literal queries 2026-10-09 02:42:39 +08:00
morluto f2e22e13a6 fix(hopper): preserve native navigation and annotation semantics 2026-10-09 02:42:39 +08:00
YsopwnableandGitHub b743bd20a0 fix(hopper): verify lease cleanup and repair the macOS test lane (#1111)
* fix(hopper): distinguish shared lease roots from leaked sessions

* fix(ci): follow the moved process CLI regression tests
2026-10-09 02:41:34 +08:00
YsopwnableandGitHub ec6db0f063 perf(javascript): reuse validated immutable graphs (#1112)
* perf(javascript): reuse validated immutable graphs

* fix(ci): follow the moved process CLI boundary test
2026-10-09 02:40:21 +08:00
morluto 1f5be750bc fix(ghidra): harden location resolution and provider lifecycle
Preserve boundary diagnostics, resolve procedure names before bare hex addresses, and launch the inspected JVM directly on macOS for verifiable startup cancellation. Keep private socket paths within host limits and retain ownership when cleanup is incomplete.

Exercise the fixes through real CLI/MCP Ghidra workflows and provider-boundary integration tests, replacing redundant unit coverage.
2026-10-09 02:38:07 +08:00
morluto e2fc2493b6 fix(process): retain ownership after incomplete cleanup 2026-10-09 02:37:47 +08:00
morluto be783efaba fix(release): always bump minor for releases 2026-10-09 02:15:30 +08:00
morluto 7005166f58 test(hopper): verify real boundaries and prune redundant happy paths 2026-10-09 02:05:58 +08:00
morluto 2f71375efc fix(hopper)!: bind analysis to the active target and correct boundary contracts
Preserve native document identity, nullable bookmark labels, address diagnostics, segment-end byte prefixes, and verified file mappings. Validate batch rename addresses before mutation.

BREAKING CHANGE: remove set_current_document. Use open_binary to switch targets; document arguments are scoped to the active target.
2026-10-09 02:05:31 +08:00
YsopwnableandGitHub 34cc9259c1 fix(process): derive executable names from the capture host (#1108) 2026-10-09 02:02:24 +08:00
Rudy Mizrahi CelekliandGitHub 8b08ec0af9 fix(browser): compose nested source-map column offsets (#1109)
* test(browser): verify native nested source-map coordinates

Qualify valid compiler-produced nested index maps through actual owned Chrome and the built public analyze-web-bundle CLI. Preserve same-line and zero-column controls, physical UTF-16 mapping positions and the independently correct dedicated decoder control. No production or workflow changes.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

* test(browser): retain native source-map failure receipts

Serialize the already-created phase and proof fields for failed native source-map owners. Preserve all producer inputs, scenarios, commands and assertions; first native two-failure/two-control evidence remains unchanged.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

* test(browser): preserve nested source-map boundary contracts

Qualify ordinary maps, nested offsets, sibling clipping, parent gaps, empty sections and deeper composition against the actual browser fetcher. Compare the existing dedicated decoder only for its valid non-overlapping profile. Preserve the four frozen compiler/Chrome/public CLI scenarios.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

* fix(browser): compose nested source-map column offsets

Normalize indexed sections to absolute leaf offsets before upstream flattening. Apply enclosing column offsets only for same-line child sections and retain parent clipping boundaries for gaps and empty sections. Preserve raw map evidence, source identities, transport budgets and the dedicated decoder profile.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

---------

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
2026-10-09 02:01:29 +08:00
morluto 82560ebf59 test(process): serialize real CLI capture boundaries 2026-10-09 01:55:12 +08:00
morluto 8a4c4b657f test(process): report the cause of terminal capture failures 2026-10-09 01:55:12 +08:00
morluto 1d0baa352f fix(native): preserve bridge failure types during cleanup 2026-10-09 01:55:12 +08:00
morluto 7c323870c2 fix(native): bound retained observations and preserve failure evidence 2026-10-09 01:55:12 +08:00
morluto d6cc9ca3a6 fix(javascript): preserve lexical open and exact template semantics 2026-10-09 01:55:12 +08:00
morlutoandGitHub fe297e56e8 fix(apple): classify empty dyld settings by consumer semantics (#1110) 2026-10-09 01:54:32 +08:00
YsopwnableandGitHub 1e63144390 ci: run signature target binding regressions on native macOS (#1107) 2026-10-09 01:53:40 +08:00
YsopwnableandGitHub cd756b07d5 fix(hopper): report launcher failures before bridge timeout (#1105)
* fix(hopper): report launcher failures before bridge timeout

* fix(hopper): separate helper ownership from GUI lifetime
2026-10-09 01:51:45 +08:00
morlutoandGitHub 7914e36ab9 fix(apple): validate dylib candidates and preserve search uncertainty (#1100)
* fix(apple): validate dylib candidates and preserve search uncertainty

* fix(apple): scope embedded dyld uncertainty by search phase

* fix(apple): scope dyld prefixes to eligible paths and platforms

* fix(apple): ignore embedded process settings on library roots
2026-10-09 01:43:52 +08:00
Rudy Mizrahi CelekliandGitHub 3b2ec69c54 fix(apple): report missing keyed archive hierarchy references (#1104)
* test(apple): qualify malformed native hierarchy references

Freeze actual Foundation/AppKit producer, single-slot positive UID mutation and real optional nil control at filesystem and compiled CLI boundaries. Preserve original raw-byte evidence and existing CI.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

* fix(apple): report missing keyed archive hierarchy references

Preserve known hierarchy objects and links while reporting unresolved nonnil object-table references as partial coverage. Retain archived nil semantics and original byte evidence.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

---------

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
2026-10-09 01:31:53 +08:00
YsopwnableandGitHub 4bb056bbb5 fix(artifacts): retain failed DMG cleanup state and diagnostics (#1103) 2026-10-09 01:28:42 +08:00
morlutoandGitHub c789af04b7 fix(native): bind signature observations to registered target versions (#1099)
* fix(native): bind signature observations to registered target versions

* fix(native): preserve target drift and cancellation across signature captures

* fix(native): inspect signatures from a verified byte snapshot
2026-10-09 01:27:26 +08:00
morluto acb3e0f2e5 fix(release): restore automatic release proposals on main 2026-10-09 01:25:36 +08:00
Rudy Mizrahi CelekliandGitHub 3db46813f8 fix(javascript): parse plain TypeScript artifact dialects (#1102)
* test(javascript): reproduce TypeScript source dialect loss

Add native directory/ASAR and built public CLI owners with actual locked native TypeScript syntax/emit and independent Babel source-range oracles. Production and every workflow remain unchanged.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

* test(javascript): admit isolated native compiler inputs

Use the actual TypeScript7 producer-requested --ignoreConfig flag when supplying isolated source filenames. Preserve every graph input, owning assertion, control and production file.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

* fix(javascript): parse plain TypeScript artifacts without JSX

Select the plain .ts dialect using the artifact source path so valid angle-bracket assertions retain syntax-derived graph facts. Preserve existing JSX defaults and all other dialects. Document the inert artifact parser contract.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

---------

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
2026-10-09 01:24:47 +08:00
YsopwnableandGitHub cc0ee2edc6 fix(javascript): honor cancellation before evidence publication (#1098) 2026-10-09 01:15:50 +08:00
f8e143a3bc fix(apple): decode Interface Builder XML byte encodings (#1095)
* test(apple): qualify native Interface Builder XML encodings

Exercise actual AppKit/Foundation XML archive representations through filesystem and compiled CLI consumers. Preserve independent UID/hierarchy and original-byte provenance oracles.

Tests-only candidate qualification on unchanged native Apple CI; no production or workflow changes.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

* fix(apple): share BOM-aware Interface Builder XML decoding

Reuse the existing fatal BOM-aware XML plist decoder for Interface Builder archives. Preserve original serialized-byte digests, binary decoding, and frozen native producer/consumer tests.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

* fix(apple): reject unsupported XML encoding declarations

---------

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-09 01:11:49 +08:00
Kaoru0822andGitHub ae0630a619 Merge pull request #1097 from morluto/fix/web-runtime-script-initiators
fix(browser): treat module-import initiator positions as script positions
2026-10-08 10:00:21 -07:00
YsopwnableandGitHub 81b5104276 fix(analysis): bind overview document identity to the active target (#1094)
* fix(analysis): bind overview document identity to the active target

* fix(analysis): refresh legacy overview snapshot results
2026-10-09 00:57:28 +08:00
morluto 4a1837d9d5 fix(javascript): bound primitive string growth and repeated evaluation 2026-10-09 00:49:31 +08:00
morluto 770d2f60aa test(workflows): verify residual questions in CLI snapshots 2026-10-09 00:49:31 +08:00
morluto eb89a16d9b fix(process): identify unknown live processes in cleanup diagnostics 2026-10-09 00:49:31 +08:00
morluto bfb7776006 fix(release): publish prereleases under the next npm tag 2026-10-09 00:49:30 +08:00
morluto 4b1066501b refactor(setup): validate formats through canonical configuration 2026-10-09 00:49:30 +08:00
morluto 8b01e0cc89 fix(providers): validate ELF versions and preserve probe cancellation 2026-10-09 00:49:30 +08:00
morluto 915855e75c fix(native): supervise LLDB and preserve bounded capture uncertainty 2026-10-09 00:49:30 +08:00
morluto 488f448ae2 fix(apple): derive dispatch coverage from located decode facts 2026-10-09 00:49:30 +08:00
morluto 15f92f2988 fix(apple): validate physical slices and dylib platform compatibility 2026-10-09 00:49:30 +08:00
morluto 16f4609d68 fix(javascript): bound semantic expansion and resolve lexical receivers 2026-10-09 00:49:30 +08:00
morluto eaf5734083 fix(browser): bound source-map fetches and retain predecessor requests 2026-10-09 00:49:30 +08:00
morluto 135d102f9d refactor(artifacts): centralize stable inventory identities 2026-10-09 00:49:30 +08:00
morluto d4c2c3c18f fix(target): preserve app bundle filesystem failure reasons 2026-10-09 00:49:30 +08:00
morluto 17809caacd fix(managed): match exact method bytes without decoded signatures 2026-10-09 00:49:30 +08:00
morluto 0136ee8c22 fix(workflows): retain residual unknowns through CLI and MCP 2026-10-09 00:49:30 +08:00
morluto 65a6288257 refactor(input): share schema issue error construction 2026-10-09 00:49:30 +08:00
Kaoru0822andClaude Opus 5.5 99d31ee24a fix(browser): treat module-import initiator positions as script positions
An empty script_id also marks a script-type initiator, such as a module
import, whose url is the importing script file. Its position is a real
script position without a session-script association. The previous
wording called every empty-ID callsite parser markup with "no script
position" and excluded it from the external-script conversion. Describe
the empty ID as a missing session-script association, which only rules
out joining captured source or resource_start, and keep the
external-script conversion for script-file positions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:44:25 +00:00
3c157b4ec6 fix(browser): keep initiator callsites out of script coordinate guidance (#1096)
observe_web_execution also derives callsites from a request's reported
initiator, such as an image the parser found in HTML or a module import,
with an empty script_id. Those positions point into the initiating
resource, not a script, so the "script's enclosing resource" wording and
source-map conversion did not apply to them. Describe the coordinates as
relative to the resource named by url, explain empty script IDs, and
limit the documented conversion to positions with a script_id.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:43:03 +08:00
c1b370ce0d fix(javascript): retain NodeNext artifact source facts (#1087)
* test(javascript): reproduce omitted NodeNext artifact sources

Tests only: qualify current .mts/.cts artifact selection through real directory and ASAR reconstruction and the built public CLI.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

* fix(javascript): retain NodeNext artifact source facts

Read .mts and .cts artifact files through the existing inert TypeScript parser, preserving selected file identities and analysis facts in directory and ASAR inputs. Document source selection and format the existing regression assertion.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

---------

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-09 00:39:39 +08:00
5a67c7fc54 fix(hopper): diagnose MCP provider failures with stage, state, and request context (#1044)
* fix(hopper): diagnose MCP provider failures with stage, state, and request context

* fix(hopper): retain health at the actual provider lifecycle boundary

* test(hopper): distinguish socket loss from later observed process exit

* test(hopper): cover observed exit separately from socket-first replies

---------

Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-09 00:33:56 +08:00
b5c2954ad6 fix(domain): order canonical paths by code point, not UTF-16 unit (#1034)
compareCodePoints documented a Unicode code-point comparison but used
</> operators, which compare UTF-16 code units. For astral code points
(above U+FFFF) the lead surrogate 0xD800-0xDBFF is numerically below the
BMP tail U+E000-U+FFFF, so those characters sorted before it.

Delegate to the existing compareUnicodeCodePoints, which the domain
already uses elsewhere, so the two canonical comparators agree. All call
sites use only the sign, so sort and '< 0' checks are unaffected.

Adds the missing regression coverage for canonicalOrdering.

Co-authored-by: TahsinArafat <TahsinArafat@users.noreply.github.com>
Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-09 00:26:25 +08:00
6218d289e2 fix(browser): advertise zero-based units for runtime source coordinates (#1085)
* fix(browser): advertise zero-based units for runtime source coordinates

inspect_web_event_listeners and observe_web_execution return CDP
callsite, listener and resource_start line_number/column_number values
unchanged: zero-based lines and UTF-16 columns. Neither the advertised
output schema nor docs/web-runtime.md said so. trace_web_source_location
takes one-based lines, so a caller chaining a listener location into it
lands one line early.

Describe the units on the shared schema fields, which reach both tools'
advertised output schemas, and document the conversion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(browser): describe runtime positions relative to the enclosing resource

For an inline <script>, CDP reports listener and callsite positions
relative to the HTML document, and resource_start is where the script
text starts in that document. A real headless Chromium 155 page showed
this: a listener on HTML line 6 came back as line_number 5, with
resource_start {3, 8}. Describe all positions as relative to the
enclosing resource. Document that inline-script positions must have
resource_start subtracted, plus one added to the line, before
trace_web_source_location.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(browser): clarify runtime-to-source coordinate conversion

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-09 00:22:18 +08:00
Rudy Mizrahi CelekliandGitHub 23862af1b5 fix(javascript): preserve HTML carriage return source ranges (#1092)
* test(javascript): reproduce HTML carriage return source coordinates

Compare actual resolved and unresolved artifact graph evidence with parse5 source locations across HTML line terminators and UTF-16 columns. Exercise both public built CLI analysis commands. Tests only; production and workflows unchanged.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

* fix(javascript): preserve HTML carriage return source ranges

Treat LF, CRLF, and bare CR as HTML line endings when projecting actual script opening-tag offsets to retained source ranges. Preserve UTF-16 column semantics and document the source-coordinate contract.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

---------

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
2026-10-09 00:15:24 +08:00
e94be95c66 test(artifacts): verify native directory identity on supported Windows runtimes (#1069)
* fix(artifacts): guard Windows device 0 in directory open integrity check

On Windows, Node.js fs.lstat reports dev as 0 without opening a handle. When DirectoryArtifactReader.open inspects the opened handle using handle.stat(), stat.dev is populated with the volume serial number.

Consequently, the TOCTOU check observed.dev !== entry.sourceIdentity.device evaluated to true on Windows for every regular file, throwing an ArtifactReaderFailure integrity error.

Relax the device comparison when running on Windows or when the inventoried device is 0, while maintaining strict inode verification to ensure the target has not been replaced.

* test(artifacts): verify native directory identity without device bypasses

---------

Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-09 00:11:28 +08:00
Rudy Mizrahi CelekliandGitHub 83defc054b fix(javascript): preserve original BOM source coordinates (#1093)
* test(javascript): reproduce BOM source range drift

Compare original UTF-8 source bytes and their retained digests with direct Babel and parse5 source-location oracles through actual directory and ASAR artifact reconstruction and both built public CLI analysis commands. Preserve no-BOM, structured-BOM and invalid UTF-8 controls. Tests only; production and workflows unchanged.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

* fix(javascript): preserve original BOM source coordinates

Retain the initial UTF-8 BOM for JavaScript and HTML source text so graph ranges refer to the original digest-bound bytes. Preserve structured-text BOM handling and fatal UTF-8 decoding.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

* style(javascript): format BOM decoder option

Apply the locked Oxfmt line wrapping to the approved decoder option; no behavior or test changes.

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

---------

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
2026-10-09 00:09:55 +08:00
eb69763d27 fix(process): name the reason two captures cannot be compared (#1080)
* fix(process): name the reason two captures cannot be compared

compare-process-captures rejected captures whose comparison contracts
differ, but the reason never reached the caller. The CLI printed
"Process command could not complete... run `rea doctor`". MCP wrapped
the error in an AnalysisInputError with no issues ("Analysis input is
invalid. Check the arguments"). A stale capture under
max_capture_age_ms got the same treatment.

Throw typed input issues that name the differing contract fields, or
the stale side with its completion time and age, and let
runDerivedOperation pass AnalysisInputError through unchanged. Document
what the comparison contract covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(process): report CLI trace-spec issues under the MCP input path

The CLI reported an invalid trace specification under operation
"compare-process-captures" with root-relative issue paths such as
["events"]. MCP reports the same input as compare_process_captures with
paths under trace_spec. Use the canonical operation name and prefix
trace_spec so both surfaces describe the same input identically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(process): name the side and constraint for unusable capture Evidence

Adjacent compare_process_captures failures still lost their reason.
Over MCP, Evidence from another workflow, tampered Evidence and an
invalid capture result all became "Evidence is invalid or has changed".
The CLI said only "Capture evidence is malformed".

Parse each side separately. Evidence from another workflow is an input
issue on left or right that names the operation and predicate it has.
Tampered Evidence and invalid capture results stay integrity
mismatches, and their message now carries the validation detail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:04:35 +08:00
289aef6838 fix(workflows): keep caller-input details in workflow input errors (#1089)
* fix(managed): keep caller-input details in managed workflow errors

The managed workflows wrapped every rejected input as
`new AnalysisInputError(op, { cause })` with no issues. The projected
result said "Correct the listed arguments and retry" and listed nothing:

- `rea import-managed-reconstruction`,
  `rea project-managed-application-graph` and
  `rea verify-managed-native-boundaries` lost every schema issue for
  malformed JSON;
- domain checks such as a reconstruction text hash mismatch, or Evidence
  from the wrong operation, lost their message on CLI and MCP;
- `rea compare-managed-members` with a missing file did not say which
  path failed.

Add managedInputError, which projects Zod issues with their paths and
keeps a domain TypeError's message, as the JavaScript workflows already
do. Return the BinaryTargetError from an unopenable comparison path
unchanged, with details.path, as every other path-based command does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(managed): keep nested Evidence schema issues off request paths

A ZodError raised after the request parsed comes from a nested
Evidence result, so its issue paths are relative to that result.
Projecting it against the whole request reported, for example,
artifact.sha256 as a missing request field. Keep request-relative
projection for the raw request's schema failures. Report nested schema
failures as root issues whose message names the nested location.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(managed): type the malformed nested Evidence fixture

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(workflows): keep input details in the remaining workflow services

Inventory projection (project_android_application_graph and
project_apple_application_graph), build_reconstruction_obligation_ledger
and evaluate_reconstruction_coverage had the same issue-less
`AnalysisInputError({ cause })` as the managed services. For example,
evaluating coverage for an unknown boundary returned "Correct the listed
arguments" with no issues, dropping "Unknown reconstruction boundary: X"
on CLI and MCP.

Move the helpers to src/application/workflowInputError.ts and use them
for these services: requestInputError for raw request schema failures,
workflowInputError for nested schema and domain constraint failures.
Classification is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 23:56:19 +08:00
YsopwnableandGitHub a1ed4232de fix(mcp): retain complete application results across response limits
Retain complete authenticated application Evidence when the repeated MCP representation exceeds the default receive frame or Node string limit. Return a typed transport constraint, keep useful followup queries available, and stream full canonical bundles to atomic destinations.

Verify complete Notion and Obsidian workflows on Linux and native Windows, full CLI/Evidence parity, and real large Chrome screenshot recovery. Preserve complete success contracts and caller-selected evidence paths.

Refs #1065, #1053.
2026-10-08 23:46:02 +08:00
morluto c701376777 perf(build): avoid duplicate documentation generation 2026-10-08 23:29:43 +08:00
morluto 2af12a18a3 fix(process): preserve observations after successful cleanup 2026-10-08 23:29:43 +08:00
morluto 5e1afe454f fix(snapshot): retain MCP workflow replay bindings 2026-10-08 23:29:43 +08:00
morluto a9e00b83f3 docs: clarify evidence ownership and verification guidance 2026-10-08 23:29:43 +08:00
YsopwnableandGitHub 5f93b4b125 fix(ci): restore generated shard inputs and cleanup assertions
Restore ignored generated inputs before deterministic test shards consume the downloaded build. Preserve verified cleanup assertions and explicitly check residual unknowns for unrelated processes.
2026-10-08 23:20:52 +08:00
morluto 46d195e8a4 docs: check surrounding cases when changing boundaries 2026-10-08 22:57:03 +08:00
morluto 9833c94656 docs: require surrounding boundary checks for review fixes 2026-10-08 22:56:00 +08:00
morluto 6e6ff7bd2e docs: move star milestone into history sections across readmes 2026-10-08 22:54:57 +08:00
YsopwnableandGitHub 9455e7c8bc docs: celebrate 20,000 GitHub stars 🎉 (#1079) 2026-10-08 22:44:10 +08:00
morlutoandGitHub e6134056cf fix(build): make catalogs and packaged skills generated outputs (#1086)
* fix(build): generate catalogs and packaged skills without snapshot commits

* fix(build): point skill drift recovery to uncached generation
2026-10-08 22:43:51 +08:00
YsopwnableandGitHub b49beb42d7 fix(browser): normalize selected script manifest roots on Windows (#1078)
* fix(browser): normalize selected script manifest roots on Windows

* test(browser): keep manifest fixtures valid across Windows drives
2026-10-08 22:37:10 +08:00
YsopwnableandGitHub a4d77f0ff0 docs(website): add a CTF showcase and FAQ (#1077)
* docs(website): add a worked CTF reverse-engineering showcase

* docs(website): add a dedicated FAQ page
2026-10-08 22:30:10 +08:00
Kaoru0822andGitHub 82a1a525d7 Merge pull request #1073 from morluto/fix/evidence-bundle-diagnostics
fix(evidence): name the failed constraint when a bundle is rejected
2026-10-08 07:27:21 -07:00
Kaoru0822andGitHub e8b92a40c0 Merge pull request #1074 from morluto/fix/evidence-file-missing
fix(evidence): report missing evidence files instead of permission advice
2026-10-08 07:22:31 -07:00
YsopwnableandGitHub d52f66d62a fix(metadata): stop schema changes churning generated skill evidence (#1075) 2026-10-08 22:21:30 +08:00
Kaoru0822andGitHub e2f03786f4 Merge pull request #1071 from morluto/fix/asset-catalog-absent
fix(apple): report apps without asset catalogs as empty inventories
2026-10-08 07:15:17 -07:00
Kaoru0822andGitHub 5a55d5b1c6 Merge pull request #1070 from morluto/fix/keyed-archive-selection-errors
fix(artifacts): report keyed-archive selection mistakes as invalid input
2026-10-08 07:15:02 -07:00
YsopwnableandGitHub ef30343154 fix(setup): launch Windows npx registrations through cmd.exe (#1072) 2026-10-08 22:11:54 +08:00
Kaoru0822andClaude Opus 5.5 38bbb656da fix(evidence): require a complete record before the single-record hint
A malformed bundle that merely has an evidence_id field was described
as one Evidence record. Use the single-record message only when the
input matches the Evidence record schema; otherwise report the bundle
schema failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:09:28 +00:00
Kaoru0822andClaude Opus 5.5 89252f1f35 fix(evidence): report missing evidence files instead of permission advice
Reading a nonexistent Evidence bundle or snapshot, or exporting into a
directory that does not exist, failed as reason "io" with "Evidence
file could not be accessed. Check file permissions", and with no path.
In `rea compare` the caller could not tell which of the two files
failed.

Map ENOENT and ENOTDIR to a new "missing" reason with read- and
write-specific advice. Every EvidenceFileError now carries the resolved
caller path in its projected details. Permission and other I/O failures
keep the existing advice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:07:43 +00:00
5547c1e697 fix(process): stop failing captures on tokens macOS cannot expose (#1057)
* fix(process): stop failing captures on tokens macOS cannot expose

On a busy macOS 26 host the residual process sweep fails every capture:
KERN_PROCARGS2 omits the environment of Apple platform binaries for any
caller and fails with EINVAL for processes owned by another account, so an
unrelated process created after the capture baseline escalates to a hard
cleanup failure with cleanup_incomplete.

Report environment withholding distinctly from the Darwin token reader by
reading the platform-binary flag through csops, carry the effective uid in
the process table, and re-read unresolved tokens with a bounded retry plus a
final liveness confirmation so transient exec and exit states settle.
Candidates whose token the OS provably withholds, foreign-uid candidates,
and candidates that exited during validation are now recorded as unverified
processes instead of failing an otherwise clean capture or being signaled.
Related candidates and every other unreadable candidate stay fail-closed.

* test(process): align ownership lanes with the uid-aware process table

The Darwin sweep reads the effective uid through ps, so the offline
verification lanes' syscall allowlists must accept the new argv in
verify-binary-layout and verify-evm-interface. The foreign-uid ownership
test requires POSIX process uids; skip it where the platform has none
(the curated Windows lane).

* fix(verify): recognize UID-bearing ownership process tables

---------

Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-08 22:01:05 +08:00
Kaoru0822andClaude Opus 5.5 cf96ceb211 fix(evidence): name the failed constraint when a bundle is rejected
evidence-import, evidence-export, compare and the MCP bundle tools gave
every rejected file the same message: "Evidence is invalid or has
changed. Recreate or re-import it". That included a single Evidence
record, which is what every command prints, so `rea compare a.json
b.json` on two command results looked like tampering.

Report a single record as such and point to export_evidence_bundle.
Locate the first schema issue, and keep thrown constraint messages such
as a semantic identifier mismatch. EvidenceIntegrityError now accepts
the userMessage option that other typed errors already use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:59:48 +00:00
Kaoru0822andClaude Opus 5.5 a079af297b fix(apple): keep skipped catalog entries from reading as absence
A symlink or directory named Assets.car is skipped by the
non-following directory reader. When no regular catalog remains, fail
with the skipped paths instead of reporting an empty inventory. When
catalogs were read, list the skipped entries in the limitations. The
absence limitation now says that symlinks are not followed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:52:18 +00:00
Kaoru0822andClaude Opus 5.5 79e5a3923d fix(apple): report apps without asset catalogs as empty inventories
inspect_asset_catalog failed with an "unavailable" artifact error for an
app bundle that has no compiled Assets.car. Its advice was "Check
artifact support and required tools", which does not apply: assetutil
was present, and the absence of a catalog is itself an observation.
decode_interface_builder already returns an empty graph for an app
without NIBs.

Return the empty page with an explicit limitation. Interface Builder
resource keys are still decoded and reported as unmatched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:45:43 +00:00
Kaoru0822andClaude Opus 5.5 25fb6d32a3 fix(artifacts): report keyed-archive selection mistakes as invalid input
inspect_keyed_archive reported caller selection failures as artifact
faults. A missing named root became "Artifact is invalid or has changed.
Get a fresh copy" and lost its reason, and a missing, non-canonical, or
directory archive path became "unsafe or conflicting internal path ...
correct the artifact".

Report these as invalid_request issues on `path` or `root`, listing the
available $top roots or the active plist basename. Decode failures keep
the decoder's reason, and a compiled NIBArchive points to
decode_interface_builder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:43:30 +00:00
40e18b2e37 fix(domain): preserve prototype-named JSON members through Evidence boundaries (#1049)
* fix(domain): preserve prototype-named JSON members through Evidence boundaries

Zod's z.json() record projection drops own __proto__ members as a
prototype-pollution guard, so provider replies carrying them lost evidence
before hashing/serialization and CLI/MCP output. The shared schema now
validates with z.json() as before, then rebuilds the value keeping
prototype-named own members as ordinary data properties (__proto__ via
Object.defineProperty, so the prototype is never mutated).

parseHarJson no longer rejects __proto__ members; the explicit unsupported
case from #992 is replaced with a preservation regression, and the
inspect_web_network_capture contract text is updated to match.

Fixes #1036

* fix(domain): bound JSON depth and oversized payloads at parsing boundaries

jsonValueSchema re-walked hostile input through z.json()'s recursive core, so a deeply nested payload from an MCP tool argument, inline evidence, provider result, or keyed archive threw RangeError: Maximum call stack size exceeded instead of a validation issue. The schema now runs an iterative depth guard first and keeps z.json() for full JSON semantics behind it, which also turns cyclic values into validation errors. The 1000-edge limit stays far below the range where the recursive core overflows its stack (measured between 2000 and 3000 on Node 24) while leaving room for legitimately deep target-derived trees such as compiled NIB view hierarchies, and the guard narrows with a typed record check instead of any or unchecked casts. The non-recursive JSON Schema advertisement stays intact on both the public and standard projection paths. Two payload sizes get explicit budgets tied to their producer constraints: web text artifacts reject beyond the 16 MiB source budget the web tooling already advertises, and native UI screenshot base64 beyond the 64 MiB helper output budget. Regenerated product catalog, skill catalog digest, and completion-ledger manifest follow the schema-boundary change.

* fix(validation): preserve depth diagnostics across process inputs

---------

Co-authored-by: DeryFerd <deryferdikao125@gmail.com>
Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-08 21:32:48 +08:00
bfbfc7330a fix(domain): reject excessive JSON depth with actionable diagnostics (#1029)
* fix(domain): bound JSON depth and oversized payloads at parsing boundaries

jsonValueSchema re-walked hostile input through z.json()'s recursive core, so a deeply nested payload from an MCP tool argument, inline evidence, provider result, or keyed archive threw RangeError: Maximum call stack size exceeded instead of a validation issue. The schema now runs an iterative depth guard first and keeps z.json() for full JSON semantics behind it, which also turns cyclic values into validation errors. The 1000-edge limit stays far below the range where the recursive core overflows its stack (measured between 2000 and 3000 on Node 24) while leaving room for legitimately deep target-derived trees such as compiled NIB view hierarchies, and the guard narrows with a typed record check instead of any or unchecked casts. The non-recursive JSON Schema advertisement stays intact on both the public and standard projection paths. Two payload sizes get explicit budgets tied to their producer constraints: web text artifacts reject beyond the 16 MiB source budget the web tooling already advertises, and native UI screenshot base64 beyond the 64 MiB helper output budget. Regenerated product catalog, skill catalog digest, and completion-ledger manifest follow the schema-boundary change.

* fix(validation): preserve depth diagnostics across process inputs

---------

Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-08 21:12:10 +08:00
3dcb732da3 fix(cli): preserve blank JSON paths for input validation (#1068)
* fix(cli): preserve blank JSON paths for input validation

* test(cli): avoid unchecked path field assertions

---------

Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-08 20:57:54 +08:00
6a7650e93c fix(mcp): flatten root union input schemas (#1062)
* fix(mcp): flatten root union input schemas

* fix(mcp): preserve minimum input requirements in object schemas

---------

Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-08 20:54:51 +08:00
Rudy Mizrahi CelekliandGitHub 170600541b fix: select Node legacy package entry fields for import and require (#1035)
fix(javascript): use Node legacy package entry fields

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-08 20:47:01 +08:00
68b9fa489b fix(install): guard empty prefix_args expansion on macOS Bash 3.2 under set -u (#1061)
* fix(install): guard empty prefix_args expansion under Bash 3.2 set -u (#1060)

* test(install): cover native Bash macOS installation

---------

Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-08 20:39:39 +08:00
YsopwnableandGitHub 8a31508c89 Merge pull request #1064 from morluto/fix/release-checkpoint-guards
fix(release): validate frozen checkpoint versions and breaking notes
2026-10-08 18:31:45 +08:00
N0zoM1z0 f5009ed55f fix(release): recognize conventional titles in GitHub merge commits 2026-10-08 10:21:02 +00:00
N0zoM1z0 60825fb447 fix(release): validate frozen checkpoint versions and breaking notes
Audit actual first-parent ancestry instead of trusting the release bot
chronological cutoff, and block mismatched versions before tag creation.
Sync the published 6.0.0 baseline without dropping newer main changes.

Fixes #1063

Assisted-by: Sakuya (gensokyo-skills:sakuya-checkpointed-execution)
2026-10-08 10:13:05 +00:00
N0zoM1z0 5dfd9905ed chore(release): sync 6.0.0 checkpoint metadata 2026-10-08 09:49:53 +00:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>N0zoM1z0
6fee42689a chore(release/6.0.0): release rea-agents 6.0.0 (#1058)
* chore(release/6.0.0): release rea-agents 5.1.0

* chore: normalize release product catalog

* chore(release): correct 6.0.0 candidate and migration notes

Keep the recorded application checkpoint unchanged. The upstream history walk
stops at the merged side-branch release commit and omits older unreleased main
commits; restore the missing notes and absolute-path migration instructions.
Set all release metadata to the agreed SemVer 6.0.0.

Assisted-by: Sakuya (gensokyo-skills:sakuya-checkpointed-execution)

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-08 17:40:01 +08:00
5ccb9241be perf(mcp): advertise each tool JSON Schema once per target (#1059)
The MCP SDK reconverts every registered Zod schema on each tools/list.
With 139 tools that is ~45 ms of Zod JSON Schema work per listing, for a
catalog that cannot change after registration. Memoize the advertised
input and output projections per target on the registration clones.

Listings are byte-identical; repeated tools/list drops from ~62 ms to
~18 ms on the stdio server.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 17:31:08 +08:00
YsopwnableandGitHub 163ee09a74 feat(native): inspect recorded crashes through pwntools and pwndbg (#1054)
* feat(native): inspect recorded crashes through pwntools and pwndbg

* fix(native): bind core note semantics to raw owner bytes

* fix(native): validate recorded core ABI and evidence scalars

* fix(native): require complete recorded note interpretations

* fix(native): bind physical core notes to original headers
2026-10-08 16:50:31 +08:00
YsopwnableandGitHub b9fb438835 fix(javascript): complete large-ASAR analysis with file-local projection (#1055) 2026-10-08 16:15:35 +08:00
YsopwnableandGitHub 7af7bc88ce feat(evm): inspect offline bytecode interfaces through EVMole (#1051)
* feat(evm): inspect offline bytecode interfaces through evmole

* fix(evm): distinguish unsupported bytecode targets

* style(evm): format verifier string-prefix checks
2026-10-08 15:32:05 +08:00
kimtthandGitHub c5d165bd03 docs: improve Korean wording and particles (#1047) 2026-10-08 15:10:28 +08:00
Kaoru0822andGitHub 78d594cacd Merge pull request #927 from morluto/feat/dylib-resolution
feat(artifacts): trace Mach-O dylib resolution in bundles
2026-10-07 23:52:42 -07:00
Kaoru0822andClaude Opus 5.5 3362dfa31e chore: merge main into feat/dylib-resolution
main now carries #1037 (skill version 30, reworded native binary row):
- README_ja/ar/ko/zh: take main's tables and count
  trace_dylib_resolution in the artifact graph row (5 -> 6).
- Skill: move to version 31.
- Regenerate the product catalog, conformance manifest, skill digest and
  package metadata (137 tools).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 06:31:43 +00:00
Kaoru0822andClaude Opus 5.5 6fa6300987 chore: merge main into feat/dylib-resolution
main now carries #964 (skill version 29, macOS native row 7 -> 8):
- README_ja/ar/ko/zh: take main's tables and count
  trace_dylib_resolution in the artifact graph row (5 -> 6).
- Skill: move to version 30.
- Regenerate the product catalog, conformance manifest, skill digest and
  package metadata (136 tools).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 06:23:44 +00:00
YsopwnableandGitHub 4fc6565b0f feat(native): inspect offline ELF layout through pwntools (#1037)
* feat(native): inspect offline ELF layout through pwntools

* fix(build): prepare offline ELF verification on clean runners

* fix(native): preserve snapshot acquisition cancellation

* fix(native): handle sectionless ELF and retain decoder diagnostics

* test(native): verify malformed symbol table entry sizes

* fix(native): respect inherited limits and unused segment fields

* fix(native): preserve packed relocations and decoder launch constraints

* fix(native): validate symbol spans and preserve resource failures

* fix(native): preserve diagnostic completeness and memory failures

* fix(native): guard bridge imports against allocation failures

* fix(native): validate relocation symbol and section references

* fix(native): retain diagnostics through compounded cleanup failures

* fix(native): preserve unresolved relocation section references

* fix(native): bound linkage evidence and preserve CPU signal diagnostics

* fix(native): preserve file-size resource failure evidence

* fix(native): retain opaque linkage names and late cancellation output

* test(native): make CPU-limit fixture robust across runners

* fix(native): verify resource failure branches before interpreting exits

* fix(native): validate declared ELF string references

* fix(native): validate section-name table before decoding

* fix(native): enforce canonical ELF name-table escapes
2026-10-08 14:22:08 +08:00
8d5c88f17c fix(javascript): resolve module URLs and reject invalid exports targets (#1031)
Resolve module URL paths once and reject invalid root package exports targets before normalization. Preserve actionable graph diagnostics and array selection behavior.

Fixes #1040.

Co-authored-by: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com>
2026-10-08 14:00:34 +08:00
Kaoru0822andGitHub 1d13cef5f4 Merge pull request #964 from morluto/feat/native-call-observation
feat(native): observe function and Objective-C method calls under LLDB
2026-10-07 22:57:02 -07:00
1154f2f03c fix(javascript): reconstruct HTML script loads accurately (#1042)
* fix: parse HTML script references with HTML semantics

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>

* test(javascript): cover exact HTML script resource identity

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-08 13:24:24 +08:00
Kaoru0822andClaude Opus 5.5 2a7dcb77c1 chore: merge main into feat/native-call-observation
Resolve conflicts against main:
- package.json: ship both the LLDB tracer bridge and main's process run
  token reader sources.
- README.md: take main's simplified English README, which no longer has
  the tool-family table.
- README_ja.md: take main's reworded table and count
  observe_native_calls in the macOS native row (7 -> 8).
- Skill: the references gain the observe_native_calls paragraph, so the
  version moves to 29 (main already uses 28).
- Regenerate the product catalog, conformance manifest, skill digest and
  package metadata (135 tools). The MCP tool catalog is now built rather
  than committed (#997), so it is removed here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 04:58:36 +00:00
Kaoru0822andClaude Opus 5.5 5acdb9dc6f chore: merge main into feat/dylib-resolution
Resolve conflicts against main:
- README.md: take main's simplified English README, which no longer has
  the tool-family table.
- README_ja.md: take main's reworded table and count
  trace_dylib_resolution in the artifact graph row (5 -> 6).
- Skill: the references gain the trace_dylib_resolution paragraph, so
  the version moves to 29 (main already uses 28).
- Regenerate the product catalog, conformance manifest, skill digest and
  package metadata (135 tools). The MCP tool catalog is now built rather
  than committed (#997), so it is removed here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 04:45:50 +00:00
53409ab196 fix(javascript): resolve root package exports arrays (#1046)
Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-08 12:45:21 +08:00
AlexandGitHub 03424ae91e fix(javascript): recognize digits in artifact URI schemes (#1045) 2026-10-08 12:28:34 +08:00
6b55de017b fix(mcp): bound capture comparison input schema depth (#1005)
* fix(mcp): advertise JSON values without recursive schemas

`z.json()` projects a self-referential `$defs/__schema0` entry. Twenty
tool input schemas and 123 output schemas carried it, and clients or
model APIs that reject recursive JSON Schemas refuse the whole tool
list. Every JSON value satisfies `{}`, so the shared JSON value schema
now advertises that. Runtime parsing still admits only JSON values.

Refs #919

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mcp): keep advertised input schemas within model function-calling limits

Some model providers reject the whole tool list when any input schema
contains a recursive $ref or nests deeper than 10 levels (fixes #919).
Sanitize the advertised wire projection: break cyclic $refs, bound nesting
depth, and drop unreachable $defs. Canonical Zod contracts are untouched.

---------

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: sx4im <sx4im@users.noreply.github.com>
Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-08 12:26:00 +08:00
7425f70fd0 fix(mcp): advertise JSON values without recursive schemas (#995)
`z.json()` projects a self-referential `$defs/__schema0` entry. Twenty
tool input schemas and 123 output schemas carried it, and clients or
model APIs that reject recursive JSON Schemas refuse the whole tool
list. Every JSON value satisfies `{}`, so the shared JSON value schema
now advertises that. Runtime parsing still admits only JSON values.

Refs #919

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-08 11:48:57 +08:00
YsopwnableandGitHub 28f10cf2bc docs: simplify the English README and organize guides (#1043)
* docs: simplify the English README

Preserve the opening and add the official skills.sh badge. Move setup diagnostics and CLI/Evidence details into guides, add three website showcases, and check setup-client facts in the installation guide.

Refs #1017

* docs: add README workflow and FAQ

Restore the original Disclaimer verbatim, invite community showcases, reuse the website investigation figure, and answer six common questions.

* docs: clarify README updates and collapse FAQ

* docs: welcome contributions in the README

* docs: highlight website guidance before quick start

* docs: keep README capability links aligned with main

* fix(docs): preserve README backlinks and release checks
2026-10-08 11:44:00 +08:00
YsopwnableGitHubKaoru0822github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2bf3663f65 feat(browser): inspect historical web network captures (#992)
* feat(browser): inspect retained HAR and mitmproxy capture evidence

Add independent offline format adapters, inline CLI/MCP evidence, bounded
owned decoder processes, and real producer verification.

Refs #882

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* fix(browser): preserve capture boundaries during cancellation and decoding

Honor cancellation during owned-command cleanup, retain opaque HAR
extension content, and clean verifier resources on every failure path.

Refs #882

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* fix(browser): exclude sensitive capture fields across representations

Normalize known native transport URLs for both bytes and strings, including backups. Exclude explicitly sensitive text fields with null so redaction markers cannot reintroduce declared literals. Cover both provider identities in catalog conformance.

Refs #882

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* fix(browser): exclude sensitive capture identities and bound diagnostics

* fix(browser): validate excluded HAR subtrees before projection

* fix(browser): distinguish capture input limits from decoder failures

* fix(browser): preserve decoder failure categories

* fix(browser): validate excluded native credential values

* test(browser): report capture verifier assertions directly

* fix(browser): classify native representation failures precisely

* fix(browser): exclude URL userinfo after header whitespace

* fix(browser): preserve capture evidence and failure boundaries

* fix(browser): exclude sensitive literals from capture limitations

* test(browser): check limitation values without JSON syntax

* fix(browser): preserve acquisition integrity and exclude selection diagnostics

* fix(browser): preserve capture acquisition and declaration boundaries

* fix(mcp): project sensitive capture argument validation

* fix: preserve capture bytes and prelaunch cancellation

* fix(browser): exclude sensitive HAR record ancestry

* fix(browser): preserve transport URL and parameter exclusions

* fix(browser): preserve numeric redaction and canonical capture provenance

* fix(browser): cover derived capture values and ownership preparation deadlines

* fix(browser): exclude explicitly sensitive artifact digests

* chore: regenerate generated metadata

* fix(browser): reject lossy HAR member normalization

* test(browser): assert the advertised unsupported-profile outcome

* fix(browser): preserve JSON visitor nesting failure locations

* fix(browser): exclude explicitly sensitive native source ranges

---------

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-08 07:53:41 +08:00
YsopwnableandGitHub 6e4535c101 refactor(artifacts): move inventory and extraction into provider owners (#1032) 2026-10-08 05:51:14 +08:00
YsopwnableandGitHub c6dad93208 feat(ghidra): support native x86 PE applications on Windows (#1030) 2026-10-08 05:11:47 +08:00
morlutoandGitHub 63cf28fd8f fix(process): preserve partial evidence and verify detached cleanup (#1011)
* test(ci): allow transport delivery in source identity regression

* fix(process): preserve partial evidence and verify detached cleanup

* fix(process): scope Linux token sweeps by stable process identity

* fix(process): retain environment permission failure codes

* fix(process): wait for signaled processes before verifying cleanup

* fix(process): verify sampled detached groups during cleanup

* fix(electron): preserve prelaunch identities through cleanup

* fix(process): retry growing Darwin process table snapshots

* fix(process): retain compatibility with saved v3 capture evidence

* fix(process): migrate saved v3 captures during reconstruction verification

* fix(process): preserve cancellation during Darwin helper compilation

* fix(process): propagate cancellation through ownership baselines

* perf(javascript): cache canonical export comparison sort keys

* fix(process): cancel provider ownership preparation before spawn

* fix(process): isolate native compiler waiter cancellation
2026-10-08 04:46:46 +08:00
morlutoandGitHub 63ba7424c0 perf(javascript): cache canonical export comparison sort keys (#1028) 2026-10-08 04:35:32 +08:00
YsopwnableandGitHub 28d5c397c8 fix(windows): allow Java to read retained target snapshots (#1027) 2026-10-08 04:33:01 +08:00
morluto aafdf8a01b test: prune redundant conformance checks and document test priorities 2026-10-08 04:03:24 +08:00
morluto f8a53a8c06 test: consolidate service and process fixtures 2026-10-08 04:03:24 +08:00
morluto d88d6c1897 test(cli): reduce repeated startup and filesystem setup 2026-10-08 04:03:24 +08:00
morluto 9c3479ed95 test(mcp): consolidate SDK schema checks and scenario ports 2026-10-08 04:03:24 +08:00
morluto 26829045d0 test(browser): consolidate CDP workflows and colocate parser coverage 2026-10-08 04:03:24 +08:00
morluto 1fd1989d34 test(providers): verify framing through client socket boundaries 2026-10-08 04:03:23 +08:00
morluto f517a5a1d7 test(javascript): consolidate source analysis and export regressions 2026-10-08 04:03:23 +08:00
morluto c93422a898 refactor: remove abandoned test-only analysis scaffolding 2026-10-08 04:03:23 +08:00
YsopwnableandGitHub 984fc27ce1 test(javascript): cover plain long-line loader identities (#1025) 2026-10-08 03:56:10 +08:00
YsopwnableandGitHub 0e25befe78 fix(cli): stream large JavaScript JSON results (#1024)
* fix(javascript): report application analysis progress on the CLI

* test(cli): preserve progress in filtered output checks

* fix(cli): stream large JavaScript JSON results

* test(cli): include streaming output in dispatcher fixture
2026-10-08 03:44:51 +08:00
morlutoandGitHub ed725dace4 test(ci): stabilize browser source identity fixture delivery (#1023) 2026-10-08 03:25:15 +08:00
YsopwnableandGitHub 7b18c58140 fix(javascript): report application analysis progress on the CLI (#1008)
* fix(javascript): report application analysis progress on the CLI

* test(cli): preserve progress in filtered output checks
2026-10-08 03:19:37 +08:00
YsopwnableandGitHub 51956c1a49 fix(ci): avoid slow Azure archive for the native cross compiler (#1016)
* fix(ci): avoid slow Azure archive for the native cross compiler

* fix(ci): use HTTPS for Ubuntu archive transfers
2026-10-08 03:08:53 +08:00
morlutoandGitHub 93731e17dd fix(ci): decouple stdio smoke tests from Linux display probes (#1014)
* fix(ci): decouple stdio smoke tests from Linux display probes

* test(ci): check compiled catalog without an initial target
2026-10-08 02:50:08 +08:00
YsopwnableandGitHub 6a278a68dd fix(release): support default credentials and record 5.0.0 boundary (#1012)
fix(release): support built-in credentials and refresh release boundary

GitHub now creates approval-required PR workflows for GITHUB_TOKEN-authored
opened/synchronize/reopened events. Restore optional custom credentials and
cover prepare/publish without a separate secret. Record the verified 5.0.0
checkpoint, public 133-tool catalog and package update in the installation guide.

Assisted-by: Sakuya (gensokyo-skills:sakuya-checkpointed-execution)
2026-10-08 02:26:29 +08:00
morlutoandGitHub 76e7e510bb perf(artifacts): replace quadratic ZIP overlap checks (#1013) 2026-10-08 02:08:39 +08:00
morluto 966cca8efe fix(release): sync 5.0.0 checkpoint back to main 2026-10-08 01:43:02 +08:00
1d62e54e1f fix(snapshot): replay validated workflow evidence (#984)
* fix(snapshot): bind replay to validated cache entry

* fix(snapshot): persist composed workflow results

* fix(snapshot): enforce workflow replay identity

* fix(snapshot): honor cancellation after route resolution

* test(snapshot): make cancellation race deterministic

* fix(snapshot): replay all composed workflow evidence

* fix(snapshot): index workflow evidence once per snapshot

Validate workflow entries against a query and content index instead of scanning the evidence bundle for every entry.

* fix(snapshot): reuse previewed routes and bound workflow cache

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-08 01:33:39 +08:00
96a46ada0a fix(javascript): bound semantic node projection for resource safety (#966)
* fix(javascript): bound semantic node projection for resource safety

analyze_javascript_application exhausted the heap or failed serialization on
ordinary inputs. A small extracted tree crashed outright, and a larger
application tree failed at both the default heap and an 8 GB heap.

Cause: the semantic projection emits one node per AST expression, call site,
binding and property slot, with no ceiling. A five-file tree already yields on
the order of 170,000 nodes, and the whole result is serialized after the object
graph is built and validated. Result ceilings were deliberately removed upstream,
so nothing bounded this growth.

Apply two resource-safety ceilings and report them through the graph's own
coverage contract rather than dropping data silently:

- SEMANTIC_GRAPH_NODE_CEILING bounds retained nodes per tree (100,000, roughly
  300 MB serialized, below V8's 512 MB string limit).
- SEMANTIC_GRAPH_FILE_NODE_CEILING bounds each source file (20,000) so one
  bundled library cannot starve the application's own modules.

Exhausting either marks coverage truncated with a partial status and records the
ceiling in coverage.limits. A small application stays complete and untruncated.

Verified: a previously-crashing application tree now returns Evidence in under a
minute, while a small tree stays complete. Existing tests and check:fast pass.

* fix(javascript): report semantic graph truncation accurately

---------

Co-authored-by: REA Contributor <contributor@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-08 01:22:10 +08:00
b4fc0ecaa1 fix(release): publish from explicit source checkpoints (#1006)
* fix(release): publish from explicit source checkpoints

Dispatch preparation and publication against a frozen release branch.
Bind package builds and provenance to the same tagged commit, enable
release-branch PR CI, and document the maintainer workflow.

Assisted-by: Sakuya (gensokyo-skills:sakuya-checkpointed-execution)

* fix(release): stop publication when the checkpoint has moved

Reject unknown phases and a release-branch tip that differs from the
dispatch SHA before Release Please creates a tag. Keep registry jobs and
catalog pushes on their own phase, and run website checks for release PRs.

* fix(release): require a token that can trigger release CI

Stop falling back to GITHUB_TOKEN, which cannot start workflow runs for the release pull request it creates.

* test(release): require the CI-capable release token

Lock the workflow to RELEASE_PLEASE_TOKEN and fail preparation when that secret is missing.

* style(release): format the release-token test

---------

Co-authored-by: N0zoM1z0 <1705004398@qq.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-08 01:15:54 +08:00
YsopwnableandGitHub 64c71880ae fix(release): publish from explicit source checkpoints (#1003)
* fix(release): publish from explicit source checkpoints

Dispatch preparation and publication against a frozen release branch.
Bind package builds and provenance to the same tagged commit, enable
release-branch PR CI, and document the maintainer workflow.

Assisted-by: Sakuya (gensokyo-skills:sakuya-checkpointed-execution)

* fix(release): drive checkpoint releases from frozen branches

Use branch pushes for Release Please preparation and reviewed-merge publication.
Reject moved source refs before tagging and bind registry provenance to the
triggering release commit. Remove phase inputs and document the three steps.

Assisted-by: Sakuya (gensokyo-skills:sakuya-checkpointed-execution)
Assisted-by: Cirno (gensokyo-skills:cirno-radical-simplification)
2026-10-08 01:12:18 +08:00
fe64fd65d9 fix(mcp): reject unknown MCP tool inputs (#975)
* fix(mcp): reject unknown official tool inputs

* fix(mcp): reject unknown native tool inputs

* test(mcp): follow native contract module move

* fix(mcp): reject unknown remaining tool inputs

* fix(mcp): satisfy strict unknown and browser input schemas

Remove obsolete approval fields from residual-unknown fixtures, project update inputs explicitly, and stop passing target_id to list-targets parsing.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-08 01:10:53 +08:00
ae0f725fad fix(browser): preserve authorized redirect hops (#986)
* fix(browser): preserve authorized redirect hops

* fix(browser): compare redirect semantics without event times

* fix(browser): classify missing redirect predecessors

* fix(browser): distinguish malformed redirect URLs

* fix(browser): preserve evidence on malformed redirect envelopes

* fix(browser): freeze evidence after malformed redirect

* docs: regenerate catalogs after main integration

* fix(browser): account for skipped redirect body shapes

* fix(browser): mark skipped redirect response shapes unavailable

When a malformed redirect keeps a JSON predecessor, record that its response body shape was not captured.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-08 01:08:31 +08:00
a6132be054 fix(cli): classify ENOTDIR JSON input failures (#1000)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-08 01:06:31 +08:00
morlutoandGitHub cbc0dd1e7c fix(contracts)!: require absolute local paths for remaining caller-supplied file inputs (#1004)
* fix(contracts)!: require absolute local paths for remaining caller-supplied file inputs

Follow-up to the session snapshot/export tightening: the same invisible server-cwd resolution applied to open_binary target paths (parseBinaryTarget(path, process.cwd())), import_evidence_bundle bundle reads, firmware input files (resolve(raw)), browser executable launch paths (resolve-then-spawn), Electron executable/application/root paths (realpath on raw input), and the expected server path check.

Each field gains the shared isAbsoluteLocalPath refinement plus a catalog-visible description, since JSON Schema cannot express the rule. Examples, scenario fixtures, and contract tests updated to absolute paths; new schema cases cover rejection/acceptance/omission per field.

Explicitly unchanged (verified rooted or out of scope): inspect_plist paths (resolved against the target bundle dir), firmware output_directory (already provider-enforced), filesystem observation paths (resolved against the caller-supplied working_directory), keyed-archive member paths (archive-relative), JS evidence member paths (evidence-relative under root_alias), provider-internal execution schemas, CLI-only doctor flows, and CLI options (own resolve against the operator cwd).

* fix(cli): resolve operator-relative paths before shared absolute-path validation

The contract tightening rejects relative paths, but three CLI commands forward operator-relative input into the shared schemas: firmware commands pass raw args to FirmwareAnalysisService, and the Electron/browser scenario commands validate parsed scenario JSON directly. CLI paths are relative to the operator shell, so resolve them against the working directory first: resolve() in firmwareCommands, and a shared resolveCliJsonPaths JSON key-path helper (cliJsonInput) applied in electronCommands and cliBrowserScenarioCommands. MCP behavior is unchanged — MCP callers must still send absolute paths.

Covers the regression with a relative-path firmware CLI acceptance test (asserts provider-stage capability_unavailable, not a validation error) and unit tests for the helper.
2026-10-08 00:52:37 +08:00
YsopwnableandGitHub eb8f6491a8 fix(ci): generate source catalog before shard tests (#1002) 2026-10-08 00:47:11 +08:00
YsopwnableandGitHub 6aa9afb549 refactor(process): separate capture implementation from analyst workflows (#988)
* refactor(process): separate capture implementation from analyst workflows

* fix(ci): generate source catalog before shard tests
2026-10-08 00:46:21 +08:00
5b172b1042 fix(contracts)!: require absolute local paths for session filesystem inputs (#948)
fix(contracts): require absolute local paths for session filesystem inputs

Snapshot, evidence-bundle export, and managed-target inputs accepted any non-empty string, so relative paths were resolved against the MCP server working directory instead of the caller's intent. Refine these contract fields to absolute host paths (Windows drive and forward-slash forms included), advertise the constraint on the property descriptions, and regenerate the derived catalog, product catalog, and skill catalog digest. Relative CLI options are unaffected because CLI flows do not parse these MCP contracts.

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-08 00:31:55 +08:00
06b047a092 fix(release): extend npm propagation window and improve diagnostics (#943)
* fix(release): extend npm propagation window and improve diagnostics

The release verification step polls npm for up to 20 minutes (80 attempts
× 15s), but npm registry propagation can take longer. When the version
doesn't appear within the window, the workflow fails even though the
package was already published successfully.

Changes:
- Extend polling window from 20 to 30 minutes (120 attempts × 15s)
- Add --force flag to npm view to bypass cache on each poll
- Report full retry window in diagnostics (attempts × sleep time)
- Distinguish propagation delay from CLI verification failure in error message

Fixes #621

* fix(release): drop no-op --force from npm propagation polling

npm view --force is accepted but only prints a protections warning on a read-only command; --prefer-online already forces fresh registry metadata on each poll.

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-08 00:26:25 +08:00
YsopwnableandGitHub 161ed91571 test(process): wait for observable snapshot ctime changes (#999) 2026-10-08 00:24:44 +08:00
morlutoandGitHub 6f0ef16050 fix(build): clarify generated-file check hint and drop redundant catalog step (#998)
The shared stale-file hint told developers to commit regenerated output, which is wrong for the gitignored build catalog. mcp-catalog:generate no longer re-runs the generator after build:cached since build:unlocked already produces it.
2026-10-08 00:21:39 +08:00
morlutoandGitHub bda4ada7fb fix(dotnet): validate managed metadata and CIL boundaries (#938)
* perf(dotnet): reuse decoded metadata rows

* test(dotnet): preserve expanded fixture metadata

* style(dotnet): format metadata inventory

* fix(dotnet): bound method bodies to PE section data

* fix(dotnet): derive fixture metadata index widths

* fix(dotnet): validate managed metadata bounds

* test(dotnet): cover large metadata ReadyToRun layout

* fix(dotnet): validate exception instruction boundaries

* fix(dotnet): report invalid metadata coded indexes

* fix(dotnet): clarify MemberForwarded null diagnostics

* fix(dotnet): honor complete CIL and EH boundaries

* fix(dotnet): validate metadata identity rows and offsets
2026-10-08 00:17:26 +08:00
morlutoandGitHub 35a4e18d30 refactor(build): generate MCP tool catalog at build time instead of committing it (#997)
src/generatedMcpToolCatalog.ts had zero runtime importers and churned on every contract edit, guaranteeing merge conflicts on long-lived branches. It is now gitignored and regenerated by build:unlocked; typecheck and knip depend on build via turbo. Tests consume the build output unchanged. Adds a regen-generated CI workflow that pushes tracked generated metadata (product catalog, skill frontmatter, verification manifest/ledger, package metadata, error schema) to same-repo PR branches.
2026-10-08 00:16:34 +08:00
Kaoru0822andGitHub c67530535a Merge pull request #994 from HoneyTyagii/perf/canonical-digest-batching-904
perf(domain): batch canonical digest writes with bounded buffer
2026-10-07 09:14:23 -07:00
Kaoru0822andGitHub 701efd5f90 Merge pull request #996 from morluto/docs/ja-readme-polish
docs: polish the Japanese README and align it with current facts
2026-10-07 09:12:10 -07:00
Kaoru0822andClaude Opus 5.5 e45da24297 docs: polish the Japanese README and align it with current facts
Correct statements that drifted from the English README: IDA Pro was
missing, the diagram labeled Ghidra read-only although Linux/macOS
support annotations, Android verification named Linux instead of macOS
arm64, and the security model described only Unix sockets. Add scoped
doctor checks, provider selection, the Ghidra/Java FAQ, and Hopper
request behavior, and polish phrasing and terminology throughout.

Refs #958

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:03:37 +00:00
Honey Tyagi dca14900c4 perf(domain): batch canonical digest writes with bounded buffer 2026-10-07 21:16:44 +05:30
Kaoru0822 6fc77206e4 Merge remote-tracking branch 'origin/main' into feat/dylib-resolution 2026-10-07 14:48:35 +00:00
Kaoru0822andClaude Opus 5.5 90b25f2e30 Merge origin/main into feat/native-call-observation
Follow #973's native ownership: the call-observation schemas move to
src/domain/native/, and the contract now lives in src/contracts/native/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:47:05 +00:00
Kaoru0822andClaude Opus 5.5 a11c6bd5e4 Merge origin/main into feat/native-call-observation
observe-native-calls now runs through composition/directAnalysis, where #965
moved runProviderAnalysis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:17:08 +00:00
Kaoru0822 3d889dc934 Merge remote-tracking branch 'origin/main' into feat/dylib-resolution
# Conflicts:
#	docs/product-catalog.json
#	docs/verification/managed-conformance-manifest.json
#	skills/reverse-engineer-anything/SKILL.md
#	src/generatedMcpToolCatalog.ts
2026-10-07 14:12:18 +00:00
Kaoru0822 a0f5e33bd8 Merge remote-tracking branch 'origin/main' into feat/native-call-observation
# Conflicts:
#	package.json
2026-10-07 13:39:42 +00:00
Kaoru0822andClaude Opus 5.5 002503d0c0 fix(artifacts): qualify weak and lazy findings below conditional loaders
Weak and lazy unresolved-load findings on edges whose loader loads only
conditionally now say that they apply only if that loader loads, as the
required-load finding already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:33:27 +00:00
Kaoru0822andClaude Opus 5.5 9a487a4e00 fix(artifacts): flag truncated Mach-O headers and keep canonicalization denials
- A file with a recognized Mach-O or FAT magic but fewer than 8 bytes is a
  malformed image, so it stays visible in coverage and root diagnostics.
- Canonicalize the analyzed root and target inside the translated region,
  so a permission revoked after open_binary keeps its reason and path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:29:12 +00:00
Kaoru0822andClaude Opus 5.5 e5c6fccc35 Merge origin/main into feat/dylib-resolution
Follow #952's Apple ownership: the Mach-O load-command reader, dylib
resolution adapter and fixture move to src/artifacts/apple/, the resolver,
dyld path semantics and findings to src/domain/apple/, and the dylib lane
helper to scripts/verify/apple/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:28:14 +00:00
Kaoru0822andClaude Opus 5.5 9837203234 feat(native): observe function and Objective-C method calls under LLDB
Add observe_native_calls (CLI: observe-native-calls <path> <input-json>). It
launches the active Mach-O as an owned process under `lldb --batch`, stops at
caller-selected functions or Objective-C methods, records each entry and
auto-continues:

- thread, image, symbol, load and file address;
- raw integer argument registers and optional caller frames;
- for methods, the selector read from _cmd and the receiver's dynamic class,
  from LLDB's runtime reader without running target code.

No expression is evaluated and no existing process is attached. The run ends
on exit, max_events or duration_ms; REA then kills the process and confirms
that it is gone. The result also carries exit status, bounded stdout/stderr,
breakpoint resolution with partial coverage for unresolved requests, and
limitations. A target LLDB may not debug fails with a debugger-attach-denied
reason and remediation.

verify:native-calls checks the tool through the CLI and stdio MCP against a
compiled fixture, compares breakpoint addresses with nm, exercises every
outcome, and checks hardened-runtime targets with and without get-task-allow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:22:55 +00:00
Kaoru0822andClaude Opus 5.5 5e334a0e12 fix(artifacts): propagate conditional loads and bind digests to parsed images
- Edges below a conditional fallback now carry loader_conditional, reuse of
  a conditional image by install name is conditional, and their findings say
  they apply only if that image loads.
- Hash each traced image only if it is still the file whose headers were
  parsed (device, inode, size and change times), before and after hashing.
- Resolve explicit roots segment by segment, so a root below a symlinked
  directory that points outside the analyzed root is rejected.
- Require a complete 16-byte LC_CODE_SIGNATURE command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:58:20 +00:00
Kaoru0822 9049620dfe Merge remote-tracking branch 'origin/main' into feat/dylib-resolution
# Conflicts:
#	docs/product-catalog.json
#	docs/verification/managed-conformance-manifest.json
#	skills/reverse-engineer-anything/SKILL.md
#	src/generatedMcpToolCatalog.ts
2026-10-07 12:54:53 +00:00
Kaoru0822andClaude Opus 5.5 ef245c0bd0 fix(artifacts): trace .app-named files standalone and flag empty FAT tables
- Treat the target as a bundle only when it was opened from an app bundle
  directory (it carries the bundle's Info.plist); a regular Mach-O file whose
  name ends in .app is traced from its parent directory.
- Classify a FAT header with zero architecture records as malformed, so it
  stays visible in unclassified roots and coverage.
- State that dylib compatibility versions are not compared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:25:37 +00:00
Kaoru0822 19f01e2610 Merge remote-tracking branch 'origin/main' into feat/dylib-resolution
# Conflicts:
#	docs/product-catalog.json
#	docs/verification/managed-conformance-manifest.json
#	skills/reverse-engineer-anything/SKILL.md
#	src/generatedMcpToolCatalog.ts
2026-10-07 12:23:08 +00:00
Kaoru0822andClaude Opus 5.5 91280393cd fix(artifacts): defer lazy loads and tighten Mach-O validation
Address review findings in trace_dylib_resolution:
- Resolve LC_LAZY_LOAD_DYLIB dependencies without traversing them, since
  dyld loads them on first use. An unresolved one becomes
  `lazy-load-unresolved`, not a launch failure.
- Accept FAT tables up to 128 records, as target opening does. A table
  that names no Mach-O CPU type, such as a Java class file, is not
  Mach-O.
- Require 8-byte load-command alignment in 64-bit images. 1,508 real
  slices still match otool.
- Report EACCES/EPERM while probing, reading, or hashing as an
  `unavailable` failure naming the code and path, rather than generic
  io.

Findings and limitations move to dylibResolutionFindings.ts to keep the
resolver under the module size limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:50:00 +00:00
Kaoru0822andClaude Opus 5.5 a0ec3e2016 fix(artifacts): follow dyld slice grading and report unparsed images
Address review findings in trace_dylib_resolution:
- Choose child slices by dyld's graded architectures, so an x86_64h
  process loads a generic x86_64 slice instead of reporting
  architecture-missing.
- Read FAT containers with the byte-swapped FAT_CIGAM and FAT_CIGAM_64
  magics, which target opening already accepts.
- Report string-bearing load commands too short to hold their string
  offset as malformed instead of failing with a RangeError.
- Keep recognized but unsupported images as an `unsupported` candidate
  outcome. It is undeterminable like an outside-root path, not
  malformed.
- List Mach-O files that do not parse during root enumeration as
  unparsed images, so coverage is partial rather than silently complete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:07:31 +00:00
Kaoru0822 63800ff5a2 Merge remote-tracking branch 'origin/main' into feat/dylib-resolution
# Conflicts:
#	docs/product-catalog.json
#	docs/verification/managed-conformance-manifest.json
#	skills/reverse-engineer-anything/SKILL.md
#	src/generatedMcpToolCatalog.ts
2026-10-07 11:04:20 +00:00
morluto 71e5cb1640 chore: merge main and regenerate combined catalog metadata 2026-10-07 18:44:35 +08:00
Kaoru0822andClaude Opus 5.5 11b7f918c2 feat(artifacts): trace Mach-O dylib resolution in bundles
Add trace_dylib_resolution (CLI trace-dylib-resolution) to the artifact
family. For the active Mach-O, or every executable in the active .app
(the main app, XPC services, extensions, login items, helpers), it
reports which file each dylib load reaches and how.

A pure TypeScript reader decodes FAT and Mach-O load commands:
LC_LOAD/WEAK/REEXPORT/LAZY/UPWARD_DYLIB, including the newer
dylib_use_command encoding with weak, re-export, upward and delayed-init
flags, plus LC_ID_DYLIB, LC_RPATH, LC_DYLD_ENVIRONMENT and code-signature
presence. Malformed tables are reported with the failing command instead
of being guessed.

Resolution follows dyld:
- @executable_path comes from each process root;
- @loader_path comes from the loading image, or from the owner of the
  LC_RPATH entry being expanded;
- @rpath searches the loader's rpaths first, then up the load chain;
- dependencies load dependents-first in load-command order, once per
  process, and a matching install name reuses an already loaded image;
- symlinks are followed inside the analyzed root, and escapes stop.

Every candidate path is reported with its outcome. Each edge resolves to
resolved, conditional (an outside-root candidate such as /usr/lib/swift
precedes), unresolved, or undetermined. Absolute and shared-cache system
paths are never checked against the host. Derived findings cover
unresolved required or weak loads, absent earlier @rpath candidates (with
library validation left unknown), and LC_DYLD_ENVIRONMENT.

verify:macos-bundle now also:
- checks every traced image against otool -l;
- asserts each resolution status on a clang-built fixture that adds a
  missing Overrides rpath, an absolute rpath, and a delay-init library;
- compares the predicted load order of two process roots with what dyld
  loads under DYLD_PRINT_LIBRARIES.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:31:17 +00:00
1102 changed files with 80667 additions and 19226 deletions
+6 -1
View File
@@ -28,7 +28,7 @@ intentionally not included. -->
- Provider, bridge, target-format, or platform compatibility:
- Evidence, artifact, provenance, or reconstruction contract:
- Process execution, authorization, cleanup, or containment impact:
- Generated metadata (`docs/product-catalog.json`), package, or installation impact:
- Generated metadata (`docs/public/product-catalog.json`), package, or installation impact (CI generates and validates ignored catalogs and `skills/`; edit authored instructions in `skill-src/` and never commit build outputs):
## Evidence and regression coverage
@@ -59,6 +59,11 @@ relevant real-provider or platform workflow, or explain why it was unavailable.
limits, security/privacy/containment implications, and meaningful proof gaps. -->
- Breaking changes or migration steps:
<!-- For ! / BREAKING CHANGE: show a previously valid call/configuration,
its new behavior and migration, and why compatibility/deprecation cannot
preserve it. Every release increments minor, including breaking changes;
keep breaking markers and migration notes so callers can assess compatibility.
See docs/releasing.md. -->
- Real Hopper/Ghidra, browser, or OS coverage:
- Package or release metadata impact:
- Security, privacy, process, or containment review:
+55 -4
View File
@@ -102,6 +102,12 @@ jobs:
with:
name: rea-dist
path: dist
- name: Restore generated outputs from downloaded build
run: |
node scripts/generate-mcp-tool-catalog.mjs
node scripts/generate-skill-metadata.mjs
node scripts/generate-product-catalog.mjs
node scripts/generate-completion-ledger.mjs
- run: npm run test:ci:shard:run -- --shard=${{ matrix.shard }}
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
@@ -229,7 +235,16 @@ jobs:
with:
node-version-file: .nvmrc
- name: Prepare cross compiler
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
run: |
# Slow Azure archive transfers can exhaust the job before compilation.
# Keep the same signed packages and avoid slow HTTP mirror fallback.
for source in /etc/apt/apt-mirrors.txt /etc/apt/sources.list /etc/apt/sources.list.d/ubuntu.sources; do
if [ -f "$source" ]; then
sudo sed -i 's|azure.archive.ubuntu.com|archive.ubuntu.com|g; s|http://archive.ubuntu.com|https://archive.ubuntu.com|g' "$source"
fi
done
sudo apt-get update
sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
- run: npm run build:windows-native
- run: npm run verify:windows-native:artifact
- uses: actions/upload-artifact@v4
@@ -306,14 +321,20 @@ jobs:
- run: npm run verify:keyed-archive
- run: npm run verify:asset-catalog
- run: npm run verify:macos-bundle
- name: Native compiled NIB hierarchy regressions
run: npm run test:focused -- tests/boundary/filesystem/nibNativeHierarchy.test.ts tests/boundary/filesystem/interfaceBuilderBundle.test.ts
- name: Native installer and compiled NIB hierarchy regressions
run: npm run test:focused -- tests/boundary/filesystem/installScript.test.ts tests/boundary/filesystem/nibNativeHierarchy.test.ts tests/boundary/filesystem/interfaceBuilderBundle.test.ts
- name: Real binary metadata and Interface Builder integration
run: npm run verify:apple-dispatch && npm run verify:interface-builder
- name: Native plist CLI selection regressions
run: npm run test:focused -- tests/boundary/cli/nativePlistCli.test.ts tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts tests/boundary/cli/nativePlistSelectionFailureCli.test.ts
- name: Native Swift demangling CLI and MCP regressions
run: npm run test:focused -- tests/boundary/cli/nativeDemangleCli.test.ts tests/boundary/mcp/nativeDemangleMcp.test.ts
- name: Native signature target binding and CLI/MCP recovery regressions
run: npm run test:focused -- tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts tests/boundary/cli/nativeSignatureCli.test.ts tests/boundary/mcp/nativeSignatureTargetBindingMcp.test.ts
- name: Process filesystem snapshot descriptor regressions
run: npm run test:focused -- src/process/capture/FilesystemSnapshot.test.ts
- name: Native Darwin ownership and process cleanup regressions
run: npm run test:focused -- tests/boundary/process/darwinProcessRunTokenReader.test.ts tests/boundary/process/processCaptureLifecycle.test.ts tests/boundary/process/processCli.test.ts tests/boundary/mcp/sessionMcp.test.ts
reconstruction-readiness:
name: Reconstruction readiness
@@ -428,12 +449,17 @@ jobs:
tests/boundary/filesystem/safeOutputTree.test.ts
tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts
tests/boundary/filesystem/setupUnavailableProvider.test.ts
tests/boundary/filesystem/windowsNpxRegistration.test.ts
src/application/ClientRegistrationIdentity.test.ts
tests/boundary/filesystem/webScriptManifestPaths.test.ts
tests/boundary/process/ProcessEvidence.host.test.ts
tests/acceptance/webModuleCli.test.ts
tests/acceptance/webSourceLocationCli.test.ts
tests/boundary/cli/referenceSourceImportStatus.test.ts
src/cliReferenceSourceImportStatus.test.ts
tests/boundary/filesystem/asarInventoryFreshness.test.ts
tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts
tests/conformance/providers/ghidra/windowsCapabilities.test.ts
tests/conformance/providers/ghidra/ghidraWindowsFixture.test.ts
src/ghidra/GhidraInstallation.test.ts
tests/boundary/providers/ghidra/ghidraTransport.test.ts
tests/boundary/providers/ghidra/ghidraTargetSnapshot.test.ts
@@ -450,6 +476,21 @@ jobs:
tests/conformance/providers/ghidra/realGhidraWindowsWorkflow.test.ts
- run: npm run verify:windows-native
- run: npm run verify:package:windows
- name: Verify native directory identity on Node 24
run: >-
npx vitest run --coverage.enabled=false
tests/boundary/filesystem/directoryArtifactReaderOpen.test.ts
tests/boundary/filesystem/javascriptPackagePrecedence.test.ts
tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts
- uses: actions/setup-node@v4
with:
node-version: 22.19.0
- name: Verify native directory identity on Node 22
run: >-
npx vitest run --coverage.enabled=false
tests/boundary/filesystem/directoryArtifactReaderOpen.test.ts
tests/boundary/filesystem/javascriptPackagePrecedence.test.ts
tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts
- uses: actions/upload-artifact@v4
if: always()
with:
@@ -469,3 +510,13 @@ jobs:
cache: npm
- run: npm ci
- run: npm run docs:check
- name: Generation leaves authored files unchanged
run: git diff --exit-code
- uses: actions/upload-artifact@v4
with:
name: generated-docs
path: |
docs/public/product-catalog.json
docs/verification/managed-conformance-*.json
skills/reverse-engineer-anything
if-no-files-found: error
+78
View File
@@ -0,0 +1,78 @@
name: Real offline binary layout
on:
workflow_dispatch:
inputs:
large_mcp:
description: "Also verify the complete 65,281-section result over MCP (slow)"
type: boolean
default: false
pull_request:
paths:
- ".github/workflows/real-binary-layout.yml"
- "bridge/pwntools/**"
- "src/native/pwntools/**"
- "src/domain/native/binaryLayout*"
- "src/application/binaryDiagnostics/**"
- "src/contracts/native/binaryDiagnosticsToolContracts.ts"
- "src/composition/binaryDiagnostics.ts"
- "src/server/registerBinaryDiagnosticsTools.ts"
- "src/server/createServer.ts"
- "src/cli/binaryDiagnosticsCommands.ts"
- "src/cli.ts"
- "src/cliCommandNames.ts"
- "src/contracts/toolContracts.ts"
- "src/contracts/toolEffects.ts"
- "src/process/OwnedCommand.ts"
- "src/process/ProviderProcess.ts"
- "src/process/ProviderDeadline.ts"
- "src/process/ProcessOwnership*.ts"
- "src/process/PrivateRuntimeRoot.ts"
- "src/artifacts/readStableArtifact*.ts"
- "src/artifacts/ArtifactReader.ts"
- "src/domain/analysisError*.ts"
- "scripts/verify-binary-layout.mjs"
- "scripts/lib/elf-section-name-fixtures.mjs"
- "scripts/lib/large-result-mcp.mjs"
- "scripts/fixtures/binary-layout*"
- "package.json"
- "package-lock.json"
permissions:
contents: read
concurrency:
group: real-binary-layout-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
NODE_OPTIONS: --max-old-space-size=768
HUSKY: "0"
REA_VERIFY_LARGE_ELF_MCP: ${{ inputs.large_mcp && '1' || '0' }}
jobs:
real-binary-layout:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- name: Acquire exact upstream libraries in an isolated runner environment
shell: bash
run: |
layout_python_root="${RUNNER_TEMP}/rea-pwntools-layout"
python3 -m venv "${layout_python_root}"
"${layout_python_root}/bin/python" -m pip install --disable-pip-version-check \
'pwntools==4.15.0' 'pyelftools==0.33' 'unicorn==2.1.2'
command -v gcc
command -v ld
command -v strip
command -v strace
echo "REA_PWNTOOLS_PYTHON=${layout_python_root}/bin/python" >> "${GITHUB_ENV}"
echo "REA_VERIFY_STRACE_COMMAND=$(command -v strace)" >> "${GITHUB_ENV}"
- name: Verify source-owned fixtures through public CLI and MCP
run: npm run verify:binary:layout
+66
View File
@@ -0,0 +1,66 @@
name: Real offline EVM interface
on:
workflow_dispatch:
pull_request:
paths:
- ".github/workflows/real-evm-interface.yml"
- "src/evm/**"
- "src/domain/evm/**"
- "src/application/evm/**"
- "src/contracts/evm/**"
- "src/composition/evm.ts"
- "src/cli/evmCommands.ts"
- "src/server/registerEvmTools.ts"
- "src/server/createServer.ts"
- "src/cli.ts"
- "src/cliCommandNames.ts"
- "src/contracts/toolContracts.ts"
- "src/contracts/toolEffects.ts"
- "src/process/OwnedCommand.ts"
- "src/process/ProviderProcess.ts"
- "src/process/ProcessOwnership*.ts"
- "src/process/ProviderDeadline.ts"
- "src/process/PrivateRuntimeRoot.ts"
- "src/artifacts/readStableArtifact*.ts"
- "src/artifacts/ArtifactReader.ts"
- "scripts/verify-evm-interface.mjs"
- "scripts/fixtures/evm-interface.sol"
- "third_party/evmole/**"
- "package.json"
- "package-lock.json"
permissions:
contents: read
concurrency:
group: real-evm-interface-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
NODE_OPTIONS: --max-old-space-size=768
HUSKY: "0"
jobs:
real-evm-interface:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- name: Acquire unchanged fixture compiler in isolated runner storage
shell: bash
run: |
evm_fixture_root="${RUNNER_TEMP}/rea-evm-fixture-compiler"
mkdir -p "${evm_fixture_root}"
npm install --prefix "${evm_fixture_root}" --ignore-scripts --no-audit --no-fund --save-exact solc@0.8.30
command -v prlimit
command -v strace
echo "REA_VERIFY_STRACE_COMMAND=$(command -v strace)" >> "${GITHUB_ENV}"
echo "REA_VERIFY_SOLC_MODULE=${evm_fixture_root}/node_modules/solc/index.js" >> "${GITHUB_ENV}"
- name: Verify source-owned compiled bytecode through public CLI and MCP
run: npm run verify:evm:interface
+18 -1
View File
@@ -21,7 +21,14 @@ jobs:
with:
node-version-file: .nvmrc
- name: Prepare cross compiler
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
run: |
for source in /etc/apt/apt-mirrors.txt /etc/apt/sources.list /etc/apt/sources.list.d/ubuntu.sources; do
if [ -f "$source" ]; then
sudo sed -i 's|azure.archive.ubuntu.com|archive.ubuntu.com|g; s|http://archive.ubuntu.com|https://archive.ubuntu.com|g' "$source"
fi
done
sudo apt-get update
sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
- run: npm run build:windows-native
- run: npm run verify:windows-native:artifact
- uses: actions/upload-artifact@v4
@@ -53,10 +60,18 @@ jobs:
env:
REA_ANALYSIS_PROVIDER: ghidra
run: npm run verify:ghidra:windows | Tee-Object -FilePath windows-ghidra-proof.log
- name: Verify native x86 Windows Ghidra operations
env:
REA_ANALYSIS_PROVIDER: ghidra
run: npm run verify:ghidra:windows -- --x86 | Tee-Object -FilePath windows-ghidra-x86-proof.log
- name: Verify ordinary-user CLI and MCP operations
env:
REA_ANALYSIS_PROVIDER: ghidra
run: npm run verify:ghidra:windows:package | Tee-Object -FilePath windows-ghidra-package-proof.log
- name: Verify ordinary-user x86 CLI and MCP operations
env:
REA_ANALYSIS_PROVIDER: ghidra
run: npm run verify:ghidra:windows:package -- --x86 | Tee-Object -FilePath windows-ghidra-x86-package-proof.log
- uses: actions/upload-artifact@v4
if: always()
with:
@@ -64,4 +79,6 @@ jobs:
path: |
windows-ghidra-proof.log
windows-ghidra-package-proof.log
windows-ghidra-x86-proof.log
windows-ghidra-x86-package-proof.log
retention-days: 30
+99
View File
@@ -0,0 +1,99 @@
name: Real recorded crash evidence
on:
workflow_dispatch:
pull_request:
paths:
- ".github/workflows/real-recorded-crash.yml"
- "bridge/pwntools/**"
- "bridge/pwndbg/**"
- "src/native/pwntools/**"
- "src/native/pwndbg/**"
- "src/domain/native/recordedCrash*"
- "src/application/binaryDiagnostics/**"
- "src/contracts/native/binaryDiagnosticsToolContracts.ts"
- "src/contracts/toolEffects.ts"
- "src/contracts/toolOutputSchemaGroups.ts"
- "src/composition/binaryDiagnostics.ts"
- "src/server/registerRecordedCrashTools.ts"
- "src/server/createServer.ts"
- "src/server/sessionAvailabilityPolicy.ts"
- "src/cli/binaryDiagnosticsCommands.ts"
- "src/cliCommandNames.ts"
- "src/process/**"
- "src/artifacts/readStableArtifact*.ts"
- "src/domain/analysisError*.ts"
- "scripts/verify-recorded-crash.mjs"
- "scripts/verify/recorded-crash/**"
- "scripts/lib/recorded-core-fixtures.py"
- "scripts/fixtures/recorded-crash.c"
- "third_party/pwndbg/**"
- "package.json"
- "package-lock.json"
permissions:
contents: read
concurrency:
group: real-recorded-crash-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
NODE_OPTIONS: --max-old-space-size=768
HUSKY: "0"
UV_CONCURRENT_BUILDS: "1"
UV_CONCURRENT_INSTALLS: "1"
UV_PYTHON_DOWNLOADS: never
jobs:
real-recorded-crash:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- 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: |
command -v gcc
command -v gdb
command -v strace
core_decoder_root="${RUNNER_TEMP}/rea-core-decoder"
core_pwndbg_root="${RUNNER_TEMP}/rea-core-pwndbg"
core_pwndbg_venv="${RUNNER_TEMP}/rea-core-pwndbg-venv"
python3 -m venv "${core_decoder_root}"
"${core_decoder_root}/bin/python" -m pip install --disable-pip-version-check \
'pwntools==4.15.0' 'pyelftools==0.33' 'unicorn==2.1.2' 'uv==0.12.0'
git init "${core_pwndbg_root}"
git -C "${core_pwndbg_root}" remote add origin https://github.com/pwndbg/pwndbg.git
git -C "${core_pwndbg_root}" fetch --depth=1 origin 55596aa676a2756efb8d67034f31675170eabe48
git -C "${core_pwndbg_root}" checkout --detach FETCH_HEAD
test "$(git -C "${core_pwndbg_root}" rev-parse HEAD)" = 55596aa676a2756efb8d67034f31675170eabe48
UV_PROJECT_ENVIRONMENT="${core_pwndbg_venv}" \
"${core_decoder_root}/bin/uv" sync --project "${core_pwndbg_root}" \
--frozen --no-editable --no-default-groups --python /usr/bin/python3 --no-managed-python
test -z "$(git -C "${core_pwndbg_root}" status --porcelain --untracked-files=no)"
echo "REA_PWNTOOLS_PYTHON=${core_decoder_root}/bin/python" >> "${GITHUB_ENV}"
echo "REA_PWNDBG_GDB=$(command -v gdb)" >> "${GITHUB_ENV}"
echo "REA_PWNDBG_GDBINIT=${core_pwndbg_root}/gdbinit.py" >> "${GITHUB_ENV}"
echo "REA_PWNDBG_VENV_PATH=${core_pwndbg_venv}" >> "${GITHUB_ENV}"
echo "REA_VERIFY_STRACE_COMMAND=$(command -v strace)" >> "${GITHUB_ENV}"
- name: Build and install the exact package in isolated runner storage
shell: bash
run: |
npm run build:cached
npm pack --ignore-scripts --json --pack-destination "${RUNNER_TEMP}" > "${RUNNER_TEMP}/rea-core-pack.json"
core_package_name=$(node -p 'JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8"))[0].filename' "${RUNNER_TEMP}/rea-core-pack.json")
npm install --prefix "${RUNNER_TEMP}/rea-core-installed" --ignore-scripts --no-fund --no-audit "${RUNNER_TEMP}/${core_package_name}"
- name: Verify installed CLI and MCP against the source-owned recording
shell: bash
run: node scripts/verify-recorded-crash.mjs "${RUNNER_TEMP}/rea-core-installed/node_modules/rea-agents/scripts/rea.mjs"
@@ -0,0 +1,75 @@
name: Real historical web network captures
on:
workflow_dispatch:
pull_request:
paths:
- ".github/workflows/real-web-network-captures.yml"
- "bridge/mitmproxy/**"
- "src/browser/history/**"
- "src/domain/webNetworkCapture*.ts"
- "src/domain/analysisError*.ts"
- "src/contracts/errorSchemas.ts"
- "src/domain/explicitSensitive*.ts"
- "src/application/WebNetworkCapture*"
- "src/composition/webNetworkCaptures.ts"
- "src/cli/webNetworkCaptureCommands.ts"
- "src/server/registerWebNetworkCaptureTool.ts"
- "src/server/createServer.ts"
- "src/server/toolRegistrationOptions.ts"
- "src/cli.ts"
- "src/cliCommandNames.ts"
- "src/contracts/toolContracts.ts"
- "src/contracts/toolEffects.ts"
- "src/process/OwnedCommand.ts"
- "src/process/ProviderProcess.ts"
- "src/process/ProviderDeadline.ts"
- "src/process/ProcessOwnership*.ts"
- "src/process/PrivateRuntimeRoot.ts"
- "src/process/capture/ProcessCaptureLifecycle.ts"
- "src/artifacts/readStableArtifact*.ts"
- "src/artifacts/ArtifactReader.ts"
- "src/contracts/webNetworkCaptureToolContracts.ts"
- "scripts/verify-web-network-captures.mjs"
- "scripts/fixtures/generate-mitmproxy-capture.py"
- "package.json"
- "package-lock.json"
permissions:
contents: read
concurrency:
group: real-web-network-captures-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
HUSKY: "0"
NODE_OPTIONS: --max-old-space-size=768
jobs:
real-web-network-captures:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- name: Use pinned package manager
run: npm install --global npm@11.16.0
- run: npm ci
- name: Acquire fixed upstream native profile in runner temporary storage
shell: bash
run: |
capture_tool_root="${RUNNER_TEMP}/rea-mitmproxy-12.2.3"
mkdir -p "${capture_tool_root}"
curl --fail --location --retry 3 --max-time 180 \
https://downloads.mitmproxy.org/12.2.3/mitmproxy-12.2.3-linux-x86_64.tar.gz \
--output "${capture_tool_root}/mitmproxy.tar.gz"
echo '2e95286b618fa6fd33e5e62a78c2e5112571d85f42ec2bac29b97ee242bdb5c5 mitmproxy.tar.gz' \
| (cd "${capture_tool_root}" && sha256sum --check --strict)
tar -xzf "${capture_tool_root}/mitmproxy.tar.gz" -C "${capture_tool_root}"
echo "REA_MITMDUMP_COMMAND=${capture_tool_root}/mitmdump" >> "${GITHUB_ENV}"
- name: Verify upstream producer fixtures through public CLI and MCP
run: npm run verify:web:network-captures
+140 -68
View File
@@ -2,132 +2,199 @@ name: Release
on:
push:
branches: ["release/*"]
branches: [main]
workflow_dispatch:
inputs:
release_branch:
description: Frozen release branch (release/VERSION)
required: true
type: string
phase:
description: Prepare the release PR, or publish its reviewed merge
required: true
default: prepare
type: choice
options: [prepare, publish]
env:
HUSKY: "0"
concurrency:
group: release-${{ github.ref }}
group: release-${{ inputs.release_branch || github.ref_name }}
cancel-in-progress: false
jobs:
release-proposal:
name: Update release proposal
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: googleapis/release-please-action@v4
with:
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
target-branch: main
skip-github-release: true
release-please:
name: Release Please
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
sha: ${{ steps.release.outputs.sha }}
steps:
- name: Validate release selection
shell: bash
env:
GH_TOKEN: ${{ github.token }}
RELEASE_BRANCH: ${{ github.ref_name }}
RELEASE_BRANCH: ${{ inputs.release_branch }}
RELEASE_PHASE: ${{ inputs.phase }}
run: |
if [[ "${GITHUB_REF}" != "refs/heads/${RELEASE_BRANCH}" || "${RELEASE_BRANCH}" != release/* ]]; then
echo "Select a release/ branch; main and tags are not release checkpoints." >&2
if [[ "${RELEASE_PHASE}" != prepare && "${RELEASE_PHASE}" != publish ]]; then
echo "phase must be prepare or publish." >&2
exit 1
fi
if [[ "${RELEASE_PHASE}" == prepare && "${GITHUB_REF}" != "refs/heads/main" ]]; then
echo "Run prepare from main; release_branch selects the frozen source." >&2
exit 1
fi
if [[ "${RELEASE_PHASE}" == publish && "${GITHUB_REF}" != "refs/heads/${RELEASE_BRANCH}" ]]; then
echo "Run publish from the selected frozen release branch so npm provenance records its source." >&2
exit 1
fi
if [[ ! "${RELEASE_BRANCH}" =~ ^release/[^/]+$ ]]; then
echo "release_branch must select a release/ branch; main is not a release checkpoint." >&2
exit 1
fi
if ! git check-ref-format "refs/heads/${RELEASE_BRANCH}"; then
echo "Invalid release branch: ${RELEASE_BRANCH}" >&2
echo "Invalid release_branch: ${RELEASE_BRANCH}" >&2
exit 1
fi
selected_sha="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/${RELEASE_BRANCH}" --jq '.object.sha')"
if [[ "${selected_sha}" != "${GITHUB_SHA}" ]]; then
echo "The release branch moved after this run was triggered; review its new checkpoint before publication." >&2
- name: Resolve release branch tip
id: branch
shell: bash
env:
GH_TOKEN: ${{ github.token }}
RELEASE_BRANCH: ${{ inputs.release_branch }}
run: |
sha="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/${RELEASE_BRANCH}" --jq '.object | select(.type == "commit") | .sha')"
if [[ ! "${sha}" =~ ^[0-9a-f]{40}$ ]]; then
echo "The release branch tip is not a commit." >&2
exit 1
fi
printf 'Selected release checkpoint: %s\n' "${selected_sha}"
echo "sha=${sha}" >> "${GITHUB_OUTPUT}"
- name: Require the publish dispatch to match the branch tip
if: inputs.phase == 'publish'
shell: bash
env:
BRANCH_SHA: ${{ steps.branch.outputs.sha }}
run: |
if [[ "${BRANCH_SHA}" != "${GITHUB_SHA}" ]]; then
echo "The release branch tip differs from the dispatched checkpoint; refusing to create a release for a moved source." >&2
exit 1
fi
- name: Check out release controller and Git history
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Set up Node.js for checkpoint validation
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
package-manager-cache: false
- name: Install checkpoint validation dependencies
run: npm ci
- name: Validate checkpoint version and ancestry before release creation
env:
RELEASE_PHASE: ${{ inputs.phase }}
RELEASE_BRANCH: ${{ inputs.release_branch }}
SOURCE_SHA: ${{ steps.branch.outputs.sha }}
run: >-
node scripts/verify-release-checkpoint.mjs
--phase "$RELEASE_PHASE" --release-branch "$RELEASE_BRANCH"
--source-sha "$SOURCE_SHA"
- uses: googleapis/release-please-action@v4
id: release
with:
# A dedicated credential lets release-PR updates trigger CI. GitHub
# requires manual approval for pull_request events from GITHUB_TOKEN.
# 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: ${{ github.ref_name }}
target-branch: ${{ inputs.release_branch }}
skip-github-release: ${{ inputs.phase == 'prepare' }}
skip-github-pull-request: ${{ inputs.phase == 'publish' }}
- name: Bind publication to the triggering checkpoint
if: steps.release.outputs.release_created == 'true'
- name: Require a merged release PR for publication
if: inputs.phase == 'publish' && steps.release.outputs.release_created != 'true'
run: |
echo "No new release was created. Merge the reviewed release PR into its frozen branch before publishing. For a partial publication, re-run failed jobs in the original run." >&2
exit 1
- name: Bind publication to the dispatched checkpoint
if: inputs.phase == 'publish' && steps.release.outputs.release_created == 'true'
shell: bash
env:
RELEASE_SHA: ${{ steps.release.outputs.sha }}
run: |
if [[ "${RELEASE_SHA}" != "${GITHUB_SHA}" ]]; then
echo "The tagged release SHA differs from the triggering checkpoint; refusing to publish mismatched source provenance." >&2
echo "The tagged release SHA differs from the dispatched checkpoint; refusing to publish mismatched source provenance. The GitHub release already exists; do not dispatch publish again." >&2
exit 1
fi
- name: Check out release pull request
if: steps.release.outputs.prs_created == 'true'
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
uses: actions/checkout@v6
with:
ref: ${{ fromJSON(steps.release.outputs.pr).headBranchName }}
fetch-depth: 0
persist-credentials: false
- name: Set up Node.js for generated documentation
if: steps.release.outputs.prs_created == 'true'
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
package-manager-cache: false
- name: Install dependencies
if: steps.release.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.release.outputs.prs_created == 'true'
env:
RELEASE_BRANCH: ${{ inputs.release_branch }}
SOURCE_SHA: ${{ steps.branch.outputs.sha }}
run: >-
node scripts/verify-release-checkpoint.mjs
--phase prepare --stage candidate --release-branch "$RELEASE_BRANCH"
--source-sha "$SOURCE_SHA"
- name: Regenerate release documentation
if: steps.release.outputs.prs_created == 'true'
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
run: npm run docs:generate
- name: Commit canonical release catalog
if: steps.release.outputs.prs_created == 'true'
shell: bash
env:
# Use the same identity for the follow-up commit so it also triggers CI.
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
RELEASE_PR_JSON: ${{ steps.release.outputs.pr }}
run: |
release_pr_branch="$(node --input-type=module -e '
const value = JSON.parse(process.env.RELEASE_PR_JSON ?? "");
if (
typeof value !== "object" ||
value === null ||
typeof value.headBranchName !== "string" ||
value.headBranchName.length === 0
) {
process.exit(1);
}
process.stdout.write(value.headBranchName);
')"
changed_paths="$(git diff --name-only)"
if [[ -z "${changed_paths}" ]]; then
echo "Release documentation is already canonical."
exit 0
fi
if [[ "${changed_paths}" != "docs/product-catalog.json" ]]; then
echo "docs:generate changed unexpected tracked paths:" >&2
printf '%s\n' "${changed_paths}" >&2
exit 1
fi
git diff --check
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -- docs/product-catalog.json
git commit -m "chore: normalize release product catalog"
gh auth setup-git
git push origin "HEAD:${release_pr_branch}"
- name: Validate generated release documentation
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
run: npm run docs:check && git diff --exit-code
publish:
name: Publish to npm
needs: release-please
if: needs.release-please.outputs.release_created == 'true'
if: inputs.phase == 'publish' && needs.release-please.outputs.release_created == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
@@ -163,7 +230,8 @@ jobs:
if [ "${published_version}" = "\"${version}\"" ]; then
echo "rea-agents@${version} is already published; verifying the existing package."
else
npm publish --access public
tag="$(node scripts/release-npm-tag.mjs "${version}")"
npm publish --access public --tag "${tag}"
fi
- name: Verify published CLI from npm
@@ -173,28 +241,32 @@ jobs:
verification_root="$(mktemp -d)"
trap 'rm -rf -- "${verification_root}"' EXIT
cd "${verification_root}"
for attempt in $(seq 1 80); do
max_attempts=120
sleep_seconds=15
total_window_minutes=$((max_attempts * sleep_seconds / 60))
for attempt in $(seq 1 "${max_attempts}"); do
published_version="$(npm view --prefer-online "rea-agents@${version}" version --json 2>/dev/null || true)"
if [ "${published_version}" = "\"${version}\"" ]; then
echo "rea-agents@${version} is visible on npm after attempt ${attempt}; verifying the published CLI."
echo "rea-agents@${version} is visible on npm after attempt ${attempt}/${max_attempts}; verifying the published CLI."
npm exec --yes --package="rea-agents@${version}" -- rea --help
npm exec --yes --package="rea-agents@${version}" -- rea --llms
node "${GITHUB_WORKSPACE}/scripts/verify-published-package.mjs" "${version}"
exit 0
else
echo "rea-agents@${version} is not visible on npm yet (attempt ${attempt}/80; up to 20 minutes)" >&2
echo "rea-agents@${version} is not visible on npm yet (attempt ${attempt}/${max_attempts}; up to ${total_window_minutes} minutes)" >&2
fi
if [ "${attempt}" -lt 80 ]; then
sleep 15
if [ "${attempt}" -lt "${max_attempts}" ]; then
sleep "${sleep_seconds}"
fi
done
echo "rea-agents@${version} did not become visible on npm within 20 minutes" >&2
echo "rea-agents@${version} did not become visible on npm within ${total_window_minutes} minutes (${max_attempts} attempts × ${sleep_seconds}s)" >&2
echo "This indicates npm registry propagation delay, not a CLI verification failure." >&2
exit 1
publish-mcp:
name: Publish to MCP Registry
needs: [release-please, publish]
if: needs.release-please.outputs.release_created == 'true' && needs.publish.result == 'success'
if: inputs.phase == 'publish' && needs.release-please.outputs.release_created == 'true' && needs.publish.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
+1 -1
View File
@@ -2,7 +2,7 @@ name: Website checks
on:
pull_request:
branches: [main]
branches: [main, "release/*"]
paths:
- "website/**"
- "scripts/verify-website.py"
+18
View File
@@ -38,6 +38,7 @@ test-results.xml
.turbo/
.cache/
docs/.vitepress/cache/
docs/.vitepress/dist/
# OS
.DS_Store
@@ -48,3 +49,20 @@ Thumbs.db
# Generated website download
/website/public/examples/notes-example.zip
# Local Cloudflare development state and credentials
/website/.wrangler/
/website/.dev.vars*
/website/.env*
!/website/.dev.vars.example
!/website/.env*.example
# Build-generated MCP tool catalog (regenerated by `npm run build`; never commit)
src/generatedMcpToolCatalog.ts
# Derived catalogs, portable conformance projections, and packaged skill bundles
# (regenerated from this checkout by `npm run build:cached`; never commit)
/skills/
/docs/public/product-catalog.json
/docs/verification/managed-conformance-manifest.json
/docs/verification/managed-conformance-ledger.json
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "5.0.0"
".": "6.0.0"
}
+16 -9
View File
@@ -2,7 +2,7 @@
## Product Direction
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper, the bring-your-own Ghidra adapter, and the bring-your-own IDA MCP adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86-64 PE applications; on Linux and macOS it supplies inventory, function analysis, and atomic function annotation edits in an ephemeral database, without modifying executable bytes or controlling a GUI. Windows P0 has no mutation authority. IDA adapts upstream legacy attached GUI and modern headless database-supervisor profiles for live read-only analysis; initial real verification covers Windows, and an attached GUI database is never saved or closed. Keep provider-specific code out of the domain and application layers.
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper, the bring-your-own Ghidra adapter, and the bring-your-own IDA MCP adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86 and x86-64 PE applications; on Linux and macOS it supplies inventory, function analysis, and atomic function annotation edits in an ephemeral database, without modifying executable bytes or controlling a GUI. Windows P0 has no mutation authority. IDA adapts upstream legacy attached GUI and modern headless database-supervisor profiles for live read-only analysis; initial real verification covers Windows, and an attached GUI database is never saved or closed. Keep provider-specific code out of the domain and application layers.
Prioritize:
@@ -21,10 +21,11 @@ REA is a layered ESM TypeScript application. Dependencies flow inward from pure
- `src/domain/` owns pure provider-neutral semantics; `src/contracts/` owns caller-visible schemas and the canonical tool inventory.
- `src/hopper/`, `src/ghidra/`, `src/ida/`, `src/browser/`, `src/inspector/`, `src/native/`, `src/artifacts/`, and `src/dotnet/` own provider-specific boundaries. Keep provider protocols out of domain and application code.
- `src/application/` composes shared CLI/MCP workflows; `src/server/` translates MCP requests; `src/cli.ts` and `src/main.ts` are the CLI and MCP entry points.
- `src/process/` owns shared process lifecycle primitives, not provider wire protocols. `bridge/` contains provider-side adapters.
- `src/application/` composes shared CLI/MCP workflows, including Evidence provenance, unknowns, and eligible snapshot bindings; `src/server/` translates MCP requests; `src/cli.ts` and `src/main.ts` are the CLI and MCP entry points.
- `src/process/` owns shared process lifecycle primitives, not provider wire protocols. Reuse its supervision and identity primitives before adding provider-local lifecycle code; a PID and executable pathname alone do not establish ownership after exit or reuse. `bridge/` contains provider-side adapters.
- `tests/` contains unit, composition, boundary, acceptance, and conformance tests. `scripts/verify-*` and capability directories under `scripts/verify/` contain real-toolchain checks.
- `docs/product-catalog.json` is generated. Update its source contracts and regenerate it; do not edit it directly.
- `docs/public/product-catalog.json`, `docs/verification/managed-conformance-*.json`, and `skills/` are ignored build outputs. Update source contracts and authored instructions in `skill-src/`, then run `npm run build:cached`; never commit derived catalog digests or portable conformance projections.
- `src/generatedMcpToolCatalog.ts` is build-generated and gitignored. Never commit it; resolve any trace of it in merges by deleting it and running `npm run build:cached`.
## Build, Test, and Development Commands
@@ -35,7 +36,7 @@ REA is a layered ESM TypeScript application. Dependencies flow inward from pure
- `npm run check:changed`: run cached static checks and source tests affected since the branch merge base (default `origin/main`).
- `npm run check:fast`: run cached typecheck and lint checks.
- `npm run check:pr`: opt into the complete local deterministic gate and generated-document checks for broad changes; CI owns full coverage. Routine iterations need focused tests and relevant checks, not the whole gate each time.
- `npm run docs:check`: check committed generated documents; `npm run docs:generate` regenerates them.
- `npm run docs:check`: build and validate generated documents for the current checkout; `npm run docs:generate` regenerates them. CI retains ignored outputs as artifacts and does not push snapshot commits to feature branches.
- For provider-dependent changes, see [docs/testing.md](docs/testing.md) and run the matching real-provider verification.
- Keep each verification lane's prerequisites limited to the claim it checks. Use host-native fixtures for host/provider acceptance; put optional cross-target formats and their external toolchains in a separate lane. Preflight required commands and report the missing dependency and lane clearly.
@@ -53,9 +54,13 @@ Use ESM TypeScript, two-space indentation, and the committed Oxfmt configuration
Treat a boundary as a contract between the producer's actual representation and the consumer's required meaning. When implementing or auditing a boundary, trace the value through parsing, normalization, authorization, serialization, and the CLI/MCP result. Establish affected callers from their code paths; similar tool names or workflows do not prove that they share a schema or failure mode.
Keep portable evidence and scenario validation distinct from host-native execution checks. Absolute filesystem paths, file URLs, and HTTP paths have different semantics; do not substitute one platform's syntax for the domain concept. Interpret provider metadata according to its documented or observed producer behavior. When a transformation loses information, preserve the reported value and an explicit unknown rather than guessing a canonical identity.
When changing boundary behavior, inspect adjacent input representations, failure paths, and affected callers, and correct the underlying assumption across those cases.
Preserve meaningful failure reasons through application and adapter layers. Malformed input is distinct from an unsupported target, unavailable provider, or host operating-system permission denial. Diagnostics should identify the failed constraint and the target or lifecycle request it applies to. Recovery advice must address that reason and point to an available workflow; generic catches must not erase actionable validation details.
Keep portable evidence and scenario validation distinct from host-native execution checks. Absolute filesystem paths, file URLs, and HTTP paths have different semantics; do not substitute one platform's syntax for the domain concept. Interpret provider metadata according to its documented or observed producer behavior. Keep values used for identity, provenance, matching, and path resolution separate from display formatting. When normalization loses information, preserve the source value and an explicit unknown; display placeholders must not feed back into lookup or selection.
Preserve meaningful failure reasons through application and adapter layers. Malformed input is distinct from an unsupported target, unavailable provider, or host operating-system permission denial. Diagnostics should identify the failed constraint and the target or lifecycle request it applies to. Recovery advice must address that reason and point to an available workflow; generic catches must not erase actionable validation details. Preserve collected observations on execution failure independently of whether cleanup succeeds.
Preserve partial native facts. A shared schema must not require one provider's full metadata before another provider can report the facts it observes. Keep observed classifications and endpoints alongside explicit unknowns for unsupported flags; never invent missing flags or discard known facts to satisfy an all-or-nothing availability shape. Before claiming a native API lacks a capability, inspect the installed API and probe representative producer objects. Exercise the resulting partial representation through CLI, MCP, and composed results such as function dossiers.
Leave meaningful target, action, capture, and output choices to the agent. A selected operation already expresses intent; do not require approval booleans or repeated permission declarations. Trace each setting to its consumer: remove ignored options and single-value confirmations, derive built-in lifecycle behavior, and supply defaults for omitted optional metadata. Report actual effects and limitations where they help interpret results rather than asking callers to restate them.
@@ -79,7 +84,7 @@ Let agents compose experiments with ordinary commands, scripts, and local fixtur
- Use **observe/capture** only when runtime activity is required, and declare authority and lifecycle effects in the contract.
- Extend an existing tool when intent and result contract are unchanged. Add a tool for a distinct analyst outcome or materially different authority.
- Keep caller-facing names and results provider-neutral. Put engine-specific behavior in provider adapters and report each provider's exact coverage.
- Keep results complete by default. Add a limit only when it follows from a real format, protocol, authority, or resource-safety constraint; explain truncation and unsupported facets. Keep observed, derived, inferred, and unknown results distinct.
- Keep results complete by default. Add a limit only when it follows from a real format, protocol, authority, or resource-safety constraint; explain truncation and unsupported facets. Account for representation expansion and products of independently bounded dimensions before allocating or retaining output. Derive facet completeness from what was examined and exhausted, not an empty failure list. Keep observed, derived, inferred, and unknown results distinct.
- Include artifact identity, source locations, Evidence references, actionable errors, and relevant limitations when they affect conclusions. Return the evidence needed for the next analysis inline rather than requiring a resource or identifier lookup.
- Implement shared application workflows behind CLI and MCP adapters. Update canonical contracts and generated catalog together.
@@ -87,6 +92,8 @@ See [docs/tool-design.md](docs/tool-design.md) for the design checklist. When us
## Testing Guidelines
Prioritize real end-to-end workflows, boundary integration, and golden producer data. Keep a focused module test when it covers a distinct failure or semantic case absent from those workflows; test paths do not establish depth. See [docs/testing.md](docs/testing.md) for pruning and classification rules.
Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use fake launcher/socket seams; MCP tests connect with the client SDK version pinned in `package.json`. Preserve the canonical tool inventory defined by `TOOL_CONTRACTS` and verified through `CATALOG_IDENTITY` and generated product metadata. Cover malformed input, cancellation, lifecycle cleanup, and actual format, protocol, host-permission, and target-identity boundaries. Do not add tests that merely freeze arbitrary caps or prescribed call sequences. Real Hopper, Ghidra, browser, managed conformance, and any real managed-tool claims cannot be replaced by mocks; use the corresponding `verify:*` command.
Keep tool catalogs complete and self-describing; prefer capability- and session-scoped availability over schema truncation. Serialized bytes alone do not measure agent usability or model context cost.
@@ -99,7 +106,7 @@ Development requires Node.js 24.18.0 and npm 11.16.0 (`.nvmrc` and `packageManag
## Commit & Pull Request Guidelines
Use Conventional Commit subjects because Release Please derives versions and changelogs from them. Examples: `feat: add historical source import`, `fix(process): stop timers after exit`, and `docs: update architecture`. Use `!` or a `BREAKING CHANGE:` footer for breaking changes. Pull request titles must follow the same format because squash merges use the title as the release commit. Pull requests should describe contract or behavior changes, list verification commands, link issues, and include sanitized MCP examples when schemas change. State whether real Hopper/Ghidra verification was performed. Never commit binaries, Hopper or Ghidra project documents, credentials, `dist/`, `node_modules/`, or local planning artifacts (e.g. `.codex/`).
Use Conventional Commit subjects because Release Please derives changelogs from them. Release Please uses `always-bump-minor`: every release increments minor, including breaking changes. Examples: `feat: add historical source import`, `fix(process): stop timers after exit`, and `docs: update architecture`. Use `!` or a `BREAKING CHANGE:` footer for breaking changes. Pull request titles must follow the same format because squash merges use the title as the release commit. Pull requests should describe contract or behavior changes, list verification commands, link issues, and include sanitized MCP examples when schemas change. State whether real Hopper/Ghidra verification was performed. Never commit binaries, Hopper or Ghidra project documents, credentials, `dist/`, `node_modules/`, or local planning artifacts (e.g. `.codex/`).
<!-- BEGIN:turborepo-agent-rules -->
+99
View File
@@ -1,5 +1,104 @@
# Changelog
## [6.0.0](https://github.com/morluto/rea/compare/rea-agents-5.0.0...rea-agents-6.0.0) (2026-10-08)
### ⚠ BREAKING CHANGES
* **contracts:** MCP filesystem inputs now require absolute host paths, including target/snapshot paths, evidence bundle paths, managed and firmware inputs, browser/Electron launch paths and expected server paths. Calls such as `open_binary({"path":"./app"})` must send an absolute path such as `/home/analyst/app` or `C:\analysis\app`. Optional paths remain optional. CLI operator-relative paths continue to work. See [#948](https://github.com/morluto/rea/pull/948) and [#1004](https://github.com/morluto/rea/pull/1004).
### Features
* **artifacts:** trace Mach-O dylib resolution in bundles ([78d594c](https://github.com/morluto/rea/commit/78d594cacd376725f833a99402069c103aae9aac))
* **browser:** inspect historical web network captures ([#992](https://github.com/morluto/rea/issues/992)) ([2bf3663](https://github.com/morluto/rea/commit/2bf3663f654386c9fd8c1437c49659df97bd4697))
* **evm:** inspect offline bytecode interfaces through EVMole ([#1051](https://github.com/morluto/rea/issues/1051)) ([7af7bc8](https://github.com/morluto/rea/commit/7af7bc88ce0e8f084eaa0db1e8f834d495440fd9))
* **ghidra:** support native x86 PE applications on Windows ([#1030](https://github.com/morluto/rea/issues/1030)) ([c6dad93](https://github.com/morluto/rea/commit/c6dad932084032bcb32acc61e8066a7784497710))
* **native:** inspect offline ELF layout through pwntools ([#1037](https://github.com/morluto/rea/issues/1037)) ([4fc6565](https://github.com/morluto/rea/commit/4fc6565b0ffb36d7105b3524e08d0c5d603ae07d))
* **native:** inspect recorded crashes through pwntools and pwndbg ([#1054](https://github.com/morluto/rea/issues/1054)) ([163ee09](https://github.com/morluto/rea/commit/163ee09a7425479385e0ed0b5d74043e2587eab2))
* **native:** observe function and Objective-C method calls under LLDB ([1d13cef](https://github.com/morluto/rea/commit/1d13cef5f46f5e07f594bde7b1a4725f189b9a5e))
### Bug Fixes
* **dotnet:** validate managed metadata and CIL boundaries ([#938](https://github.com/morluto/rea/pull/938)) ([bda4ada](https://github.com/morluto/rea/commit/bda4ada7fb6bd7dd70a444bb3c57e34f7f5cc00b))
* **build:** clarify generated-file check hint and drop redundant catalog step ([#998](https://github.com/morluto/rea/pull/998)) ([6f0ef16](https://github.com/morluto/rea/commit/6f0ef160501ea3b24dfa2939dfcd3eea0dc13ee7))
* **release:** extend npm propagation window and improve diagnostics ([#943](https://github.com/morluto/rea/pull/943)) ([06b047a](https://github.com/morluto/rea/commit/06b047a0929369d5b8635b3651da115c8fcd53a7))
* **contracts:** require absolute local paths for session filesystem inputs ([#948](https://github.com/morluto/rea/pull/948)) ([5b172b1](https://github.com/morluto/rea/commit/5b172b1042fc09a4e12d956018f15d86cf397ffe))
* **ci:** generate source catalog before shard tests ([#1002](https://github.com/morluto/rea/pull/1002)) ([eb8f649](https://github.com/morluto/rea/commit/eb8f6491a88bfa79d13f7923e4ef94d791f6ba4d))
* **contracts:** require absolute local paths for remaining caller-supplied file inputs ([#1004](https://github.com/morluto/rea/pull/1004)) ([cbc0dd1](https://github.com/morluto/rea/commit/cbc0dd1e7ca36fa3504a9ea8eae303a0876b176b))
* **cli:** classify ENOTDIR JSON input failures ([#1000](https://github.com/morluto/rea/pull/1000)) ([a6132be](https://github.com/morluto/rea/commit/a6132be0546d6a9e3fa83426c8b59b1280b3e5bd))
* **browser:** preserve authorized redirect hops ([#986](https://github.com/morluto/rea/pull/986)) ([ae0f725](https://github.com/morluto/rea/commit/ae0f725fad368730c6198d49716ccc410bca0536))
* **mcp:** reject unknown MCP tool inputs ([#975](https://github.com/morluto/rea/pull/975)) ([fe64fd6](https://github.com/morluto/rea/commit/fe64fd65d93eff202ad90145544dc9ff84651974))
* **release:** publish from explicit source checkpoints ([#1003](https://github.com/morluto/rea/pull/1003)) ([64c7188](https://github.com/morluto/rea/commit/64c71880ae4f7bc658ffce46eb729c4141dbdf19))
* **release:** publish from explicit source checkpoints ([#1006](https://github.com/morluto/rea/pull/1006)) ([b4fc0ec](https://github.com/morluto/rea/commit/b4fc0ecaa1f6b912dc27880e6b6127ace47abc2e))
* **javascript:** bound semantic node projection for resource safety ([#966](https://github.com/morluto/rea/pull/966)) ([96a46ad](https://github.com/morluto/rea/commit/96a46ada0a36f0349f2b0fc20b4c932401b4ad84))
* **ci:** avoid slow Azure archive for the native cross compiler ([#1016](https://github.com/morluto/rea/issues/1016)) ([51956c1](https://github.com/morluto/rea/commit/51956c1a49a7dc9ed70a6fba480a670a7e7b2b9b))
* **ci:** decouple stdio smoke tests from Linux display probes ([#1014](https://github.com/morluto/rea/issues/1014)) ([93731e1](https://github.com/morluto/rea/commit/93731e17ddea76db86dfa84bb54536dcf8ebfa0b))
* **cli:** stream large JavaScript JSON results ([#1024](https://github.com/morluto/rea/issues/1024)) ([0e25bef](https://github.com/morluto/rea/commit/0e25befe78f220691abe5d0291cc04a75c9cc877))
* **javascript:** complete large-ASAR analysis with file-local projection ([#1055](https://github.com/morluto/rea/issues/1055)) ([b9fb438](https://github.com/morluto/rea/commit/b9fb43883570bef7cb29fd16f27c25d5a7e5bb54))
* **javascript:** recognize digits in artifact URI schemes ([#1045](https://github.com/morluto/rea/issues/1045)) ([03424ae](https://github.com/morluto/rea/commit/03424ae91ef22efd57db1bdde9379a3ac450b8f7))
* **javascript:** reconstruct HTML script loads accurately ([#1042](https://github.com/morluto/rea/issues/1042)) ([1154f2f](https://github.com/morluto/rea/commit/1154f2f03c0b0bb6cb6c5da09b8d8e1c837febc4))
* **javascript:** report application analysis progress on the CLI ([#1008](https://github.com/morluto/rea/issues/1008)) ([7b18c58](https://github.com/morluto/rea/commit/7b18c581409848077de7a9cb6d7d1a0b91a02e12))
* **javascript:** resolve module URLs and reject invalid exports targets ([#1031](https://github.com/morluto/rea/issues/1031)) ([8d5c88f](https://github.com/morluto/rea/commit/8d5c88f17cde4ab4fa54c5142f0a45e612619a53))
* **javascript:** resolve root package exports arrays ([#1046](https://github.com/morluto/rea/issues/1046)) ([53409ab](https://github.com/morluto/rea/commit/53409ab1968d60c77f98feedefe237a08859a95c))
* **mcp:** advertise JSON values without recursive schemas ([#995](https://github.com/morluto/rea/issues/995)) ([7425f70](https://github.com/morluto/rea/commit/7425f70fd0247a7ea7192deb959fd6f48ba83a21)), closes [#919](https://github.com/morluto/rea/issues/919)
* **mcp:** bound capture comparison input schema depth ([#1005](https://github.com/morluto/rea/issues/1005)) ([6b55de0](https://github.com/morluto/rea/commit/6b55de017badc9066642e1ff2561eb1da0ff93f8))
* **process:** preserve partial evidence and verify detached cleanup ([#1011](https://github.com/morluto/rea/issues/1011)) ([63cf28f](https://github.com/morluto/rea/commit/63cf28fd8f8acaf656e993300a2a24280b2fd692))
* **release:** support built-in credentials and refresh release boundary ([6a278a6](https://github.com/morluto/rea/commit/6a278a68dd4fa69b9153697ecb7bba7de4c55c32))
* **release:** support default credentials and record 5.0.0 boundary ([#1012](https://github.com/morluto/rea/issues/1012)) ([6a278a6](https://github.com/morluto/rea/commit/6a278a68dd4fa69b9153697ecb7bba7de4c55c32))
* **release:** sync 5.0.0 checkpoint back to main ([966cca8](https://github.com/morluto/rea/commit/966cca8efe5ed08b3285cd06409688d871eb5e6b))
* **snapshot:** replay validated workflow evidence ([#984](https://github.com/morluto/rea/issues/984)) ([1d62e54](https://github.com/morluto/rea/commit/1d62e54e1f976e64130e4e9b651c3180aa4fa727))
* **windows:** allow Java to read retained target snapshots ([#1027](https://github.com/morluto/rea/issues/1027)) ([28d5c39](https://github.com/morluto/rea/commit/28d5c397c83f901e4086083868041711371ad32a))
### Performance Improvements
* **artifacts:** replace quadratic ZIP overlap checks ([#1013](https://github.com/morluto/rea/issues/1013)) ([76e7e51](https://github.com/morluto/rea/commit/76e7e510bb2d7d37728ca50749d524beb5890cf8))
* **javascript:** cache canonical export comparison sort keys ([#1028](https://github.com/morluto/rea/issues/1028)) ([63ba742](https://github.com/morluto/rea/commit/63ba7424c0ab69b94397e8913f8e231de111583f))
### Code Refactoring
* **build:** generate MCP tool catalog at build time instead of committing it ([#997](https://github.com/morluto/rea/pull/997)) ([35a4e18](https://github.com/morluto/rea/commit/35a4e18d3028014e24d2e209ec99a590fde63152))
* **process:** separate capture implementation from analyst workflows ([#988](https://github.com/morluto/rea/pull/988)) ([6aa9afb](https://github.com/morluto/rea/commit/6aa9afb5493a97801eb1126a0dcf11f9705165db))
* **artifacts:** move inventory and extraction into provider owners ([#1032](https://github.com/morluto/rea/issues/1032)) ([6e4535c](https://github.com/morluto/rea/commit/6e4535c1011f1982e9ab07b7ebcd600c19cd494d))
* remove abandoned test-only analysis scaffolding ([c93422a](https://github.com/morluto/rea/commit/c93422a898fdbedb7d28709144b4337a2669440b))
### Documentation
* improve Korean wording and particles ([#1047](https://github.com/morluto/rea/issues/1047)) ([c5d165b](https://github.com/morluto/rea/commit/c5d165bd03fed95a0b0105661d3ba7e0efbfa449))
* simplify the English README and organize guides ([#1043](https://github.com/morluto/rea/issues/1043)) ([28f10cf](https://github.com/morluto/rea/commit/28f10cf2bc7b7c590700b50d47072015f3e3c66e))
### Tests
* **process:** wait for observable snapshot ctime changes ([#999](https://github.com/morluto/rea/pull/999)) ([161ed91](https://github.com/morluto/rea/commit/161ed9157134d5c3b0fbae19ceb756ac58a26c04))
* **browser:** consolidate CDP workflows and colocate parser coverage ([2682904](https://github.com/morluto/rea/commit/26829045d0f0e744bb60d6babc2dc3312f4bfa53))
* **ci:** stabilize browser source identity fixture delivery ([#1023](https://github.com/morluto/rea/issues/1023)) ([ed725da](https://github.com/morluto/rea/commit/ed725dace4a689e4e5c804dc6bf06c7fc663c7c9))
* **cli:** reduce repeated startup and filesystem setup ([d88d6c1](https://github.com/morluto/rea/commit/d88d6c1897740ccbd6c57f4ba01861fc751fb3a8))
* consolidate service and process fixtures ([f8a53a8](https://github.com/morluto/rea/commit/f8a53a8c065f1ccbdaa051a4e1ef267a2047d759))
* **javascript:** consolidate source analysis and export regressions ([f517a5a](https://github.com/morluto/rea/commit/f517a5a1d7c0766ed50f8dfd5ee69e65525ac744))
* **javascript:** cover plain long-line loader identities ([#1025](https://github.com/morluto/rea/issues/1025)) ([984fc27](https://github.com/morluto/rea/commit/984fc27ce19d57f708bdae688a258cf8663cdea6))
* **mcp:** consolidate SDK schema checks and scenario ports ([9c3479e](https://github.com/morluto/rea/commit/9c3479ed95100d8ee5ed4d9158a4550e4ac2a752))
* **providers:** verify framing through client socket boundaries ([1fd1989](https://github.com/morluto/rea/commit/1fd1989d342375a01b6d14b282ad58001b7d1800))
* prune redundant conformance checks and document test priorities ([aafdf8a](https://github.com/morluto/rea/commit/aafdf8a01b42fb6fd927c0f2a66e758e1a510abc))
## [5.0.0](https://github.com/morluto/rea/compare/rea-agents-4.1.0...rea-agents-5.0.0) (2026-10-07)
+39 -15
View File
@@ -28,19 +28,25 @@ Keep dependencies flowing inward through the existing domain, contracts, provide
## Documentation website
The VitePress site uses the Markdown files in `docs/` and deploys to
<https://morluto.github.io/rea/>. Run `npm run docs:dev` for live editing,
`npm run docs:build` to check the production build and links, and
`npm run docs:preview` to preview that build at `/rea/`.
The VitePress site uses the Markdown files in `docs/`. Run `npm run docs:dev`
for live editing, `npm run docs:build` to check the production build and
links, and `npm run docs:preview` to preview that build at `/rea/`.
Site navigation lives in `docs/.vitepress/config.ts`. Keep links to guides
relative so they work on GitHub and the website; link to repository files
outside `docs/` using their full GitHub URLs. Generated reference documents
still use `npm run docs:generate`; `docs:build` only builds the website.
use `npm run docs:generate`; `docs:build` generates them before building the website.
The documentation workflow builds pull requests and deploys changes on `main`.
The repository's **Settings → Pages → Build and deployment → Source** must be
set to **GitHub Actions** before the first deployment.
Pull requests run `npm run docs:check`. `.github/workflows/pages.yml` is a
manual VitePress build and does not publish. The public site at
<https://rea.tools/> serves `website/public/` through Cloudflare Workers. See
the [website README](website/README.md#cloudflare-workers) for previews and
manual deployments.
The separate GitHub Pages host uses only the manual
`.github/workflows/website-pages.yml` workflow on `main`. The repository's
**Settings → Pages → Build and deployment → Source** must be set to
**GitHub Actions** before deploying there.
## Development feedback and PR verification
@@ -72,8 +78,24 @@ require the matching real-provider `verify:*` lane.
Formatting uses Oxfmt and the committed `.oxfmtrc.json`; generated sources use
the same configuration. Pre-commit formats and lints staged files; pre-push runs
`check:fast`.
`docs:check` checks committed generated metadata. `docs:generate` regenerates
those files, and the docs CI lane checks them.
`docs:check` builds and validates generated metadata for the current checkout.
The product catalog (`docs/public/product-catalog.json`), portable managed
conformance projections (`docs/verification/managed-conformance-*.json`), and
packaged skill (`skills/`) are ignored build outputs. Edit skill instructions
and references in `skill-src/`; the build adds catalog-dependent metadata to
the packaged copy without rewriting authored files. The generated manifest
commits to that exact packaged skill bundle. It is a portable projection of
the deterministic managed verifier, not a record of optional real-provider runs.
CI validates these outputs and retains them as artifacts instead of pushing
generated-only commits onto feature branches. Reviewed source metadata such as
`src/generatedPackageMetadata.ts` and `docs/error-contract.schema.json` remains
tracked and checked for freshness. Do not commit ignored generated outputs.
The build-generated product catalog contains documented facts and their provider
identity, rather than full runtime schema hashes. The managed skill contains
instructions and inventory metadata; doctor compares its installed files with
the canonical bundle. Schema-only fixes should not change these outputs or the
skill commitment in the conformance manifest. Runtime schema identity remains
available through doctor and `binary_session`.
Real-provider execution remains uncached; deterministic builds use Turbo.
Local `npm test` runs every deterministic Vitest project without coverage or
@@ -120,11 +142,13 @@ Describe the behavior change and verification performed in the pull request. Nev
## Maintainer release checklist
Use the [checkpoint release guide](docs/releasing.md). Releases start from an
explicit `release/VERSION` branch cut at a recorded commit. Pushing the branch prepares
the bot PR. Wait for its exact-head CI and review; merging into that frozen
branch triggers publication through the official Release workflow. Main
pushes do not update release PRs or publish packages. Both publishers build
Use the [checkpoint release guide](docs/releasing.md). Main pushes automatically
refresh a release proposal with the next version and changelog. Keep this preview
PR unmerged until it is superseded by the published release's metadata sync.
Publication starts from an explicit `release/VERSION` branch cut at a recorded
application commit. Manually prepare its bot PR, wait for its exact-head CI and
review, merge into that frozen branch, and manually publish through the official
Release workflow. Main pushes cannot publish packages. Both publishers build
the exact SHA tagged by Release Please.
Keep new implementation commits on main for the next release. The workflow
+200 -689
View File
@@ -10,16 +10,17 @@
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#tool-catalog-for-investigation)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
[![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
[![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM)
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
**[Website](https://morluto.github.io/rea/) · [Guides](https://morluto.github.io/rea/guides/) · [Showcases](https://morluto.github.io/rea/showcase/)**
**[Website](https://rea.tools/) · [Guides](https://rea.tools/guides/) · [Showcases](https://rea.tools/showcase/)**
[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [Tool catalog](#tool-catalog-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
[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>
@@ -53,767 +54,273 @@ REA connects your agent to tools for inspecting native binaries, JavaScript and
Setup registers REA with your agent and installs matching workflow instructions. Native analysis can use an existing Hopper or Ghidra installation; setup can optionally install Hopper with approval. Static JavaScript analysis needs neither engine.
> **[Visit the REA website](https://rea.tools/)** for setup instructions, illustrated guides, and real case studies.
## Quick start
### Run setup (recommended)
### Set up your agent
Set up REA with your agent:
With Node.js and npm installed, run:
```bash
npx rea-agents setup
```
Choose which supported agents should use REA, then review the exact paths and changes before approving. Existing REA registrations are selected by default; newly detected agents are available to select, but detection alone does not select them. Setup adds MCP access and REA's guided workflow for selected agents. Hopper is a separate optional choice with its own consent. Setup can also record an existing Ghidra installation.
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 shows its changes before applying them and backs up existing configuration. See [Installation and setup](docs/installation.md) for requirements and setup options.
Setup supports Claude Code, Codex, Cursor, Gemini CLI, Grok Build, Grok Bot and
[other agents](docs/installation.md#supported-agents). See
[installation and setup](docs/installation.md) for provider configuration and
manual MCP registration.
### AI Coding Assistants (optional)
Add the skill to your AI coding assistant for richer context:
```bash
npx skills add morluto/rea --skill reverse-engineer-anything
```
The skill provides REA's investigation workflow. Run setup above to connect REA to your agent and configure analysis tools. Setup already installs a version-matched skill by default; this command installs the repository version.
### With an agent (recommended)
After setup, restart your agent and [describe the app or feature](#just-ask-your-agent) you want to understand. Hopper can run in demo mode; if it shows a first-run prompt, choose the demo or enter an existing license.
REA supports Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, Command Code, and VS Code. Existing REA registrations are selected by default during setup; other detected agents remain unselected until chosen. Other agents can use the [manual MCP configuration](#manual-mcp-configuration).
### First result from the terminal
For your extracted JavaScript/Electron application tree or ASAR, run:
```bash
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
Replace the path with your target (for example, `"D:/apps/example"` on Windows).
This returns inline Evidence, recovered graph, limitations, and unknowns without
MCP setup, Hopper, Ghidra, or executing the application. For a native app, configure
its engine first, then use `analyze` with that app's path. Run `doctor` when you
need diagnosis; it is not a prerequisite for each analysis.
### Check readiness for the task at hand
`rea doctor` without options is an audit of the whole integration. It checks
every detected agent registration, the installed skill, and every optional
analysis engine, so it can report `healthy: false` while your current task works.
Choose a readiness scope for the work you are doing:
| Task | Readiness check |
| ----------------------------------- | -------------------------------------------------------------------------------------------- |
| Static JavaScript/Electron analysis | None. Run `analyze-javascript-application` directly. |
| Troubleshoot one analysis engine | `rea doctor --provider ghidra --json` (or `hopper`, `ida`) |
| Check one agent's MCP registration | `rea doctor --client codex --json` (see [client IDs](docs/installation.md#supported-agents)) |
| Check the installed skill | `rea doctor --skill --json` |
A scoped report has `scope.mode: "explicit"`. Only `scope_checks` determine
`healthy` and the exit status. Everything else is listed in
`informational_checks`; you don't need to fix it for this task. For example, a
missing engine you are not using needs no repair. `environment_healthy` still
summarizes the full audit. `--target` adds a target check, but without a scope
option the report remains audit-wide.
When more than one installed engine supports a native target, REA does not pick
one: opening the target fails with `code: "capability_unavailable"`,
`details.selection_reason: "ambiguous"`, and the providers in
`details.candidate_ids`. Choose one once: pass `--provider` on
the CLI or `provider_id` on `open_binary`, or set `REA_ANALYSIS_PROVIDER` as a
standing preference. An explicit selector overrides the environment variable.
The session keeps that choice and never falls back to another engine.
Recovery depends on `details.selection_reason`. For `ambiguous`, choose one of
the candidates. For `provider_unavailable`, the engine you selected needs repair,
so run `rea doctor --provider <id> --json` and follow its remediation. Neither
reason means you have to install every engine. See
[Choosing a deep-analysis provider](#choosing-a-deep-analysis-provider).
### Install the rea command
Install the command-line interface:
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
```
The installer adds `rea` to your system and starts setup when run in a terminal. It requires Node.js and npm to be installed already.
Alternatively, install with npm, then run setup:
```bash
npm install --global rea-agents
rea setup
```
Update either installation with `rea update`.
### Requirements
Static JavaScript inspection requires the Node/npm runtime only. Host and
external-tool prerequisites depend on the selected workflow; the native
provider guides describe their supported platforms.
- macOS 12 or newer
- Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, or CachyOS
- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+
- npm; REA does not require or install a particular npm version
Deep native binary analysis requires [Hopper](https://www.hopperapp.com/), [Ghidra](#ghidra-analysis-provider), or [IDA Pro](#ida-pro-analysis-provider). Hopper is separate software with its own license; its demo supports analysis with vendor-defined limits. Ghidra and IDA are bring-your-own providers.
Firmware region inspection and explicit extraction use caller-supplied Binwalk and Unblob on Linux. See [Firmware analysis](docs/firmware-analysis.md) for setup, provenance, resource limits and native handoff.
Static APK analysis uses a separately supplied headless JADX JAR and a full JDK, with no emulator or APK execution. See [Android analysis](docs/android-analysis.md) for setup, CLI/MCP operations, coverage and public test fixtures. Authenticated IPA and macOS `.app`, ZIP, or DMG inventory Evidence can be projected into bundle anatomy, such as XPC services, app extensions, login items, privileged helpers, and launchd plists, with [Apple application analysis](docs/apple-application-analysis.md).
Repository main and npm 4.1.0 include experimental Windows x64 Ghidra support for native x86-64 PE applications on local NTFS, with bundled Job Object, private-DACL, and path-admission controls. Check the [release boundary](docs/installation.md#released-package-and-main) before expecting this from an older npm package. See [Windows Ghidra P0](docs/windows-ghidra-p0.md) for prerequisites and verified scope.
If something is not working, run:
```bash
npx -y rea-agents@latest doctor
```
`doctor` checks your host, dependencies, analysis tools, and agent configuration without changing them. Use `--json` for structured diagnostics.
### Linux installation and troubleshooting
On macOS, setup can install Hopper in `~/Applications` after approval. It verifies the official download and does not need Homebrew or administrator privileges.
On supported Linux distributions, setup can install Hopper and its demo-session dependencies through your system package manager. You may see a system authorization prompt. Demo sessions use a private virtual display, leaving your desktop alone. See [Hopper installation](docs/installation.md#hopper) for download verification and platform details.
REA prefers an executable `/opt/hopper/bin/Hopper` on Linux. If it is unavailable, REA automatically checks `~/.local/share/rea/hopper/bin/Hopper`. If Hopper was installed elsewhere:
```bash
export HOPPER_LAUNCHER_PATH=/absolute/path/to/Hopper
rea doctor --json
```
If doctor reports a missing analysis engine even though the file exists, inspect shared-library resolution with:
```bash
ldd /absolute/path/to/Hopper | grep 'not found'
```
Install the missing packages and rerun `rea setup`. The Linux demo needs Xvfb, Python 3, X11, and XTEST; approved setup installs these dependencies. If you use the curl installer, add `~/.local/bin` to your shell `PATH` when needed.
REA uses `/Applications/Hopper Disassembler.app/Contents/MacOS/hopper` by default on macOS. On Linux it prefers executable `/opt/hopper/bin/Hopper`, then executable `~/.local/share/rea/hopper/bin/Hopper`, and keeps `/opt/hopper/bin/Hopper` as the diagnostic fallback if neither exists. Explicit `HOPPER_LAUNCHER_PATH` configuration always takes precedence.
### Ghidra analysis provider
Already use Ghidra? REA can connect it to your agent on Linux x64 or macOS x64/arm64. It accepts **Ghidra 12.1.x** and the **64-bit full JDK** that installation declares (`application.java.min` through `application.java.max`). Current 12.1 releases require JDK 21 or newer and set no maximum. The bridge is verified with Ghidra 12.1.4 and JDK 21. On macOS, your Ghidra installation must also include the native decompiler for your architecture.
Set the installation paths, then run setup:
```bash
export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.4_PUBLIC
export JAVA_HOME=/absolute/path/to/jdk-21 # optional when java and javac resolve from PATH
rea doctor --json
rea setup
rea providers --json
```
Setup checks the installations and saves their paths in your selected agents' configuration after approval. Ghidra and Java must already be installed; REA does not download or change them.
The adapter exposes inventory, function, memory and load-image inspection, plus atomic function annotation edits on Linux and macOS. `annotate_native_function` edits names and entry comments in the session database and returns a refreshed function dossier; executable bytes stay unchanged. Ghidra does not provide GUI controls through REA.
Opening a Ghidra target selects its provider; the first analysis query starts import and auto-analysis. That query can take longer than a client's default request deadline. See [first-query deadlines and recovery](docs/mcp-contracts.md#ghidra-first-query-deadlines-and-recovery) for an SDK example and cancellation recovery.
REA analyzes a temporary copy of the target and removes the temporary project when the session closes. Results identify what Ghidra observed and what it could not resolve. Decompilation produces pseudocode rather than the original source.
Ghidra also imports DOS MZ executables with an explicit 16-bit x86 real-mode profile. Function results include complete observed body ranges, distinguishing owned bytes from the enclosing span. See the [DOS analysis guide](docs/ghidra-dos.md) for addresses, packing, and verification boundaries.
See [optional NativeAOT metadata recovery](docs/ghidra-nativeaot.md) for the pinned
headless adapter, supported layout and existing native-tool workflow.
Windows Ghidra P0 uses bundled native controls for its read-only native x86-64 PE boundary on local NTFS; see the [Windows Ghidra P0 guide](docs/windows-ghidra-p0.md). See [Ghidra installation](docs/installation.md#ghidra), [provider evaluation](docs/provider-evaluation.md), and [testing](docs/testing.md) for configuration details, coverage, and real-provider verification.
To remove only REA-owned MCP registrations and the managed skill:
```bash
rea uninstall
rea uninstall --purge-data # also removes only ~/.rea/cache and ~/.rea/state
```
Uninstall preserves Hopper, Node.js, Evidence files, captures, unrelated skills, and other MCP servers. It refuses malformed client configuration and never follows purge-data symlinks.
### IDA Pro analysis provider
Already have [mrexodia/ida-pro-mcp](https://github.com/mrexodia/ida-pro-mcp) working? REA can reuse its MCP registration for read-only analysis of the current GUI target, or use its database supervisor to open and analyze a supplied binary headlessly.
```bash
export REA_IDA_MCP_CONFIG=/absolute/path/to/ida-mcp.json
rea function /absolute/path/to/program main --provider ida --json
```
The registration selects `attached` (default, legacy 1.4 tools) or `headless` (modern database supervisor). Setup can preserve this file reference in your selected agent's REA registration. REA adapts existing analysis contracts and manages its own headless database; it leaves an attached GUI database open and never saves it. Results stay live because external IDA database changes are not immutable snapshots.
See the [IDA provider guide](docs/ida-provider.md) for upstream installation links, exact configuration examples, Windows-native headless operation, lifecycle cleanup, and coverage. The initial real workflows cover a Windows GUI and Windows x64 headless IDA 9.3; other engine/platform combinations remain unverified. Use a package version that includes this adapter; repository main can lead the npm release.
### CLI or agent?
| If you want to… | Use |
| ---------------------------------------------------------------- | ------------------------------------------------------------------- |
| Ask an agent to investigate an app and build a feature | Run setup, restart your agent, then describe the task |
| Inspect or decompile one part of an app from the Terminal | `rea analyze` or `rea decompile` |
| Validate, canonicalize, or compare Evidence bundles | `rea evidence-import`, `rea evidence-export`, or `rea compare` |
| Map a local JavaScript/Electron application without executing it | `rea analyze PATH` or `rea analyze-javascript-application` |
| Reuse immutable analysis results without relaunching a provider | Pass `--snapshot /path/to/analysis.json` to a deep-analysis command |
| Import source as historical reference | `rea import-reference-source` |
| Capture or compare controlled process behavior | `rea capture-process` or `rea compare-process-captures` |
```bash
rea evidence-import /absolute/path/to/evidence/bundle.json
rea evidence-export /absolute/path/to/evidence/bundle.json /absolute/path/to/evidence/canonical.json
rea compare /absolute/path/to/evidence/left.json /absolute/path/to/evidence/right.json
```
Analyze a JavaScript application directory or ASAR without executing it:
```bash
rea analyze /absolute/path/to/releases/app.asar --json
rea analyze-javascript-application /absolute/path/to/releases/app.asar --json
```
For a directory or `.asar`, generic `rea analyze` automatically selects the
static JavaScript application provider when neither `--provider` nor
`--snapshot` is supplied. Both routes return the analysis and its Evidence
context inline.
Import an older source tree as a reference. REA keeps it separate from observations of the current app:
```bash
rea import-reference-source /absolute/path/to/source
```
Historical-source import requires safe no-follow file opens and currently runs
on Linux and macOS. Native Windows reports `unsupported_host`; run the import
with Linux REA in WSL or on another supported host. Changing permissions or
reinstalling REA does not enable this Windows workflow.
Imports read the path supplied to the command. File names do not cause automatic omissions; files are represented by hashes and metadata. To exclude selected paths, set `REA_REFERENCE_SECRET_PATTERNS_JSON` to a JSON string array of ignore patterns. Exports never replace an existing file unless `--overwrite` is explicit.
Use a snapshot to save successful analysis results and reuse them on later runs. REA reuses a result only when the target bytes, operation, parameters, analysis tool, and settings match. It does not cache changes or cursor-dependent calls. Snapshot files stay local and use owner-only permissions.
```bash
rea analyze /absolute/path/to/app --snapshot /absolute/path/to/analysis/app.json
# The same exact query can be answered from that snapshot.
rea analyze /absolute/path/to/app --snapshot /absolute/path/to/analysis/app.json
```
Exact CLI cached-evidence reads happen before any provider process starts. In MCP sessions,
pass `snapshot_path` to `open_binary` to import a snapshot atomically while
opening its matching target; MCP providers may still start before a cached call
result is returned. Pass `snapshot_path` and, when required, `overwrite: true` to
`close_binary` to save atomically before Hopper resources are released. If the
save fails, REA deliberately leaves the session open.
## Just ask your agent
After [setup](#quick-start), restart your agent and ask:
### Ask your agent
```text
Understand how search works in the Notes app, show me the evidence, and build a
similar feature for my project.
```
Replace Notes with the app you want to understand, or ask for an overview first.
Replace Notes with your target app and the feature you want to understand.
## The investigation model
### Use the terminal
<table>
<tr>
<td width="33%" valign="top">
<strong>Decompile</strong><br /><br />
Open an app and recover readable code, strings, names, and other clues about how it works.
</td>
<td width="33%" valign="top">
<strong>Understand</strong><br /><br />
Follow the code from one part of the app to another until the agent can explain how a feature actually works.
</td>
<td width="33%" valign="top">
<strong>Recreate</strong><br /><br />
Turn what the agent learned into a feature for your own product, adapted to your stack, interface, and requirements.
</td>
</tr>
</table>
REA shows how it reached its conclusions. It does not claim to recover original source code or automatically clone an application.
## Why REA
| | |
| ------------------------ | ----------------------------------------------------------------------------------------------------- |
| **Built for agents** | Ask what an app does and let your agent inspect it instead of guessing. |
| **CLI and MCP** | Run the same reverse-engineering capabilities from your terminal or agent. |
| **Guided setup** | Configure your agent, connect an existing analysis tool, or install Hopper with your approval. |
| **From insight to code** | Understand a feature, then build your own version in the same coding session. |
| **Local by design** | Analysis runs on your supported local host. REA does not upload the app to a hosted analysis service. |
| **Keeps context** | Investigate several apps without starting over for every question. |
## One prompt, a full investigation
```text
Reverse engineer the Notes app. Find how offline search works, explain it,
and build a version for my project using TypeScript and SQLite.
```
REA gives the agent a clear path from that request to working code:
| Step | What the agent does | REA tools |
| ---: | --------------------------------------- | ---------------------------------------------------------------- |
| 1 | Opens and identifies the binary | `open_binary`, `binary_overview` |
| 2 | Finds likely offline-search clues | `search_strings`, `search_procedures`, `list_names` |
| 3 | Connects those clues to executable code | `find_xrefs_to_name`, `xrefs`, `procedure_callers` |
| 4 | Reconstructs the relevant control flow | `get_call_graph`, `procedure_callees`, `procedure_info` |
| 5 | Decompiles the relevant routines | `procedure_pseudo_code`, `procedure_assembly`, `batch_decompile` |
| 6 | Builds the feature in your project | code adapted to your stack, product, and requirements |
REA handles the app analysis in steps 1 through 5. The agent performs step 6 with its normal file-editing and test tools, using what it learned about the app.
## Showcase
### DX-Ball: game reconstruction (in progress)
[DX-Ball](https://github.com/N0zoM1z0/dx-ball) follows the journey from a classic
Windows game's executable to maintainable C. Using REA's Ghidra provider, the
project traces functions, game state and dependencies, then checks reconstructed
behavior with original-x86 differential tests and pinned-compiler replay.
A [sound-pan investigation](https://github.com/N0zoM1z0/dx-ball/blob/main/docs/GAMEPLAY_OWNER.md)
turns incomplete pseudocode into a C implementation that passes 3,205 original-x86
cases and reproduces all 63 compiled function bytes. Follow its
[REA workflow](https://github.com/N0zoM1z0/dx-ball/blob/main/docs/REA.md)
from binary evidence to reconstruction.
## What agents can do
- Investigate a feature you like and build a version tailored to your own product.
- Explain how a feature works when its source code is unavailable.
- Reconstruct an app's authentication, storage, update, or networking flow.
- Recover enough structure to document an undocumented format or interface.
- Trace a suspicious behavior from a string or symbol to the code that implements it.
- Turn recovered behavior into product features, tests, migration notes, ports, or interoperable replacements.
- Analyze Swift and Objective-C metadata without manually untangling every mangled symbol.
- Leave names, comments, and bookmarks in Hopper so human and agent analysis reinforce each other.
See [native investigation](docs/native-investigation.md) for keyed archives, instruction/call/type primitives, typed dispatch metadata, value traces and native desktop observation.
## Tool catalog for investigation
| Tool family | Count | Examples |
| ------------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Native inspection | 41 | functions, pseudocode, assembly, strings, symbols, calls, references, annotations, byte reads, and file offsets |
| Investigation workflows | 14 | app overviews, function dossiers, native APIs and dispatch, batch decompilation, feature traces, call paths, call graphs, Swift and Objective-C discovery |
| Native macOS utilities | 7 | Mach-O metadata, code signatures, plists, architectures, and Swift demangling without launching Hopper |
| Artifact graph | 5 | directory and package inventories, compiled Interface Builder files, Apple asset catalogs, and extraction |
| Managed PE/CLI | 7 | .NET identity, metadata, CIL instructions, native dependencies, reconstruction imports, and build comparisons |
| Firmware | 2 | Linux firmware region inspection and explicit extraction |
| Android APK | 5 | package and manifest declarations, class search, member inventories, method decompilation, and incoming static references |
| Browser observation | 11 | page structure, network metadata, scripts, source maps, WebMCP discovery, screenshots, and capture comparisons |
| Electron analysis | 5 | renderer observation, static app mapping, and static/runtime reconciliation |
| JavaScript runtime | 2 | Node/Electron Inspector target discovery, script locations, and execution-context events |
| Application workflows | 13 | captured website script export; Android/Apple inventory projections; cross-layer feature traces, build comparisons, historical source mapping, static return-shape comparison, and reconstruction checks |
| Workspace and observation | 21 | sessions, evidence bundles, navigation context, process/artifact/function comparisons, and open-question tracking |
The public interface describes what the agent is trying to learn. Providers decide how to answer. macOS utilities handle common semantic inspection without launching Hopper; Hopper handles deeper native analysis; the process harness records direct behavioral captures.
## Current status
REA supports native application, JavaScript, Electron, .NET, and browser investigation on macOS and Linux. Individual tools have platform and runtime prerequisites. `rea capabilities` and `rea providers` describe the binary-session providers and auxiliary operations; they are not an inventory of every browser, Android, or application workflow. Use the connected MCP tool list and `binary_session` tool availability for the full MCP surface, and the relevant guide for each tool's prerequisites. Repository main can be ahead of [the npm release](docs/installation.md#released-package-and-main).
Static Android APK inspection supports Linux and macOS; the current metadata bridge is verified on macOS arm64 with headless JADX; see [Android analysis](docs/android-analysis.md) for its separate prerequisites and coverage.
- **Native binaries:** Open Mach-O, ELF, PE, and Mac `.app` targets through a selected deep provider. Hopper and Ghidra cover broad inventory and function analysis; the IDA adapter supplies its documented read-only function/string operations. Hopper also accepts `.hop` databases and supports annotations.
- **Packages and resources:** Inspect directories, ZIP, APK, IPA, ASAR, plists, compiled Interface Builder files, and Apple asset catalogs. Artifact requests name the input and requested extraction or traversal directly; macOS DMG traversal also requires the host's native mounting support.
- **JavaScript and Electron:** Map modules, imports, source maps, routes, IPC channels, storage, and native add-ons without running the app. Compare builds and trace a feature across the recovered graph. Dynamic and ambiguous relationships remain unresolved. See [JavaScript application workflows](docs/javascript-application-workflows.md).
- **Websites:** Inspect a selected page in an existing Chrome-family browser. Capture page structure, network metadata, script evidence, and screenshots requested by the call. Passive observation does not navigate or execute page JavaScript. See [browser observation](docs/browser-observation.md).
Export retained scripts with `export_web_scripts` / `rea export-web-scripts`
into a verified local directory, then use the existing JavaScript analysis
and tracing tools. Source URLs, frame or transaction references, competing
versions, and missing-source states remain inline. See
[captured website scripts](docs/website-script-export.md).
Inspect a selected node's listener sources or observe an externally triggered
execution window with `inspect_web_event_listeners` / `observe_web_execution`.
Precise coverage resets counters and affects optimized execution; see
[website runtime attribution](docs/web-runtime.md).
Trace one exported script's native imports with `trace_web_module_imports` /
`rea trace-web-module-imports`, preserving query/fragment identity and optional
import-map context. Requires caller-supplied Chromium via
`REA_BROWSER_EXECUTABLE`; see [module relationships](docs/website-module-trace.md).
- **Source locations:** Trace one retained website script point through a selected
local source map with `rea trace-web-source-location`. See
[captured website source locations](docs/web-source-location.md) for byte
identities, embedded original content and coverage limits.
Recover readable modules from selected local bundles with
`recover_javascript_sources` / `rea recover-javascript-sources`, then pass
the returned `analysis_input` to static analysis. This optional Linux x64
adapter requires caller-supplied Wakaru 1.13.0; see
[JavaScript source recovery](docs/javascript-recovery.md).
- **Electron and Node runtime observation:** Inspect selected Electron pages or attach to a Node/Electron V8 Inspector target. Inspector observation records script locations and execution-context events; it does not infer imports, IPC activity, or which modules executed. See [runtime observation](docs/javascript-runtime-observation.md).
- **.NET assemblies:** Inspect metadata and CIL instructions, compare builds, and check declared native dependencies without loading or running the assembly. Imported decompiler output is labeled as analyst inference. See [managed-code analysis](docs/managed-code-analysis.md).
- **Controlled behavior capture:** Run process, browser, or Electron scenarios with the target, actions, and lifecycle declared in each request, then compare the resulting evidence. Missing observations cannot establish that two runs behaved the same way.
- **Evidence and comparison:** Save results with artifact identity, provider, locations, confidence, and limitations. Export or import bundles, compare artifacts and functions, and connect static findings to runtime observations without claiming causality from correlation.
- **Open questions:** Track unresolved findings, contradictions, and follow-up probes. Reconstruction checks report pass, fail, or unknown rather than treating missing evidence as a pass.
- **Guided workflows:** Start six [MCP investigation workflows](docs/mcp-prompts.md) with suggestions based on your current session.
Windows x64 Ghidra P0 supports native, non-managed, non-DLL x86-64 PE applications on fixed local NTFS with 25 read-only operations. Linux/macOS Ghidra additionally supports atomic session-scoped function names and entry comments. Ghidra has no GUI controls; Windows P0 has no mutation authority.
### Website observation with CDP
REA can inspect an already-running Chrome-family browser through a literal loopback CDP endpoint. Each request names the endpoint and target; an optional origin filter can narrow discovery:
Inspect an extracted JavaScript/Electron app directory or ASAR:
```bash
rea list-browser-targets http://127.0.0.1:9222 --json
rea inspect-web-page http://127.0.0.1:9222 TARGET_ID --json
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
The eight passive browser tools work through both CLI and MCP. They inspect the selected page without navigating, clicking, or evaluating its JavaScript. Credentials, cookies, authorization headers, and raw payload values are not retained. A request selects whether to include script sources, accessibility text, screenshots, or console and payload summaries. REA cannot observe activity that happened before it attached. See [browser observation](docs/browser-observation.md) for browser startup, capture options, and limits.
The result includes modules, imports, Electron boundaries and their evidence.
Replace the path with your target, such as `"D:/apps/example"` on Windows.
### Controlled browser scenarios
`capture_browser_scenario` runs a caller-declared sequence of browser actions through
Playwright. Unlike passive observation, it can interact with the page. Each
step records evidence such as screenshots, page structure, navigation, and
network activity. Missing or truncated observations cannot establish that two
runs behaved the same way.
```bash
rea capture-browser-scenario ./scenario.json --json
```
Launch mode owns a temporary browser profile and removes it after terminating
the launched browser. Connect mode accepts one exact loopback CDP target and
disconnects without closing the external browser. The request supplies the
selected executable or endpoint, actions, and any origin or environment
selections needed by the scenario. Scenario JSON contains secret references and
environment-variable names, never secret values. The default capture retains
only the final URL; request `dom`, `accessibility`, or `screenshot` when an
interaction changes the page without navigating. For asynchronous updates,
wait for a page-specific result-ready condition before capture. See the
[browser scenario contract](docs/browser-scenario-contract.md), including its
interaction example.
### Node and Electron V8 Inspector observation
Node and Electron runtime observation attaches to an existing Inspector target named in the request:
```bash
rea list-javascript-runtime-targets http://127.0.0.1:9229 --json
rea observe-javascript-runtime http://127.0.0.1:9229 TARGET_ID \
--runtime-kind node --json
```
REA records script locations and execution-context events without evaluating
code or setting breakpoints. These observations do not establish import
relationships, event activity, IPC, process identity, or Electron roles. See
[Node and Electron runtime observation](docs/javascript-runtime-observation.md)
for the exact coverage.
Exact package, tool-family, provider, setup-client, schema, and CLI facts are generated from source in [`docs/product-catalog.json`](docs/product-catalog.json). PR CI verifies this catalog, narrative documentation, and generated schemas.
## Roadmap
The [current status](#current-status) section describes shipped capabilities. These are the next areas of work.
### Now
1. **Keep documentation accurate:** update the generated catalog and documentation checks when tools, providers, setup options, or versions change.
2. **Test more native binaries:** expand architecture and indirect-call coverage across Hopper and Ghidra.
### Next
1. **Connect more application layers:** add static extractors and runtime observations to feature traces.
2. **Extend .NET analysis:** improve comparisons of obfuscated assemblies and connect managed findings to verified native analysis. See the [managed-code guide](docs/managed-code-analysis.md).
3. **Compare more runtime behavior:** expand process, protocol, filesystem, reconnect, and version-comparison coverage.
### Later
1. **Expand browser and Electron interaction:** add scenario actions beyond the current click and wait operations.
2. **Observe native apps at runtime:** explore LLDB, Frida, system logs, and native API tracing.
3. **Evaluate more tools and targets:** assess Binary Ninja, Rizin, LIEF, Windows-native tools, mobile apps, and firmware. IDA support already ships via the bring-your-own upstream adapter; see the [IDA provider guide](docs/ida-provider.md).
Setup already lets you choose agent integration and Hopper installation. Support for installing additional analysis tools is future work, described in the [installation roadmap](docs/roadmap.md).
See [provider evaluation](docs/provider-evaluation.md) for coverage and remaining requirements, and the [native investigation guide](docs/native-investigation.md) for UI, dispatch, and value-flow analysis.
## Using REA with other agents
Setup offers supported agent integrations for selection. Existing REA registrations are selected by default; newly detected agents remain unselected until chosen. Any agent that supports local MCP servers can use the configuration below.
### Manual MCP configuration
<!-- x-release-please-start-version -->
```json
{
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@5.0.0", "mcp"]
}
}
}
```
<!-- x-release-please-end -->
Persistent registrations should use one exact package version. `rea setup`
maintains that pin, updates the bundled skill at the same time, and gives Codex
a 30-second startup allowance for a cold package-runner start. `rea update`
installs an exact release and verifies the new executable. It returns an
unapplied maintenance plan for existing REA integrations, with a scoped setup
command to review and approve their changes. Restart affected agents afterward.
MCP clients that support prompts can also discover six ordered investigation
workflows through `prompts/list`. Their optional identifier arguments use the
current session for bounded `completion/complete` suggestions; see
[Guided MCP prompts and completion](docs/mcp-prompts.md).
## How it works
```mermaid
flowchart LR
Agent["Agent"] --> REA["REA<br/>CLI + MCP"]
Terminal --> REA
REA --> Session["Target-bound session router"]
Session --> Registry["Deep-provider registry<br/>deterministic selection"]
Registry --> Hopper["Hopper provider"]
Registry --> Ghidra["Ghidra provider<br/>inventory + function analysis + annotations"]
Registry --> Ida["IDA MCP provider<br/>attached GUI or owned headless database"]
Hopper --> Runtime["Owned provider runtime<br/>deadline + bounded diagnostics + cleanup"]
Ghidra --> Runtime
Ida --> IdaRuntime["Upstream MCP lifecycle<br/>live observations + owned database cleanup"]
IdaRuntime --> Target
Session --> Native["Native macOS provider"]
Session --> Artifact["Artifact graph provider"]
REA --> Browser["Browser CDP provider"]
REA --> Android["Android static provider<br/>headless JADX adapter"]
Android --> Runtime
REA --> Firmware["Firmware providers<br/>Binwalk / Unblob adapters"]
Firmware --> Runtime
REA --> Process["Process capture provider"]
Runtime --> Target["Target software"]
Process --> Target
Native --> Target
Artifact --> Target
```
The CLI and MCP server use the same application workflows and evidence contracts. A provider declares which capabilities it supports and the side effects those capabilities may have. Terminal commands are short-lived; an MCP session can retain an active target and evidence ledger for the session.
## CLI
The agent workflow above is the easiest way to use REA. For a one-off overview from the Terminal:
```bash
npx -y rea-agents@latest analyze /Applications/Notes.app
npx -y rea-agents@latest inspect /Applications/Notes.app
npx -y rea-agents@latest search /Applications/Notes.app "offline"
npx -y rea-agents@latest function /Applications/Notes.app 0x1000
npx -y rea-agents@latest xrefs /Applications/Notes.app 0x1000
npx -y rea-agents@latest trace /Applications/Notes.app "offline"
npx -y rea-agents@latest compare /absolute/path/to/left-evidence.json /absolute/path/to/right-evidence.json
npx -y rea-agents@latest capabilities
npx -y rea-agents@latest providers
```
Run `npx -y rea-agents@latest --help` for direct decompilation, bounded search and
other options. `analyze` and `inspect` share the same overview workflow;
`function`, `xrefs`, and `trace` return the same Evidence envelopes as MCP.
Or install the `rea` command globally:
To install the `rea` command for regular use:
```bash
npm install --global rea-agents
rea --help
```
For native analysis, configure a provider first. See the
[CLI and Evidence guide](docs/cli.md) for native commands, provider selection,
snapshots and scripting.
### Update REA
REA changes quickly, and new releases include frequent bug fixes. Keep your
installation up to date.
For an npm-installed CLI:
```bash
rea update
rea mcp
```
REA accepts a Mac `.app` folder directly. If an agent cannot find an app by name, tell it where the app is installed.
To refresh your agent registrations and skill, run the setup command printed
by the update.
### Choosing a deep-analysis provider
Choose which analysis tool to use from the CLI:
If you use `npx`, update your agent setup with:
```bash
rea providers --json
rea analyze /absolute/path/to/program --provider hopper
REA_ANALYSIS_PROVIDER=hopper rea decompile /absolute/path/to/program 0x1000
npx rea-agents@latest setup
```
For MCP, pass the optional selector on `open_binary`:
Review the setup changes and restart your agent. For one-off CLI commands,
use `npx rea-agents@latest` followed by the command.
```json
{
"path": "/absolute/path/to/program",
"provider_id": "hopper"
}
```
## How REA works
Use `--provider`, or `provider_id` in MCP, to choose Hopper, Ghidra, or IDA for a target. This choice overrides `REA_ANALYSIS_PROVIDER`.
Your agent calls REA through MCP to inspect the target and trace relevant code.
REA returns findings with their evidence. The agent uses them to ask follow-up
questions, explain the behavior, or write and test an implementation.
CLI commands use the same workflows.
With `auto`, REA selects the only available tool that supports the target. If both are available, specify one before opening the target. The session keeps that choice until you explicitly switch or close it; a failure never silently switches tools. Artifact-only analysis can work without a native analysis tool.
![REA investigation flow: your agent asks about a local target, REA inspects and traces it using analysis tools, and the agent uses the returned code, references and unknowns to explain, implement and test.](website/public/assets/figures/rea-investigation-flow.svg)
Run `rea providers` and `rea capabilities` to check availability and supported operations. Ghidra supports inspection and atomic function annotation edits on Linux and macOS. Its database edits are discarded on close; GUI controls require Hopper. Windows Ghidra P0 supports read-only analysis of native x86-64 PE applications on local NTFS with bundled native controls.
[Open the full-size figure](website/public/assets/figures/rea-investigation-flow.svg).
The session also reports active work and cleanup status. If a caller times out, the analysis tool may still be busy; `analysis_activity` reports that state. A `cleanup_incomplete` result identifies resources whose shutdown or removal could not be verified. See [provider selection and analysis profiles](docs/adr/0001-provider-selection-and-analysis-profiles.md) for session, cache, and process-tracking details.
<a id="current-status"></a>
### CLI exit status
## What you can analyze
| Status | Meaning |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `0` | The operation completed. Results may still include warnings, partial evidence, or unresolved questions. |
| `1` | The operation could not complete, for example because of invalid input, host permission denial, cancellation, or timeout. Structured output reports the reason when available. |
| `128 + N` | The process ended from signal `N`, where the shell or runtime preserves the conventional signal-derived status. |
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:
`setup --dry-run` returns status `planned` and exits `0`. A setup result with
status `cancelled` also exits `0`.
`setup` returns `1` for `needs_confirmation` or `needs_human`
because configuration is not ready; rerun it after approval or remediation.
`doctor` returns `1` when required checks for its readiness scope are unhealthy.
Unavailable optional provider prerequisites remain visible as informational
diagnostics and do not block setup or unrelated providers. Output format, full envelopes,
filters, and token controls never change the operation status.
| Target | What REA returns | Requirements and guide |
| ---------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](https://rea.tools/guides/native/) |
| Offline ELF layout | Sections, segments, original symbols/relocations and static mitigation candidates | Caller-supplied pwntools on Linux x64; [binary diagnostics](docs/binary-diagnostics.md) |
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier; [offline EVM guide](docs/evm-bytecode.md) |
| Recorded Linux crashes | Raw notes, every recorded thread's registers/signals and optional mapping candidates | Caller-supplied pwntools; 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; [application analysis](https://rea.tools/guides/javascript/) |
| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](https://rea.tools/guides/browser/) |
| Saved network captures | Requests, responses, exposed payloads and source locations | HAR; mitmdump on Linux for native mitmproxy captures; [capture guide](docs/web-network-captures.md) |
| .NET assemblies | Metadata, CIL instructions, declared native dependencies and build comparisons | Static inspection; [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; [Android guide](docs/android-analysis.md) |
| Firmware | Regions, extraction results and native-analysis handoffs | Binwalk / Unblob on Linux; [firmware guide](docs/firmware-analysis.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) |
When REA feeds a shell pipeline, enable `pipefail` so a downstream formatter
cannot hide its failure:
Static JavaScript and .NET inspection read the supplied files without running
the application. Runtime capture runs or interacts with the selected target
using your user permissions; each runtime guide describes its effects.
```bash
set -o pipefail
rea inspect-artifact ./app.asar --json | jq . > inspection.json
```
<a id="choosing-a-deep-analysis-provider"></a>
## Current Hopper provider
Native formats and host support vary by provider. See
[Hopper and Ghidra setup](docs/installation.md#hopper), the
[IDA guide](docs/ida-provider.md), and
[experimental Windows Ghidra support](docs/windows-ghidra-p0.md).
Ghidra also supports [16-bit DOS analysis](docs/ghidra-dos.md).
For provider selection, see the [CLI guide](docs/cli.md#choose-a-provider).
Check [release availability](docs/installation.md#released-package-and-main)
for features added since the latest npm release.
REA starts Hopper when an operation needs it. On macOS, Hopper may bring its window or a dialog to the foreground even though REA requests background startup. Demo or license prompts may need your attention.
## Showcases
Hopper handles one analysis request at a time. Cancelling your wait does not stop work already running inside Hopper; the session reports whether it is still busy. Successful decompilation results are cached until a relevant rename or comment change.
### DX-Ball: reconstruct a sound-pan calculation
Use `rea instructions` when you only need assembly instructions for a function. It avoids decompilation and a whole-program inventory.
Follow a sound call into its position-to-pan helper, inspect the instructions,
and turn incomplete pseudocode into C. The reconstruction passes 3,205
original-x86 cases and reproduces all 63 compiled function bytes.
Closing a session shuts down REA's bridge and removes its temporary socket directory while preserving a Hopper application you may be using. If cleanup cannot be verified, `close_binary` reports `cleanup_incomplete` and the affected resources.
[Read the case study](https://rea.tools/showcase/dx-ball/) ·
[Reconstruction repository](https://github.com/N0zoM1z0/dx-ball)
## Process capture
### Notion: trace the Electron clipboard bridge
Process capture runs the exact executable and scenario declared in the request,
with the requested working directory and environment. Filesystem observation
paths select what to snapshot. The process runs with your user permissions;
Process Capture records behavior and is not a security sandbox.
Find the renderer's clipboard API, follow it through preload and IPC into the
main process, and inspect the rich clipboard format.
Capture a scenario or compare two saved Process Capture Evidence records:
[Read the case study](https://rea.tools/showcase/notion/)
```bash
rea capture-process ./authority-scenario.json --json > authority-capture.json
rea capture-process ./reconstruction-scenario.json --json > reconstruction-capture.json
rea compare-process-captures authority-capture.json reconstruction-capture.json --json
```
### TH04: recover a DOS bullet-ring calculation
The comparison reports each observed dimension separately and identifies the
first terminal, interaction, exit, filesystem, or process divergence.
See [Process Capture](docs/process-capture.md) for scenario fields, limits, and
evidence boundaries.
Inspect the original PC-98 game's 16-bit instructions, recover the fixed and
aimed angle calculations, and compare the reconstructed C++ with the
historical compiler output.
Process Capture currently runs on Linux and macOS with a working native PTY
backend. Native Windows capture remains unavailable until the PTY adapter can
verify descendant cleanup; installing or reinstalling its Windows PTY binary
does not enable capture. For Linux commands, run Linux REA inside WSL.
Comparing existing capture Evidence remains available on Windows.
[Read the case study](https://rea.tools/showcase/th04/) ·
[Reconstruction repository](https://github.com/N0zoM1z0/th04)
On supported capture hosts, a missing or incompatible PTY binary can be fixed
by reinstalling REA for the current platform, architecture, and Node.js version
with optional dependencies enabled. Use separate scenario and output files;
`--json` produces the JSON consumed by the comparison command.
ASAR inventory verifies Electron integrity metadata for both archive entries
and `.asar.unpacked` companion files. Integrity failures identify the logical
path, declared and calculated SHA-256 values, and whether the entry was
unpacked; REA does not silently accept the mismatched artifact. If a supplied
ASAR declares unpacked companion bytes that are absent from the local artifact
set, REA keeps that occurrence as `unavailable` and continues analyzing the
embedded JavaScript instead of treating the missing native/resource bytes as
verified or absent.
## Security model
Analysis runs locally. REA communicates with Hopper and Ghidra through authenticated private local sockets and with IDA through its configured local MCP registration. Your agent or model provider has its own data policy.
Runtime requests act on the declared target and lifecycle. Analysis tools and launched targets run with your user permissions, and native UI capture still depends on macOS Accessibility and Screen Recording access. Static JavaScript analysis does not execute extracted modules; use direct browser, Electron, or process capture when runtime behavior is needed.
Windows Ghidra P0 automatically uses native Job Objects, protected private-runtime DACLs, and handle-based admission. Its experimental scope is native x86-64 PE applications on local NTFS. Report vulnerabilities through the private process in [SECURITY.md](SECURITY.md).
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.
## FAQ
<details>
<summary><strong>Does Hopper need to be running before I start REA?</strong></summary>
No. REA starts Hopper when an operation needs it. An already-running Hopper application is also supported.
</details>
<details>
<summary><strong>Why did Hopper appear in front of my other windows?</strong></summary>
Hopper's launcher internally activates the application. REA requests background startup, but macOS and Hopper may still bring a window or dialog forward. See [Current Hopper provider](#current-hopper-provider).
</details>
<details>
<summary><strong>Does REA include Hopper?</strong></summary>
No. Setup can install Hopper for you, but Hopper remains separate software with its own license. REA supplies the CLI, MCP server, and workflows that make it usable by agents.
</details>
<details>
<summary><strong>Does REA install or include Ghidra or Java?</strong></summary>
No. REA connects to an existing Ghidra installation. Run setup after providing the Ghidra and Java paths shown in the [Ghidra section](#ghidra-analysis-provider).
</details>
<details>
<summary><strong>Does REA upload the app?</strong></summary>
REA has no hosted analysis service. Current providers analyze artifacts and capture behavior locally. Your agent or model provider may have its own data policy, so review that separately.
</details>
<details>
<summary><strong>Can REA recover the original source code?</strong></summary>
No decompiler can guarantee the original source. REA gives an agent pseudocode, assembly, symbols, strings, metadata, and relationships that it can use to explain or compatibly recreate observed behavior.
</details>
<details>
<summary><strong>Which agents can use REA?</strong></summary>
Any agent that can run a local MCP server can use the manual configuration. Setup offers the supported integrations listed in [Installation and setup](docs/installation.md#supported-agents); existing REA registrations are selected by default, and other detected agents require selection.
Any agent that supports local MCP servers. Setup configures the
[supported agents](docs/installation.md#supported-agents); other clients can use
[manual MCP registration](docs/installation.md#mcp-registry).
</details>
## Development
<details>
<summary><strong>Do I need Hopper, Ghidra or IDA?</strong></summary>
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and contribution checks, and [docs/testing.md](docs/testing.md) for test scopes and real-tool verification. `npm run docs:check` checks committed generated documents; `npm run docs:generate` regenerates them.
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).
`npm run verify:agent` evaluates native, JavaScript, managed, and browser investigation tasks through a real local Codex CLI. Its report covers tool selection, repeated calls, token use, completion quality, and handling of permissions and unknowns.
</details>
`npm run evidence:generate` regenerates the managed conformance manifest and Evidence completion ledger from live verification results. `npm run evidence:check` reruns verification and checks for drift. Unsupported claims remain explicit and do not count as passes. See [testing](docs/testing.md) for verification commands and process-cleanup checks.
<details>
<summary><strong>Do I need to start Hopper first?</strong></summary>
REA starts Hopper when an operation needs it. On macOS, a first-run dialog may
ask you to choose demo mode or activate your license. See
[Hopper startup and troubleshooting](docs/installation.md#launcher-paths-and-troubleshooting).
</details>
<details>
<summary><strong>What does installing the skill from skills.sh do?</strong></summary>
The skill supplies investigation instructions for your agent. Use `rea setup`
to register REA's MCP server and install the matching instructions, then restart
your agent. See [skill-only installation](docs/installation.md#skill-only-installation).
</details>
<details>
<summary><strong>What code does REA return?</strong></summary>
Native analysis returns pseudocode and assembly. JavaScript/Electron analysis
recovers modules and their relationships. Your agent uses these findings to
write and test an implementation; the [showcases](#showcases) give worked examples.
</details>
<details>
<summary><strong>Does REA upload my app?</strong></summary>
REA analyzes targets locally. Your agent receives the tool results, and its
model provider has its own data policy.
</details>
<details>
<summary><strong>What should I do if I hit a bug?</strong></summary>
Update first; a recent release may already fix it.
For an npm-installed CLI:
```bash
rea update
```
For agent setup through `npx`:
```bash
npx rea-agents@latest setup
```
If you're using an agent, complete the [setup refresh](#update-rea) and restart
it. Retry the same task. If the problem persists, [open an issue](https://github.com/morluto/rea/issues)
with your REA version, target type, steps to reproduce and error output.
</details>
## Documentation
Start with the website's [worked guides](https://rea.tools/guides/).
For exact options, prerequisites and result contracts:
- [Installation and setup](docs/installation.md): agent registration, provider configuration, updates and uninstall.
- [Readiness and troubleshooting](docs/installation.md#check-readiness-for-your-task): diagnose one agent or analysis engine.
- [CLI and Evidence](docs/cli.md): commands, provider selection, snapshots, import/export and exit statuses.
- [MCP contracts](docs/mcp-contracts.md) and [agent prompts](docs/mcp-prompts.md): tool results, sessions and guided investigations.
- [Tool catalog](docs/mcp-contracts.md#generated-catalog): build-generated inventory of tools, providers and CLI commands.
- [Roadmap](docs/roadmap.md): planned work and capability trackers.
Report vulnerabilities through [SECURITY.md](SECURITY.md).
## Contributing
We'd love your help with REA! [Open an issue](https://github.com/morluto/rea/issues) to
report a bug or suggest a feature, or [send a pull request](https://github.com/morluto/rea/pulls)
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 the project structure.
## Project links
[npm](https://www.npmjs.com/package/rea-agents) · [Issues](https://github.com/morluto/rea/issues) · [Security](SECURITY.md) · [Contributing](CONTRIBUTING.md) · [Hopper](https://www.hopperapp.com/) · [Ghidra](https://github.com/NationalSecurityAgency/ghidra)
## License
[MIT](LICENSE)
[Website](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [Security](SECURITY.md)
## Star history
🎉 **20,000 GitHub stars — thank you!**
Thanks to everyone using REA, reporting bugs, testing builds, and contributing fixes.
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date&amp;theme=dark&amp;legend=top-left" />
@@ -825,3 +332,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and contribution ch
## Disclaimer
REA provides tools for lawful reverse-engineering research, analysis, and reconstruction. You are responsible for obtaining any required authorization and complying with applicable laws. The project does not endorse illegal or unauthorized use.
## License
[MIT](LICENSE)
+182 -236
View File
@@ -2,339 +2,285 @@
[English](README.md) · [简体中文](README_zh.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · **العربية**
# REA: اعكس هندسة أي شيء
# REA: هندسة عكسية لأي شيء
### وسيط MCP واحد للهندسة العكسية عبر الملفات التنفيذية والتطبيقات وسلوك وقت التشغيل.
### خادم MCP واحد للهندسة العكسية للملفات الثنائية والتطبيقات والسلوك أثناء التشغيل.
**اعثر على ميزة تعجبك. افهم آلية عملها. ابنها بالطريقة التي تريدها.**
**وجدت ميزة تعجبك؟ افهم طريقة عملها حتى مستوى الشيفرة الثنائية.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#منصة-أدوات-التحقيق)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
[![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
[![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM)
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
**[الموقع (بالإنجليزية)](https://morluto.github.io/rea/) · [أدلة الاستخدام](https://morluto.github.io/rea/guides/) · [دراسة حالة DX-Ball](https://morluto.github.io/rea/showcase/dx-ball/)**
**[الموقع](https://rea.tools/) · [الأدلة](https://rea.tools/guides/) · [دراسات الحالة](https://rea.tools/showcase/)**
[البدء السريع](#البدء-السريع) · [حالة الدعم](#حالة-الدعم) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [منصة أدوات التحقيق](#منصة-أدوات-التحقيق) · [خطة العمل](#خطة-العمل) · [كيف يعمل؟](#كيف-يعمل)
[البدء السريع](#البدء-السريع) · [كيف يعمل REA](#كيف-يعمل-rea) · [ما الذي يمكنك تحليله](#ما-الذي-يمكنك-تحليله) · [دراسات الحالة](#دراسات-الحالة) · [الأسئلة الشائعة](#الأسئلة-الشائعة) · [التوثيق](#التوثيق)
<table aria-label="REA community">
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="REA يشغّل جسر التحليل داخل Hopper لفحص ملف ثنائي للشيفرة الأصلية" width="1200" />
<br />
<table aria-label="مجتمع REA">
<tr>
<td align="center" width="360">
<a href="https://discord.gg/GkcryMnJDM">
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
<strong>انضم إلى مجتمع الهندسة العكسية</strong>
</a><br />
<sub>Discord · أسئلة وأجوبة · شارك أعمالك</sub>
<sub>Discord · أسئلة وأجوبة · مشاركة النتائج</sub>
</td>
</tr>
</table>
<br />
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="REA يشغّل جسر التحليل داخل Hopper لفحص ملف تنفيذي أصلي" width="1200" />
</div>
---
هل وجدت في تطبيق ميزة تريدها في منتجك؟ اطلب من وكيلك استقصاءها باستخدام REA. يستطيع فحص التطبيق من دون شيفرته المصدرية، وشرح طريقة عمل الميزة وعرض الأدلة، ثم بناء نسخة تناسب مشروعك.
هل وجدت في تطبيق ميزة تريد إضافتها إلى منتجك؟ اطلب من وكيلك استقصاءها باستخدام REA. يستطيع فحص التطبيق من دون شيفرته المصدرية، وشرح طريقة عمل الميزة وعرض الأدلة، ثم بناء نسخة تناسب مشروعك.
يوفر REA أدوات لتحليل الملفات التنفيذية الأصلية وتطبيقات JavaScript وElectron وتجميعات .NET والمواقع. يمكنك استخدامها من وكيلك أو من الطرفية. يعمل التحليل محليًا، وتتضمن النتائج الأدلة والقيود.
يربط REA وكيلك بأدوات لفحص الملفات الثنائية للشيفرة الأصلية، وتطبيقات JavaScript وElectron، وتجميعات .NET، والمواقع. يمكنك أيضًا استخدام الأدوات نفسها من الطرفية. يجري التحليل محليًا، وتتضمن النتائج الأدلة والقيود التي تستند إليها كل خلاصة.
يضبط Setup إعدادات الوكيل ويربطه بتثبيت موجود من Hopper أو Ghidra، ويثبّت سير عمل REA الموجّه. ويمكنه تثبيت Hopper بعد موافقتك. ولا يحتاج التحليل الثابت لتطبيقات JavaScript إلى أي محرك.
يسجّل الإعداد REA لدى وكيلك ويثبّت تعليمات سير العمل المطابقة. يمكن لتحليل الشيفرة الأصلية استخدام تثبيت موجود من Hopper أو Ghidra؛ ويمكن للإعداد أيضًا تثبيت Hopper اختياريًا بعد موافقتك. لا يحتاج التحليل الثابت لـ JavaScript إلى أي من المحركين.
## اطلب من وكيلك مباشرة
بعد [الإعداد](#البدء-السريع)، أعد تشغيل الوكيل ثم اطلب:
```text
استقصِ طريقة عمل البحث في تطبيق Notes، واعرض الأدلة، ثم ابنِ ميزة مشابهة لمشروعي.
```
استبدل Notes بالتطبيق الذي تريد فهمه، أو اطلب نظرة عامة أولًا.
## من الملف التنفيذي إلى السلوك
| فك الترجمة | الفهم | إعادة الإنشاء |
| ---------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------ |
| استخرج الشيفرة شبه المصدرية، والتعليمات، والسلاسل النصية، والرموز | تتبع تدفق التحكم والمراجع المتبادلة والاستدعاءات والأنواع | حوّل ما تعلمه الوكيل إلى ميزة تناسب تقنياتك وواجهتك ومتطلباتك |
يوضح REA كيف وصل إلى نتائجه. وهو لا يدّعي استعادة الشيفرة المصدرية الأصلية أو استنساخ التطبيق تلقائيًا.
## لماذا REA؟
| | |
| ------------------------ | ---------------------------------------------------------------------------- |
| **مصمم للوكلاء** | اسأل عما يفعله التطبيق ودع الوكيل يفحصه بدلًا من التخمين. |
| **CLI وMCP** | استخدم قدرات الهندسة العكسية نفسها من الطرفية أو الوكيل. |
| **إعداد موجّه** | اضبط وكيلك، واربط أداة تحليل موجودة، أو ثبّت Hopper بموافقتك. |
| **من الفهم إلى الشيفرة** | افهم الميزة ثم ابنِ نسختك الخاصة في جلسة البرمجة نفسها. |
| **محلي حسب التصميم** | يجري التحليل على مضيفك المحلي المدعوم، ولا يرفع REA التطبيق إلى خدمة مستضافة. |
| **يحافظ على السياق** | استقصِ عدة تطبيقات دون بدء التحليل من جديد عند كل سؤال. |
> **[زر موقع REA](https://rea.tools/)** للاطلاع على تعليمات الإعداد والأدلة المصوّرة ودراسات الحالة الفعلية.
## البدء السريع
### تشغيل الإعداد (موصى به)
### إعداد وكيلك
اربط REA بوكيلك:
بعد تثبيت Node.js وnpm، نفّذ:
```bash
npx rea-agents setup
```
يعرض Setup أولاً قائمة متعددة الاختيار للوكلاء الذين تريد ربطهم. تكون تسجيلات REA الموجودة محددة افتراضياً، ولا يؤدي اكتشاف عميل جديد وحده إلى تحديده؛ ويمكنك اختيار عميل غير مهيأ. راجع المسارات والتغييرات المحددة قبل الموافقة. يثبت Setup سير عمل REA للوكلاء المحددين افتراضياً. Hopper خيار منفصل يتطلب موافقة مستقلة، ويمكن أيضاً حفظ مسارات تثبيت Ghidra الموجودة.
اختر الوكلاء، وراجع التغييرات المقترحة، ثم وافق عليها. يضيف الإعداد خادم MCP الخاص بـ REA وتعليمات سير العمل المطابقة، مع الاحتفاظ بنسخ احتياطية من الإعدادات الموجودة. أعد تشغيل وكيلك بعد ذلك.
يعرض التغييرات قبل تطبيقها وينشئ نسخة احتياطية من الإعدادات الموجودة. راجع [دليل التثبيت والإعداد](docs/installation.md) للمتطلبات والخيارات الإضافية.
يدعم الإعداد Claude Code وCodex وCursor وGemini CLI وGrok Build وGrok Bot و[وكلاء آخرين](docs/installation.md#supported-agents). راجع [التثبيت والإعداد](docs/installation.md) لتهيئة موفّري التحليل وتسجيل MCP يدويًا.
### الاستخدام مع وكيل
### اطلب من وكيلك
بعد الإعداد، أعد تشغيل الوكيل واشرح التطبيق أو الميزة التي تريد استقصاءها. يدعم REA Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin وOpenCode وAntigravity وGitHub Copilot CLI وCommand Code وVS Code. تكون تسجيلات REA الموجودة محددة افتراضياً، ويجب اختيار العملاء المكتشفين الآخرين يدوياً. يمكن للوكلاء الآخرين استخدام إعداد MCP أدناه.
يمكن تشغيل Hopper في الوضع التجريبي. إذا ظهرت رسالة عند التشغيل الأول، فاختر التجربة أو أدخل ترخيصًا موجودًا.
### مهارة مساعد البرمجة (اختياري)
أضف المهارة إلى مساعد البرمجة للحصول على سياق أغنى:
```bash
npx skills add morluto/rea --skill reverse-engineer-anything
```text
استقصِ طريقة عمل البحث في تطبيق Notes، واعرض الأدلة، ثم ابنِ ميزة مشابهة لمشروعي.
```
توفر المهارة سير عمل التحقيق في REA. نفّذ الإعداد أعلاه لربط REA بالوكيل وضبط أدوات التحليل. يثبّت الإعداد افتراضيًا مهارة مطابقة للإصدار؛ وهذا الأمر يثبّت نسخة المستودع.
استبدل Notes بالتطبيق المستهدف وحدّد الميزة التي تريد فهمها.
لشجرة JavaScript/Electron المستخرجة أو ملف ASAR، لا يلزم إعداد MCP ولا محرك أصلي:
### استخدام الطرفية
افحص مجلد تطبيق JavaScript/Electron بعد فك حزمه، أو ملف ASAR:
```bash
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
استبدل المسار بهدفك (مثل `"D:/apps/example"` على Windows). يدعم سير العمل الثابت هذه المسارات، بينما يتطلب التحليل الأصلي محركه الخاص.
تتضمن النتيجة الوحدات وعمليات الاستيراد وحدود Electron والأدلة المتعلقة بها. استبدل المسار بمسار هدفك، مثل `"D:/apps/example"` على Windows.
### تثبيت أمر rea
ثبت واجهة سطر الأوامر:
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
```
يجب تثبيت Node.js وnpm مسبقًا. عند تشغيله من طرفية، يثبت البرنامج `rea` ويبدأ الإعداد.
يمكنك بدلًا من ذلك التثبيت باستخدام npm ثم تشغيل الإعداد:
لتثبيت أمر `rea` للاستخدام المعتاد:
```bash
npm install --global rea-agents
rea setup
rea --help
```
### المتطلبات
لتحليل الشيفرة الأصلية، هيّئ موفّرًا أولًا. راجع [دليل CLI وEvidence](docs/cli.md) لأوامر تحليل الشيفرة الأصلية، واختيار الموفّر، واللقطات، والاستخدام في السكربتات.
- macOS 12 أو أحدث
- Ubuntu 24.04+ أو Fedora 41+ أو Arch Linux بنواة 64 بت أو CachyOS
- Node.js 22.x (>=22.19) أو 24.x (>=24.11) أو 26+ وnpm
### تحديث REA
يتطلب تحليل الملفات التنفيذية الأصلية Hopper أو Ghidra. Hopper برنامج منفصل، وللوضع التجريبي قيود يحددها المورّد، لكن الترخيص المدفوع ليس إلزاميًا.
يتطور REA بسرعة، وتتضمن الإصدارات الجديدة إصلاحات متكررة للأخطاء. حافظ على تحديث نسختك.
يدعم Ghidra نظامي Linux x64 وmacOS x64/arm64. ثبت Ghidra 12.1.x وJDK الكامل بنواة 64 بت الذي يعلنه ذلك التثبيت (`application.java.min` حتى `application.java.max`) بصورة منفصلة، ثم اضبط REA لاستخدامهما. تتطلب إصدارات 12.1 الحالية JDK 21 أو أحدث ولا تحدد حدًا أعلى. جرى التحقق من الجسر مع Ghidra 12.1.4 وJDK 21. يحتاج macOS أيضًا إلى أداة فك الترجمة الأصلية المطابقة لمعمارية الجهاز.
يستطيع Setup التحقق من التثبيت وحفظ المسارات؛ ولا يثبت أو يحدّث Ghidra أو Java أو Node.js أو npm أو Homebrew.
يتضمن فرع main والحزمة npm 4.1.0 دعمًا تجريبيًا لـ Windows x64 Ghidra P0 لتطبيقات PE الأصلية x86-64 (غير المُدارة وغير DLL) على NTFS محلي، مع ضوابط Job Object وقوائم DACL الخاصة وقبول المسارات المجمّعة. تحقق من [حد الإصدار](docs/installation.md#released-package-and-main) قبل توقع ذلك من حزمة npm أقدم. راجع [دليل Windows Ghidra P0](docs/windows-ghidra-p0.md) للمتطلبات والنطاق الموثق.
### تشخيص المشكلات
يشخّص `npx -y rea-agents@latest doctor` الجهاز والاعتماديات وأدوات التحليل وإعدادات الوكيل من دون تغييرها. أضف `--json` للحصول على تشخيص منظم.
على Linux، يفضّل REA الملف التنفيذي `/opt/hopper/bin/Hopper`، وإذا لم يكن متاحًا يتحقق تلقائيًا من `~/.local/share/rea/hopper/bin/Hopper`. استخدم `HOPPER_LAUNCHER_PATH` للمسارات الأخرى. إذا كان الملف موجودًا لكن محرك التحليل لا يظهر، فشغّل `ldd /absolute/path/to/Hopper | grep 'not found'` على مسار Hopper الفعلي للتحقق من المكتبات الناقصة. راجع [دليل Hopper](docs/installation.md#hopper).
### التحديث والإزالة
- يحدّث `rea update` تثبيت REA الحالي.
- يزيل `rea uninstall` تسجيلات الوكلاء وملفات سير العمل التي يديرها REA، ويترك Hopper مثبتًا.
- يزيل `rea uninstall --purge-data` أيضًا ذاكرة REA المؤقتة وحالته. استخدمه فقط إذا أردت حذف هذه البيانات.
## حالة الدعم
تُوصف قدرات المستودع الحالية ومتطلبات الأنظمة في [دليل الدعم الإنجليزي](README.md#current-status). قد يسبق فرع main [إصدار npm](docs/installation.md#released-package-and-main).
- يوفر Ghidra 25 عملية للقراءة فقط على Linux x64 وmacOS x64/arm64 وحدود Windows x64 P0 التجريبية. يدعم Linux/macOS أيضًا تعليقات الدوال الذرية على مستوى الجلسة. نظام Windows P0 للقراءة فقط، ولا يوفر Ghidra أي تحكم في الواجهة.
- يتطلب الفحص الثابت لنظام Android تجهيز JADX/Java بشكل منفصل؛ ويغطي التحقق بالموفر الحقيقي نظام Linux. راجع [تحليل Android](docs/android-analysis.md).
- تحدد طلبات المتصفح وElectron والعمليات هدفها وإجراءاتها ودورة حياتها مباشرة، وتعتمد على وصول المضيف الفعلي دون منح أذونات منفصلة من REA. ما زالت كتابة إعدادات Setup وتثبيت Hopper تتطلب الموافقة على خطتها الدقيقة.
- يصف `rea capabilities` عمليات جلسة الملف التنفيذي لا كل أدوات التطبيقات ووقت التشغيل. راجع قائمة أدوات MCP الكاملة والدليل ذا الصلة.
## استقصاء كامل بطلب واحد
بعد الإعداد، يمكنك أن تطلب من وكيلك:
> افتح التطبيق، واكتشف آلية عمل البحث من دون اتصال، واشرح تدفق التحكم، ثم ابنِ نسخة لمشروعي باستخدام TypeScript وSQLite.
يمكن للوكيل تحويل ذلك إلى سير عمل قابل للتحقق:
```text
فتح الهدف
→ انتظار اكتمال تحليل Hopper
→ جرد المعماريات والرموز والسلاسل النصية
→ تحديد نقاط الدخول والدوال المهمة
→ تتبع الاستدعاءات والمراجع المتبادلة وتدفق التحكم
→ فك ترجمة التنفيذ ذي الصلة
→ تلخيص السلوك والقيود والحالات الطرفية
→ بناء الميزة بما يلائم تقنيات المشروع ومتطلباته
```
## ما الذي يمكنك فعله؟
### فهم تطبيق لا تملك مصدره
اكشف نقاط الدخول، وأسماء الفئات، والسلاسل النصية، والأطر المستخدمة، ومسارات الميزات من الملف التنفيذي وحده.
### بناء ميزة تعجبك بطريقتك
استقصِ الميزة، وافهم سلوكها، ثم ابنِ نسخة ملائمة لمنتجك وتقنياتك ومتطلباتك.
### توثيق بروتوكول أو تنسيق غير موثق
تتبع الموزعات، والثوابت، والمقارنات، والقراءة والكتابة لفهم الرسائل وتنسيقات الملفات وآلات الحالات.
### إعادة إنشاء سلوك متوافق
حوّل منطقًا مفكوك الترجمة إلى مواصفات واضحة، ثم نفذ بديلًا نظيفًا تحكمه اختبارات السلوك الملحوظ.
### تدقيق منطق أمني حساس
افحص التحقق من الإدخال، والتشفير، والصلاحيات، والتخزين، ومسارات الأخطاء من دون رفع الملف التنفيذي إلى خدمة بعيدة.
## منصة أدوات التحقيق
| عائلة الأدوات | العدد | الاستخدام |
| -------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| الفحص الأصلي | 41 | الدوال والشيفرة شبه المصدرية والتعليمات والسلاسل والرموز والاستدعاءات والمراجع والتعليقات وقراءة البايتات ومواضع الملفات |
| سير عمل التحقيق | 14 | نظرات عامة على التطبيقات وملفات الدوال وواجهات API الأصلية والتوزيع وفك الترجمة الدفعي وتتبع الميزات ومسارات الاستدعاء ومخططاته واكتشاف Swift وObjective-C |
| أدوات macOS الأصلية | 7 | بيانات Mach-O والتوقيعات وplist والمعماريات واستعادة أسماء Swift دون تشغيل Hopper |
| مخطط المخرجات | 5 | جرد الدلائل والحزم وملفات Interface Builder المترجمة وكتالوجات موارد Apple والاستخراج |
| PE/CLI المُدار | 7 | هوية تجميعات .NET وبياناتها الوصفية وتعليمات CIL والاعتماديات الأصلية واستيراد إعادة البناء ومقارنة الإصدارات |
| البرامج الثابتة | 2 | فحص مناطق البرامج الثابتة على Linux واستخراجها الصريح |
| Android APK | 5 | بيانات الحزمة وmanifest والبحث عن الأصناف وقوائم الأعضاء وفك ترجمة الأساليب والمراجع الثابتة |
| مراقبة المتصفح | 11 | بنية الصفحات وبيانات الشبكة والبرامج النصية وخرائط المصدر واكتشاف WebMCP والصور ومقارنة الالتقاط |
| تحليل Electron | 5 | مراقبة العارضين وخريطة التطبيق الثابتة والمواءمة بين نتائج الثبات والتشغيل |
| وقت تشغيل JavaScript | 2 | اكتشاف أهداف Node/Electron Inspector ومواضع البرامج وأحداث سياقات التنفيذ |
| سير عمل التطبيقات | 13 | تصدير النصوص البرمجية الملتقطة، تتبع الميزات عبر الطبقات ومقارنة الإصدارات ومواءمة المصدر التاريخي ومقارنة أشكال القيم المعادة والتحقق من إعادة الإنشاء |
| مساحة العمل والملاحظة | 21 | الجلسات وحزم الأدلة وسياق التنقل ومقارنة العمليات والمخرجات والدوال وتسجيل الأسئلة غير المحسومة |
## خطة العمل
يشمل العمل التالي توسيع التحقق من الأهداف الأصلية، ومقارنة إصدارات JavaScript و.NET، ومراقبة وقت التشغيل والتحقق من إعادة الإنشاء. يتيح Setup بالفعل اختيار تكامل الوكيل وتثبيت Hopper. تثبيت أدوات تحليل أخرى ما زال عملًا لاحقًا. راجع [خطة التثبيت](docs/roadmap.md) و[تقييم أدوات التحليل](docs/provider-evaluation.md).
## استخدام REA مع وكلاء برمجة آخرين
يدعم Setup Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin وOpenCode وAntigravity وGitHub Copilot CLI وCommand Code وVS Code. تكون تسجيلات REA الموجودة محددة افتراضياً، ويجب اختيار العملاء المكتشفين الآخرين يدوياً. يمكن لأي وكيل يدعم خوادم MCP المحلية الاتصال باستخدام الإعداد التالي:
<!-- x-release-please-start-version -->
```json
{
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@5.0.0", "mcp"]
}
}
}
```
<!-- x-release-please-end -->
## كيف يعمل؟
```mermaid
flowchart LR
Human[المستخدم] --> Agent[وكيل البرمجة]
Agent --> REA[REA]
Human -->|CLI| REA
REA --> Hopper[عمليات تحليل Hopper]
Hopper --> App[تطبيقك]
REA --> Ghidra[تحليل Ghidra للقراءة فقط<br/>جرد + تحليل الدوال]
Ghidra --> App
```
يستخدم CLI وخادم MCP سير عمل التحليل نفسه. تحرر أوامر الطرفية جلسة الجسر الخاصة بها عند انتهائها، بينما يمكن لجلسة الوكيل الحفاظ على الاتصال أثناء الاستقصاء. إغلاق جلسة REA لا يغلق تطبيق Hopper الذي يستخدمه المستخدم.
## أوامر CLI
سير عمل الوكيل أعلاه هو أسهل طريقة لاستخدام REA. للحصول على نظرة سريعة على تطبيق من الطرفية:
لواجهة CLI المثبّتة عبر npm:
```bash
npx -y rea-agents@latest analyze /Applications/Notes.app
npx -y rea-agents@latest compare /absolute/path/to/left-evidence.json /absolute/path/to/right-evidence.json
rea update
```
شغّل `npx -y rea-agents@latest --help` لمعرفة خيارات فك الترجمة المباشر والخيارات الأخرى.
لتحديث تسجيلات الوكلاء والمهارة، نفّذ أمر الإعداد الذي يعرضه التحديث.
يمكن لـ REA فتح مجلد `.app` على Mac مباشرة. إذا لم يعثر الوكيل على التطبيق، فأخبره بمكان تثبيته.
إذا كنت تستخدم `npx`، فحدّث إعداد وكيلك بالأمر التالي:
## سلوك Hopper
```bash
npx rea-agents@latest setup
```
يشغّل REA برنامج Hopper عند الحاجة. على macOS قد تظهر نافذته في المقدمة أو يحتاج أحد الحوارات إلى استجابة منك. لا يمكن ضمان بقاء النافذة في الخلفية.
راجع تغييرات الإعداد وأعد تشغيل وكيلك. لأوامر CLI التي تنفّذها مرة واحدة، استخدم `npx rea-agents@latest` متبوعًا بالأمر المطلوب.
يحدد REA تنسيق الهدف ومعماريته لتجنب حوارات الاختيار المعتادة. عند إغلاق الجلسة، يوقف الجسر وينظف موارده الخاصة، لكنه لا يغلق تطبيق Hopper الذي تستخدمه.
## كيف يعمل REA
## الأمان والخصوصية
يستدعي وكيلك REA عبر MCP لفحص الهدف وتتبع الشيفرة ذات الصلة. يعيد REA النتائج مع أدلتها. يستخدمها الوكيل لطرح أسئلة متابعة، أو شرح السلوك، أو كتابة تنفيذ واختباره. تستخدم أوامر CLI سير العمل نفسه.
- يبقى التحليل محليًا بين REA وموفر التحليل المختار عبر مقابس محلية خاصة، ومع IDA عبر تسجيل MCP المحلي المضبوط.
- لا يرفع REA الأهداف إلى خدمة مستضافة.
- تعمل أدوات التحليل والأهداف المطلقة بصلاحيات المستخدم نفسه؛ وما زال التقاط واجهة النظام على macOS يعتمد على الوصول إلى تسهيلات الاستخدام وتسجيل الشاشة. ولا ينفّذ التحليل الثابت للـ JavaScript الوحدات المستخرجة.
- تستخدم جلسات Ghidra مشروعًا مؤقتًا معزولًا ولا تفتح مشاريع Ghidra التي يملكها المستخدم أو تعدّلها.
- راجع الملفات التنفيذية غير الموثوقة واعزلها كما تفعل مع أي مدخل أصلي قد يكون ضارًا.
- أبلغ عن الثغرات وفق [سياسة الأمان](SECURITY.md)، وليس عبر قضية عامة.
![مسار الاستقصاء في REA: يسأل وكيلك عن هدف محلي، ويفحصه REA ويتتبعه بأدوات التحليل، ثم يستخدم الوكيل الشيفرة والمراجع والجوانب غير المحسومة التي أعادتها الأدوات لشرح السلوك وتنفيذه واختباره.](website/public/assets/figures/rea-investigation-flow.svg)
[افتح الشكل بحجمه الكامل](website/public/assets/figures/rea-investigation-flow.svg).
<a id="current-status"></a>
## ما الذي يمكنك تحليله
يتطلب REA إصدار Node.js 22.x (>=22.19) أو 24.x (>=24.11) أو 26+، إضافة إلى npm. تعتمد الأدوات الإضافية والأنظمة المضيفة المدعومة على الهدف:
| الهدف | ما يعيده REA | المتطلبات والدليل |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| الملفات الثنائية للشيفرة الأصلية | شيفرة شبه برمجية، وتعليمات تجميع، وسلاسل نصية، ورموز، واستدعاءات، ومراجع | Hopper أو Ghidra أو IDA؛ [تحليل الشيفرة الأصلية](https://rea.tools/guides/native/) |
| بنية ELF دون تشغيل | الأقسام والمقاطع والرموز ومعلومات إعادة التموضع الأصلية، وآليات الحماية المحتملة المستدلّ عليها بالفحص الثابت | pwntools يوفّره المستدعي على Linux x64؛ [تشخيص الملفات الثنائية](docs/binary-diagnostics.md) |
| شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا؛ [دليل EVM دون تشغيل](docs/evm-bytecode.md) |
| أعطال Linux المسجّلة | سجلات note الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](docs/recorded-crashes.md) |
| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](https://rea.tools/guides/javascript/) |
| المواقع | بنية الصفحة، والسكربتات، وملاحظات الشبكة، ولقطات الشاشة المطلوبة | متصفح من عائلة Chrome؛ [تحليل المتصفح](https://rea.tools/guides/browser/) |
| تسجيلات الشبكة المحفوظة | الطلبات، والاستجابات، ومحتويات الحمولة المتاحة، ومواضعها في المصدر | HAR؛ وmitmdump على Linux لتسجيلات mitmproxy بصيغتها الأصلية؛ [دليل تسجيلات الشبكة](docs/web-network-captures.md) |
| تجميعات .NET | البيانات الوصفية، وتعليمات CIL، واعتماديات الشيفرة الأصلية المعلنة، ومقارنات البناء | فحص ثابت؛ [دليل الشيفرة المُدارة](docs/managed-code-analysis.md) |
| حزم Android APK | تصريحات ملف manifest، والأصناف، والدوال الناتجة عن فك الترجمة، والمراجع | JADX دون واجهة رسومية وJDK كامل على Linux/macOS؛ [دليل Android](docs/android-analysis.md) |
| البرامج الثابتة | المناطق، ونتائج الاستخراج، وما يُمرّر إلى تحليل الشيفرة الأصلية | Binwalk / Unblob على Linux؛ [دليل البرامج الثابتة](docs/firmware-analysis.md) |
| الحزم والموارد | قوائم الملفات، والبصمات، وملفات plist، وبنية حزم Apple، والموارد المستخرجة | [دليل تحليل الملفات وJavaScript](docs/javascript-artifact-reconstruction.md)، [تطبيقات Apple](docs/apple-application-analysis.md) |
| سلوك العمليات | مخرجات الطرفية، والتفاعلات، وملاحظات الخروج ونظام الملفات، ومقارنات التشغيل | Linux/macOS مع PTY أصلي؛ [تسجيل سلوك العمليات](docs/process-capture.md) |
يقرأ الفحص الثابت لـ JavaScript و.NET الملفات المقدّمة دون تشغيل التطبيق. يشغّل التسجيل أثناء التنفيذ الهدف المحدد أو يتفاعل معه بصلاحيات مستخدمك؛ ويشرح كل دليل للتسجيل أثناء التنفيذ آثاره.
<a id="choosing-a-deep-analysis-provider"></a>
تختلف صيغ ملفات الشيفرة الأصلية والأنظمة المضيفة المدعومة باختلاف الموفّر. راجع [إعداد Hopper وGhidra](docs/installation.md#hopper)، و[دليل IDA](docs/ida-provider.md)، و[دعم Ghidra التجريبي على Windows](docs/windows-ghidra-p0.md). يدعم Ghidra أيضًا [تحليل DOS ذي 16 بت](docs/ghidra-dos.md). لاختيار الموفّر، راجع [دليل CLI](docs/cli.md#choose-a-provider). تحقّق من [توفر الميزات في الإصدارات](docs/installation.md#released-package-and-main) للميزات المضافة بعد أحدث إصدار على npm.
## دراسات الحالة
### DX-Ball: إعادة بناء حساب توزيع الصوت بين القناتين
تتبّع استدعاء صوت إلى الدالة المساعدة التي تحوّل الموضع إلى توزيع بين القناتين، وافحص التعليمات، وحوّل الشيفرة شبه البرمجية غير المكتملة إلى C. تجتاز إعادة البناء 3,205 حالات اختبار على x86 الأصلي، وتعيد إنتاج جميع بايتات الدالة المترجمة البالغ عددها 63 بايتًا.
[اقرأ دراسة الحالة](https://rea.tools/showcase/dx-ball/) ·
[مستودع إعادة البناء](https://github.com/N0zoM1z0/dx-ball)
### Notion: تتبّع جسر الحافظة في Electron
اعثر على API الحافظة في عملية العرض، وتتبّعه عبر preload وIPC إلى العملية الرئيسية، وافحص صيغة الحافظة ذات التنسيق الغني.
[اقرأ دراسة الحالة](https://rea.tools/showcase/notion/)
### TH04: استعادة حساب حلقة المقذوفات في DOS
افحص تعليمات اللعبة الأصلية على PC-98 ذات 16 بت، واستعد حساب الزوايا الثابتة والموجّهة نحو الهدف، وقارن شيفرة C++ المعاد بناؤها بمخرجات المترجم المستخدم آنذاك.
[اقرأ دراسة الحالة](https://rea.tools/showcase/th04/) ·
[مستودع إعادة البناء](https://github.com/N0zoM1z0/th04)
إذا استخدمت REA لفحص هدف مثير للاهتمام، فنود الاطلاع عليه. شارك حالتك في [issue](https://github.com/morluto/rea/issues) أو [pull request](https://github.com/morluto/rea/pulls)، مع ذكر الهدف، وسؤالك، وكيف ساعدك REA، وما توصلت إليه.
## الأسئلة الشائعة
<details>
<summary><strong>هل يتضمن REA محرك فك ترجمة خاصًا به؟</strong></summary>
<summary><strong>أي وكلاء يمكنهم استخدام REA؟</strong></summary>
لا. يستطيع Setup تثبيت Hopper، لكنه يظل برنامجًا منفصلًا بترخيص خاص به. يوفر REA واجهة CLI وخادم MCP وسير العمل المخصص للوكلاء.
أي وكيل يدعم خوادم MCP المحلية. يهيّئ الإعداد [الوكلاء المدعومين](docs/installation.md#supported-agents)؛ ويمكن للعملاء الآخرين استخدام [تسجيل MCP يدويًا](docs/installation.md#mcp-registry).
</details>
<details>
<summary><strong>هل يجب أن يكون Hopper مفتوحًا مسبقًا؟</strong></summary>
<summary><strong>هل أحتاج إلى Hopper أو Ghidra أو IDA؟</strong></summary>
لا. يستطيع REA تشغيله. لكن Hopper قد يصبح مرئيًا أو يطلب إدخالًا عند الحاجة إلى قرار تفاعلي.
يستخدم تحليل الشيفرة الأصلية المتعمق أحدها. يعمل الفحص الثابت لـ JavaScript و.NET دون محرك لتحليل الشيفرة الأصلية. يستطيع الإعداد تثبيت Hopper بعد الموافقة؛ أما Ghidra وIDA فيستخدمان تثبيتاتك الموجودة. راجع [إعداد الموفّرين](docs/installation.md#hopper).
</details>
<details>
<summary><strong>هل يعمل REA على Linux أو Windows؟</strong></summary>
<summary><strong>هل أحتاج إلى تشغيل Hopper أولًا؟</strong></summary>
يدعم REA نظام macOS 12+ وUbuntu 24.04+ وFedora 41+ وArch Linux بنواة 64 بت وCachyOS. يعمل تحليل Ghidra للقراءة فقط على Linux x64 وmacOS x64/arm64 مع Ghidra 12.1.x وJDK الكامل الذي يعلنه التثبيت (JDK 21 أو أحدث دون حد أعلى في إصدارات 12.1 الحالية)، إضافة إلى حد Windows x64 P0 التجريبي لتطبيقات PE الأصلية x86-64 على NTFS محلي؛ راجع [دليل Windows Ghidra P0](docs/windows-ghidra-p0.md) للنطاق الموثق.
يشغّل REA برنامج Hopper عندما تحتاج العملية إليه. على macOS، قد تظهر عند التشغيل الأول نافذة تطلب اختيار وضع العرض التجريبي أو تفعيل الترخيص. راجع [تشغيل Hopper واستكشاف الأخطاء](docs/installation.md#launcher-paths-and-troubleshooting).
</details>
<details>
<summary><strong>هل يمكنني تحليل قواعد Hopper المحفوظة؟</strong></summary>
<summary><strong>ماذا يفعل تثبيت المهارة من skills.sh؟</strong></summary>
نعم. يدعم وقت التشغيل الملفات التنفيذية وقواعد `.hop`.
توفّر المهارة تعليمات الاستقصاء لوكيلك. استخدم `rea setup` لتسجيل خادم MCP الخاص بـ REA وتثبيت التعليمات المطابقة، ثم أعد تشغيل وكيلك. راجع [تثبيت المهارة وحدها](docs/installation.md#skill-only-installation).
</details>
## التطوير
<details>
<summary><strong>ما الشيفرة التي يعيدها REA؟</strong></summary>
راجع [CONTRIBUTING.md](CONTRIBUTING.md) لإعداد بيئة التطوير وبنية المشروع والاختبارات وإرشادات الإصدار.
يعيد تحليل الشيفرة الأصلية شيفرة شبه برمجية وتعليمات تجميع. يستعيد تحليل JavaScript/Electron الوحدات والعلاقات بينها. يستخدم وكيلك هذه النتائج لكتابة تنفيذ واختباره؛ وتقدّم [دراسات الحالة](#دراسات-الحالة) أمثلة عملية.
## المشروع
</details>
- [حزمة npm](https://www.npmjs.com/package/rea-agents)
- [GitHub](https://github.com/morluto/rea)
- [القضايا والطلبات](https://github.com/morluto/rea/issues)
- [Hopper Disassembler](https://www.hopperapp.com/)
- [سياسة الأمان](SECURITY.md)
<details>
<summary><strong>هل يرفع REA تطبيقي؟</strong></summary>
يحلّل REA الأهداف محليًا. يتلقى وكيلك نتائج الأدوات، ولموفّر النموذج الذي يستخدمه سياسة بيانات خاصة به.
</details>
<details>
<summary><strong>ماذا أفعل إذا واجهت خطأ؟</strong></summary>
حدّث أولًا؛ فقد يكون إصدار حديث قد أصلح المشكلة بالفعل.
لواجهة CLI المثبّتة عبر npm:
```bash
rea update
```
لإعداد الوكيل عبر `npx`:
```bash
npx rea-agents@latest setup
```
إذا كنت تستخدم وكيلًا، فأكمل [تحديث الإعداد](#تحديث-rea) وأعد تشغيله. أعد محاولة المهمة نفسها. إذا استمرت المشكلة، [افتح issue](https://github.com/morluto/rea/issues) يتضمن إصدار REA، ونوع الهدف، وخطوات إعادة الإنتاج، ومخرجات الخطأ.
</details>
## التوثيق
ابدأ بـ [الأدلة العملية](https://rea.tools/guides/) على الموقع. للخيارات الدقيقة والمتطلبات وعقود النتائج:
- [التثبيت والإعداد](docs/installation.md): تسجيل الوكلاء، وتهيئة الموفّرين، والتحديثات، وإلغاء التثبيت.
- [الجاهزية واستكشاف الأخطاء](docs/installation.md#check-readiness-for-your-task): تشخيص وكيل أو محرك تحليل محدد.
- [CLI وEvidence](docs/cli.md): الأوامر، واختيار الموفّر، واللقطات، والاستيراد/التصدير، وحالات الخروج.
- [عقود MCP](docs/mcp-contracts.md) و[تعليمات الوكلاء](docs/mcp-prompts.md): نتائج الأدوات، والجلسات، والاستقصاءات الموجّهة.
- [كتالوج الأدوات](docs/mcp-contracts.md#generated-catalog): قائمة الأدوات والموفّرين وأوامر CLI المولّدة أثناء البناء.
- [خطة العمل](docs/roadmap.md): الأعمال المخطط لها ومتابعة القدرات.
أبلغ عن الثغرات وفق [SECURITY.md](SECURITY.md).
## المساهمة
نرحّب بمساعدتك في تطوير REA! [افتح issue](https://github.com/morluto/rea/issues) للإبلاغ عن خطأ أو اقتراح ميزة، أو [أرسل pull request](https://github.com/morluto/rea/pulls) لتحسين الشيفرة أو التوثيق.
راجع [CONTRIBUTING.md](CONTRIBUTING.md) لإعداد بيئة التطوير والفحوص، و[دليل الاختبار](docs/testing.md) لمسارات التحقق، و[خريطة البنية](docs/architecture.mermaid) لهيكل المشروع.
## روابط المشروع
[الموقع](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [الأمان](SECURITY.md)
## سجل النجوم
🎉 **20,000 نجمة على GitHub — شكرًا لكم!**
شكرًا لكل من يستخدم REA، ويبلّغ عن الأخطاء، ويختبر إصدارات البناء، ويساهم بالإصلاحات.
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date&amp;theme=dark&amp;legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date" />
<img alt="سجل نجوم REA على GitHub" src="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date" />
</picture>
</a>
## إخلاء المسؤولية
يوفّر REA أدوات لأبحاث الهندسة العكسية والتحليل وإعادة البناء المشروعة. أنت مسؤول عن الحصول على أي تفويض مطلوب والالتزام بالقوانين المعمول بها. لا يؤيد المشروع الاستخدام غير القانوني أو غير المصرّح به.
## الترخيص
+194 -226
View File
@@ -4,24 +4,33 @@
# REA:あらゆるものをリバースエンジニアリング
### エージェントでアプリの動作からネイティブバイナリまで調べる。
### バイナリ、アプリケーション、実行時の動作を、ひとつの MCP でリバースエンジニアリング。
**気になる機能を見つけ、仕組みを理解し、自分の形で実装する。**
**気に入った機能を見つけたら、その仕組みをバイナリのレベルまで理解する。**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#調査ツールカタログ)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
[![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
[![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM)
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
**[ウェブサイト(英語)](https://morluto.github.io/rea/) · [ガイド](https://morluto.github.io/rea/guides/) · [DX-Ball の事例](https://morluto.github.io/rea/showcase/dx-ball/)**
**[ウェブサイト](https://rea.tools/) · [ガイド](https://rea.tools/guides/) · [事例](https://rea.tools/showcase/)**
[クイックスタート](#クイックスタート) · [現在の対応状況](#現在の対応状況) · [バイナリから動作へ](#バイナリから動作へ) · [調査ツールカタログ](#調査ツールカタログ) · [ロードマップ](#ロードマップ) · [仕組み](#仕組み)
[クイックスタート](#クイックスタート) · [REA の仕組み](#rea-の仕組み) · [分析できる対象](#分析できる対象) · [事例](#事例) · [よくある質問](#よくある質問) · [ドキュメント](#ドキュメント)
<table aria-label="REA community">
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="REA が Hopper 内で分析ブリッジを起動し、ネイティブバイナリを調べる様子" width="1200" />
<br />
<table aria-label="REA コミュニティ">
<tr>
<td align="center" width="360">
<a href="https://discord.gg/GkcryMnJDM">
@@ -35,284 +44,243 @@
<br />
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="Hopper 内で解析ブリッジを起動し、ネイティブバイナリを調べる REA" width="1200" />
</div>
---
アプリの機能を自分のプロダクトにも取り入れたいときは、REA を使ってエージェントに調査を依頼できます。ソースコードがなくても、アプリを調べ、仕組みと根拠を示し、あなたのプロジェクト向けに同様の機能を実装できます。
アプリに、自分の製品にも取り入れたい機能を見つけましたか?エージェントに REA で調査を依頼してください。ソースコードがなくてもアプリを調べ、機能の仕組みを説明し、根拠を示したうえで、あなたのプロジェクト向けに実装できます。
REA はネイティブバイナリ、JavaScript/Electron アプリ、.NET アセンブリ、Web サイトの解析ツールを提供します。エージェントからもターミナルからも使えます。解析はローカルで実行され、結果には根拠と制限が含まれます。
REA はエージェントを分析ツールにつなぎ、ネイティブバイナリ、JavaScript や Electron のアプリ、.NET アセンブリ、ウェブサイトを調べられるようにします。同じツールをターミナルからも使えます。分析はローカルで実行され、結果には各結論を裏付ける根拠と制限事項が含まれます。
Setup はエージェントを設定し、既存の Hopper または Ghidra に接続します。承認後に Hopper をインストールすることもできます。
セットアップはエージェントに REA を登録し、対応するワークフローの手順をインストールします。ネイティブ分析には既存の Hopper または Ghidra を使えます。承認すれば、セットアップで Hopper をインストールすることもできます。静的な JavaScript 分析には、どちらのエンジンも必要ありません。
## エージェントに頼むだけ
[セットアップ](#クイックスタート)後にエージェントを再起動し、次のように依頼します:
```text
メモアプリの検索機能を調べて根拠を示し、私のプロジェクト向けに同様の機能を実装してください。
```
メモアプリを調べたいアプリに置き換えるか、まず概要を依頼してください。
## バイナリから動作へ
| 逆コンパイル | 理解 | 再現 |
| -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| ネイティブアプリや実行ファイルから、プロシージャ、疑似コード、アセンブリ、文字列、シンボル、セグメント、メタデータを復元します。 | 呼び出し元、呼び出し先、クロスリファレンス、コールグラフをたどり、機能やアルゴリズムの実際の動作を説明できる状態にします。 | エージェントが得た知見を、あなたの技術スタック、画面、要件に合うプロダクト機能へ変えます。 |
REA は調査をバイナリ上の根拠に結び付けます。元のソースコードを復元したり、アプリ全体を自動複製したりするとは主張しません。
## REA を選ぶ理由
| | |
| -------------------------- | ---------------------------------------------------------------------------------------------------- |
| **エージェント向け** | アプリの動作について質問し、推測ではなくエージェントに調査させることができます。 |
| **CLI と MCP** | ターミナルとエージェントから同じリバースエンジニアリング機能を使えます。 |
| **ガイド付きセットアップ** | エージェントの設定、既存の解析ツールへの接続、承認後の Hopper インストールに対応します。 |
| **洞察からコードへ** | 機能を理解したら、同じコーディングセッションで自分のプロダクト向けに実装できます。 |
| **ローカルで解析** | 解析は対応するローカルホストで実行され、REA がバイナリをホスト型解析サービスへ送ることはありません。 |
| **コンテキストを維持** | 質問ごとに解析を最初からやり直さず、複数のアプリを続けて調査できます。 |
> **[REA のウェブサイト](https://rea.tools/)**で、セットアップ手順、図解ガイド、実際の事例を確認できます。
## クイックスタート
### セットアップを実行(推奨)
### エージェントを設定する
REA をエージェントで使うための設定を行います:
Node.js と npm がインストールされている環境で実行します。
```bash
npx rea-agents setup
```
Setup は最初に連携するエージェントを複数選択できるようにします。既存の REA 登録は初期選択されますが、検出されただけのクライアントは自動選択されず、未設定のクライアントも選べます。具体的なパスと変更内容を確認してから承認してください。選択したエージェントには通常 REA のワークフローをインストールします。Hopper は別の任意操作で、個別の承認が必要です。既存の Ghidra のパスも登録できます。
使うエージェントを選び、予定されている変更を確認して承認してください。セットアップは REA の MCP サーバーと対応するワークフローの手順を追加し、既存の設定をバックアップします。完了後にエージェントを再起動してください。
変更は事前に表示され、既存の設定はバックアップされます。要件と追加オプションは[インストールガイド](docs/installation.md)を参照してください。
セットアップは Claude Code、Codex、Cursor、Gemini CLI、Grok Build、Grok Bot、および[その他のエージェント](docs/installation.md#supported-agents)に対応しています。プロバイダーの設定と MCP の手動登録については、[インストールとセットアップ](docs/installation.md)を参照してください。
### エージェントで使う
### エージェントに依頼する
設定後にエージェントを再起動し、調べたいアプリや機能を説明します。REA は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code、VS Code に対応します。既存の REA 登録は初期選択され、それ以外の検出済みクライアントは選択が必要です。その他のエージェントは下記の MCP 設定を使えます。
Hopper はデモモードで使えます。初回起動の画面ではデモを選択するか、既存のライセンスを入力してください。
### AI コーディングアシスタント向けスキル(任意)
AI コーディングアシスタントにスキルを追加すると、より豊富なコンテキストが得られます:
```bash
npx skills add morluto/rea --skill reverse-engineer-anything
```text
Notes アプリの検索機能の仕組みを調べ、根拠を示して、私のプロジェクトにも同様の機能を実装してください。
```
このスキルは REA の調査ワークフローを提供します。上記の setup を実行して REA をエージェントに接続し、解析ツールを設定してください。Setup は既定でバージョンに合ったスキルをインストールします。このコマンドではリポジトリ版のスキルをインストールします。
Notes を対象のアプリに置き換え、理解したい機能を指定してください。
展開済みの JavaScript/Electron アプリのツリーや ASAR であれば、MCP の設定もネイティブエンジンも不要です:
### ターミナルを使う
展開済みの JavaScript/Electron アプリのディレクトリ、または ASAR を調べます。
```bash
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
パスを調査対象に置き換えてください(Windows では `"D:/apps/example"` のようなパス)。この静的ワークフローではそのようなパスも扱えます。ネイティブ解析には専用のエンジン設定が必要です。
結果にはモジュール、インポート、Electron の境界と、それらの根拠が含まれます。パスは対象に合わせて変更してください。Windows では、たとえば `"D:/apps/example"` を指定します。
### rea コマンドをインストール
コマンドラインツールをインストールします:
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
```
Node.js と npm を先にインストールしてください。ターミナルで実行すると、インストーラーは `rea` を追加してセットアップを開始します。
npm でインストールし、セットアップを実行することもできます:
```bash
npm install --global rea-agents
rea setup
```
### 要件
- macOS 12 以降
- Ubuntu 24.04+、Fedora 41+、64 ビット Arch Linux、または CachyOS
- Node.js 22.x (>=22.19)、24.x (>=24.11)、または 26+ と npm
ネイティブバイナリ解析には Hopper または Ghidra が必要です。Hopper は別製品です。デモにはベンダー所定の制限がありますが、有料ライセンスは必須ではありません。
Ghidra は Linux x64 と macOS x64/arm64 に対応します。Ghidra 12.1.x と、そのインストールが宣言する完全な 64 ビット JDK(`application.java.min` から `application.java.max` まで)を別途インストールし、REA で使うように設定してください。現行の 12.1 リリースは JDK 21 以降を要求し、上限は設定しません。ブリッジは Ghidra 12.1.4 と JDK 21 で検証しています。macOS ではホストのアーキテクチャに合うネイティブデコンパイラーも必要です。
Setup はインストールを確認し、パスを保存できます。Ghidra、Java、Node.js、npm、Homebrew のインストールや更新は行いません。
リポジトリの main と npm 4.1.0 には、ローカル NTFS 上のネイティブ x86-64 PE アプリケーション(非マネージド、非 DLL 向け)を対象とする実験的な Windows x64 Ghidra P0 対応が含まれています。Job Object、プライベート DACL、パス許可の各制御が同梱されています。古い npm パッケージに期待する前に[リリース境界](docs/installation.md#released-package-and-main)を確認してください。前提条件と検証済みの範囲は [Windows Ghidra P0](docs/windows-ghidra-p0.md) を参照してください。
### トラブルシューティング
`npx -y rea-agents@latest doctor` はホスト、依存関係、解析ツール、エージェント設定を変更せずに確認します。構造化された診断には `--json` を追加してください。
Linux では実行可能な `/opt/hopper/bin/Hopper` を優先し、利用できない場合は `~/.local/share/rea/hopper/bin/Hopper` を自動的に確認します。それ以外の場所では `HOPPER_LAUNCHER_PATH` を設定します。ファイルがあるのに解析エンジンが見つからない場合は、実際の Hopper パスに対して `ldd /absolute/path/to/Hopper | grep 'not found'` を実行し、不足ライブラリを確認してください。詳しくは [Hopper ガイド](docs/installation.md#hopper)を参照してください。
### 更新とアンインストール
- `rea update` は現在の REA インストールを更新します。
- `rea uninstall` は REA が管理するエージェント登録とワークフローファイルを削除します。Hopper は残ります。
- `rea uninstall --purge-data` は REA のキャッシュと状態も削除します。それらを削除したい場合にだけ使ってください。
## 現在の対応状況
リポジトリの現在の機能とプラットフォーム要件は[英語の対応状況ガイド](README.md#current-status)で説明しています。main は [npm リリース](docs/installation.md#released-package-and-main)より先行する場合があります。
- Ghidra は Linux x64、macOS x64/arm64、実験的な Windows x64 P0 境界で 25 件の読み取り専用操作を提供します。Linux/macOS ではアトミックなセッション内関数注釈にも対応します。Windows P0 は読み取り専用であり、Ghidra に GUI 操作はありません。
- 静的な Android 検査には持ち込みの JADX/Java が別途必要です。実プロバイダー検証は Linux を対象とします。詳しくは[Android 解析](docs/android-analysis.md)を参照してください。
- ブラウザー、Electron、プロセスの各リクエストは対象・操作・ライフサイクルを直接指定し、ホストの実際のアクセスに依存します。REA 側の個別の許可付与は不要です。Setup による設定書き込みと Hopper のインストールは、引き続き正確な計画の承認が必要です。
- `rea capabilities` はバイナリセッション操作の説明であり、すべてのアプリ/ランタイムツールの目録ではありません。接続済み MCP ツール一覧と関連ガイドを参照してください。
## ひとつのプロンプトで調査を完結
```text
メモアプリをリバースエンジニアリングし、オフライン検索機能の仕組みを説明して、
TypeScript と SQLite を使って私のプロジェクト向けに実装してください。
```
| 手順 | エージェントの処理 | REA ツール |
| ---: | ------------------------------ | ---------------------------------------------------------------- |
| 1 | バイナリを開いて識別 | `open_binary`, `binary_overview` |
| 2 | オフライン検索の手掛かりを探す | `search_strings`, `search_procedures`, `list_names` |
| 3 | 手掛かりと実行コードを接続 | `find_xrefs_to_name`, `xrefs`, `procedure_callers` |
| 4 | 制御フローを復元 | `get_call_graph`, `procedure_callees`, `procedure_info` |
| 5 | 関連する処理を逆コンパイル | `procedure_pseudo_code`, `procedure_assembly`, `batch_decompile` |
| 6 | プロジェクトに機能を実装する | 技術スタック、プロダクト、要件に合わせたコード |
REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェントの通常の編集・テストツールが行います。
## エージェントにできること
- ソースコードがない機能の仕組みを説明する。
- アプリの認証、保存、更新、ネットワークフローを復元する。
- 非公開の形式やインターフェースを文書化できる構造を回収する。
- 文字列やシンボルから疑わしい動作の実装コードまで追跡する。
- 同じセッションで 2 バージョンを切り替え、実装経路を比較する。
- 気になる機能を調査し、自分のプロダクトに合わせた形で実装する。
- 復元した動作をプロダクト機能、テスト、移行ノート、移植、相互運用できる代替実装へ変換する。
- Swift / Objective-C のメタデータを解析する。
- Hopper に名前、コメント、ブックマークを残し、人間とエージェントの調査を共有する。
## 調査ツールカタログ
| ツール分類 | 数 | 用途 |
| -------------------- | --: | -------------------------------------------------------------------------------------------------------------------------------------------- |
| ネイティブ検査 | 41 | 関数、疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照、注釈、バイト読み取り、ファイルオフセット |
| 調査ワークフロー | 14 | アプリ概要、関数ドシエ、ネイティブ API とディスパッチ、一括逆コンパイル、機能トレース、コールパス、コールグラフ、Swift と Objective-C の検出 |
| macOS ネイティブ | 7 | Hopper を起動せずに扱える Mach-O メタデータ、署名、plist、アーキテクチャ、Swift 名の復元 |
| 成果物グラフ | 5 | ディレクトリとパッケージの目録、コンパイル済み Interface Builder ファイル、Apple アセットカタログ、抽出 |
| マネージド PE/CLI | 7 | .NET の識別、メタデータ、CIL 命令、ネイティブ依存関係、再構築インポート、ビルド比較 |
| ファームウェア | 2 | Linux ファームウェア領域の検査と明示的な展開 |
| Android APK | 5 | パッケージと manifest の宣言、クラス検索、メンバー一覧、メソッド逆コンパイル、静的参照 |
| ブラウザー観察 | 11 | ページ構成、ネットワークメタデータ、スクリプト、ソースマップ、WebMCP 検出、スクリーンショット、キャプチャ比較 |
| Electron 解析 | 5 | レンダラー観察、静的アプリマッピング、静的/実行時結果の対応付け |
| JavaScript 実行時 | 2 | Node/Electron Inspector 対象の検出、スクリプト位置、実行コンテキストイベント |
| アプリワークフロー | 13 | 取得した Web スクリプトのエクスポート、層をまたぐ機能トレース、ビルド比較、履歴ソース対応、静的戻り値形状の比較、再実装検証 |
| ワークスペースと観察 | 21 | セッション、証跡バンドル、ナビゲーション文脈、プロセス・成果物・関数比較、未解決事項の記録 |
## ロードマップ
次の作業はネイティブ対象の検証拡大、JavaScript と .NET のバージョン比較、実行時の観察と再実装の検証です。Setup はエージェント連携と Hopper のインストールを選択できます。他の解析ツールのインストールは今後の作業です。[インストールのロードマップ](docs/roadmap.md)と[解析ツールの評価](docs/provider-evaluation.md)を参照してください。
## 他のコーディングエージェントで使う
Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code、VS Code に対応します。既存の REA 登録は初期選択され、それ以外の検出済みクライアントは選択が必要です。ローカル MCP サーバーに対応するエージェントは、次の設定でも接続できます。
<!-- x-release-please-start-version -->
```json
{
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@5.0.0", "mcp"]
}
}
}
```
<!-- x-release-please-end -->
## 仕組み
```mermaid
flowchart LR
Agent["コーディングエージェント"] --> REA["REA<br/>CLI + MCP"]
Terminal["ターミナル"] --> REA
REA --> Hopper["Hopper 解析操作"]
Hopper --> App["あなたのアプリ"]
REA --> Ghidra["Ghidra 読み取り専用解析<br/>インベントリ + 関数解析"]
Ghidra --> App
```
CLI と MCP サーバーは同じ解析処理を使います。ターミナルコマンドは完了後に自分のブリッジセッションを解放し、エージェントセッションは調査中の接続を維持できます。REA セッションを閉じても、ユーザーが利用中の Hopper は終了しません。
## CLI
上のエージェントワークフローが、REA を使う最も簡単な方法です。ターミナルから一度だけアプリの概要を調べる場合は、次を実行します。
```bash
npx -y rea-agents@latest analyze /Applications/Notes.app
```
直接デコンパイルする方法やその他のオプションは、`npx -y rea-agents@latest --help` で確認できます。
グローバルな `rea` コマンドとしてもインストールできます。
日常的に使う場合は、`rea` コマンドをインストールできます。
```bash
npm install --global rea-agents
rea --help
rea mcp
```
REA は Mac の `.app` フォルダーを直接開けます。エージェントがアプリを見つけられない場合は、インストール場所を伝えてください。
ネイティブ分析には、先にプロバイダーの設定が必要です。ネイティブ分析のコマンド、プロバイダーの選択、スナップショット、スクリプトでの利用は、[CLI と Evidence のガイド](docs/cli.md)を参照してください。
## Hopper アプリの動作
### REA を更新する
REA は必要なときに Hopper を起動します。Hopper のランチャーは内部でアプリをアクティブ化するため、ターゲットを開くと Hopper が前面に出る場合があります。REA はバックグラウンド起動を要求しますが、常に背面に留まる保証はありません。
REA は更新が速く、新しいリリースには頻繁にバグ修正が含まれます。インストールしたバージョンを最新に保ってください。
明示的な形式・アーキテクチャ引数により一般的な FAT / ARM 選択ダイアログを避けますが、別の Hopper / macOS ダイアログは人の操作を必要とする場合があります。セッションを閉じるとブリッジとソケットを削除しますが、ユーザーが利用中の Hopper は終了しません。
npm でインストールした CLI の場合:
## セキュリティモデル
```bash
rea update
```
各セッションはランダムな capability token と現在のユーザーだけが使える Unix ソケットを使用します。Ghidra セッションは隔離された一時プロジェクトも使用し、ユーザー所有の Ghidra プロジェクトを開いたり変更したりしません。これはサンドボックスではなく、同じ OS ユーザー権限で動作する悪意あるプロセスを防御しません。信頼できないバイナリの解析は、現在のユーザー権限で選択されたローカルプロバイダーに委譲されます。脆弱性は [SECURITY.md](SECURITY.md) の非公開手順で報告してください。
エージェントの登録とスキルを更新するには、更新処理が表示するセットアップコマンドを実行してください。
## FAQ
`npx` を使っている場合は、次のコマンドでエージェントの設定を更新します。
<details><summary><strong>Hopper を先に起動する必要がありますか?</strong></summary>
```bash
npx rea-agents@latest setup
```
いいえ。REA が必要時に起動します。Hopper が起動済みでも使えますが、既存の GUI ドキュメントには接続せず、新しい解析ドキュメントを開きます。
設定の変更を確認し、エージェントを再起動してください。単発の CLI 操作では、`npx rea-agents@latest` に続けてコマンドを指定します。
## REA の仕組み
エージェントは MCP 経由で REA を呼び出し、対象を調べて関連するコードを追跡します。REA は調査結果と根拠を返します。エージェントはそれを使って追加の質問をしたり、動作を説明したり、実装を書いてテストしたりします。CLI コマンドも同じワークフローを使います。
![REA の調査フロー:エージェントがローカルの対象について質問し、REA が分析ツールで調査・追跡します。エージェントは返されたコード、参照、未解明の点を使い、説明、実装、テストを行います。](website/public/assets/figures/rea-investigation-flow.svg)
[原寸大の図を開く](website/public/assets/figures/rea-investigation-flow.svg)。
<a id="current-status"></a>
## 分析できる対象
REA には Node.js 22.x(>=22.19)、24.x(>=24.11)、または 26+ と npm が必要です。追加のツールや対応ホストは、対象によって異なります。
| 対象 | REA が返すもの | 要件とガイド |
| -------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](https://rea.tools/guides/native/) |
| オフライン ELF レイアウト | セクション、セグメント、元のシンボル/再配置情報、静的解析によるセキュリティ対策の候補 | Linux x64 で利用者が用意する pwntools;[バイナリ診断](docs/binary-diagnostics.md) |
| EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | ローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](docs/evm-bytecode.md) |
| 記録済みの Linux クラッシュ | 生の note レコード、記録された全スレッドのレジスター/シグナル、任意のマッピング候補 | 利用者が用意する pwntools;GDB/pwndbg は任意;[記録済みクラッシュ](docs/recorded-crashes.md) |
| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、IPC、ネイティブアドオンの関係 | Node.js と npm;[アプリケーション分析](https://rea.tools/guides/javascript/) |
| ウェブサイト | ページ構造、スクリプト、ネットワークの観測結果、依頼されたスクリーンショット | Chrome 系ブラウザー;[ブラウザー分析](https://rea.tools/guides/browser/) |
| 保存済みのネットワークキャプチャ | リクエスト、レスポンス、取得可能なペイロード、ソース内の位置 | HAR;mitmproxy ネイティブ形式のキャプチャには Linux 上の mitmdump;[キャプチャガイド](docs/web-network-captures.md) |
| .NET アセンブリ | メタデータ、CIL 命令、宣言されたネイティブ依存関係、ビルドの比較 | 静的な検査;[マネージドコードのガイド](docs/managed-code-analysis.md) |
| Android APK | マニフェストの宣言、クラス、逆コンパイルされたメソッド、参照 | Linux/macOS 上のヘッドレス JADX と完全な JDK;[Android ガイド](docs/android-analysis.md) |
| ファームウェア | 領域、抽出結果、ネイティブ分析への引き継ぎ | Linux 上の Binwalk / Unblob;[ファームウェアガイド](docs/firmware-analysis.md) |
| パッケージとリソース | ファイル一覧、ダイジェスト、plist、Apple バンドルの構造、抽出されたリソース | [アーティファクトと JavaScript のガイド](docs/javascript-artifact-reconstruction.md)、[Apple アプリケーション](docs/apple-application-analysis.md) |
| プロセスの動作 | ターミナル出力、対話、終了とファイルシステムの観測結果、実行の比較 | ネイティブ PTY を備えた Linux/macOS;[プロセスキャプチャ](docs/process-capture.md) |
静的な JavaScript と .NET の検査は、指定されたファイルを読み取り、アプリケーションを実行しません。実行時キャプチャは、あなたのユーザー権限で対象を実行したり、対象とやり取りしたりします。それぞれの実行時ガイドに、どのような操作が行われるかを記載しています。
<a id="choosing-a-deep-analysis-provider"></a>
対応するネイティブ形式とホストは、プロバイダーによって異なります。[Hopper と Ghidra のセットアップ](docs/installation.md#hopper)、[IDA ガイド](docs/ida-provider.md)、[Windows 向け Ghidra の実験的対応](docs/windows-ghidra-p0.md)を参照してください。Ghidra は [16 ビット DOS 分析](docs/ghidra-dos.md)にも対応しています。プロバイダーの選択は [CLI ガイド](docs/cli.md#choose-a-provider)を参照してください。最新の npm リリース以降に追加された機能については、[リリースでの利用可否](docs/installation.md#released-package-and-main)を確認してください。
## 事例
### DX-Ball:音声のパン計算を再構築する
音声の呼び出しから位置をパンに変換する補助関数をたどり、命令を調べ、不完全な疑似コードを C に書き直します。再構築した実装は、元の x86 に対する 3,205 ケースの検証を通過し、コンパイル後の関数の全 63 バイトを再現しています。
[事例を読む](https://rea.tools/showcase/dx-ball/) ·
[再構築リポジトリ](https://github.com/N0zoM1z0/dx-ball)
### Notion:Electron のクリップボードブリッジを追跡する
レンダラーのクリップボード API を見つけ、preload と IPC を経由してメインプロセスまでたどり、リッチなクリップボード形式を調べます。
[事例を読む](https://rea.tools/showcase/notion/)
### TH04:DOS の弾幕リングの計算を復元する
元の PC-98 ゲームの 16 ビット命令を調べ、固定角度と自機を狙う角度の計算を復元し、再構築した C++ を当時のコンパイラーの出力と比較します。
[事例を読む](https://rea.tools/showcase/th04/) ·
[再構築リポジトリ](https://github.com/N0zoM1z0/th04)
REA で興味深い対象を調べたら、ぜひ共有してください。[issue](https://github.com/morluto/rea/issues) または [pull request](https://github.com/morluto/rea/pulls) に、対象、調べたかったこと、REA がどう役立ったか、何が分かったかを記載してください。
## よくある質問
<details>
<summary><strong>どのエージェントで REA を使えますか?</strong></summary>
ローカル MCP サーバーに対応するエージェントなら使えます。セットアップは[対応エージェント](docs/installation.md#supported-agents)を設定します。その他のクライアントでは、[MCP の手動登録](docs/installation.md#mcp-registry)を利用できます。
</details>
<details><summary><strong>REA に Hopper は含まれますか?</strong></summary>
<details>
<summary><strong>Hopper、Ghidra、IDA は必要ですか?</strong></summary>
含まれません。Setup で Hopper をインストールできますが、Hopper は独自のライセンスを持つ別製品です。REA は CLI、MCP サーバー、エージェント向けワークフローを提供します。
深いネイティブ分析には、そのいずれかを使います。静的な JavaScript と .NET の検査には、ネイティブ分析エンジンは必要ありません。セットアップは承認後に Hopper をインストールできます。Ghidra と IDA は既存のインストールを使います。[プロバイダーのセットアップ](docs/installation.md#hopper)を参照してください。
</details>
<details><summary><strong>バイナリはアップロードされますか?</strong></summary>
<details>
<summary><strong>先に Hopper を起動する必要はありますか?</strong></summary>
REA にホスト型解析サービスはありません。ローカル Unix ソケット経由で Hopper を操作します。エージェントやモデル提供者のデータポリシーは別途確認してください。
REA は必要な操作の際に Hopper を起動します。macOS では、初回起動時にデモモードを選ぶか、ライセンスを有効化するよう求められることがあります。[Hopper の起動とトラブルシューティング](docs/installation.md#launcher-paths-and-troubleshooting)を参照してください。
</details>
<details><summary><strong>元のソースコードを復元できますか?</strong></summary>
<details>
<summary><strong>skills.sh からスキルをインストールすると何ができますか?</strong></summary>
保証できません。REA は疑似コード、アセンブリ、シンボル、文字列、メタデータ、関係を提供し、エージェントが観察した動作を説明または互換再現できるようにします。
スキルはエージェントに調査の手順を提供します。`rea setup` で REA の MCP サーバーを登録し、対応する手順をインストールしてから、エージェントを再起動してください。[スキルのみのインストール](docs/installation.md#skill-only-installation)を参照してください。
</details>
## 開発
<details>
<summary><strong>REA はどのようなコードを返しますか?</strong></summary>
開発環境、アーキテクチャ、テスト、リリース手順は [CONTRIBUTING.md](CONTRIBUTING.md) を参照してください。
ネイティブ分析は疑似コードとアセンブリを返します。JavaScript/Electron 分析はモジュールとその関係を復元します。エージェントはその結果を使って実装を書き、テストします。[事例](#事例)に具体的な手順があります。
</details>
<details>
<summary><strong>REA はアプリをアップロードしますか?</strong></summary>
REA は対象をローカルで分析します。エージェントはツールの結果を受け取り、そのモデルプロバイダーには独自のデータポリシーがあります。
</details>
<details>
<summary><strong>バグに遭遇したらどうすればよいですか?</strong></summary>
まず更新してください。最近のリリースですでに修正されている可能性があります。
npm でインストールした CLI の場合:
```bash
rea update
```
`npx` でエージェントを設定している場合:
```bash
npx rea-agents@latest setup
```
エージェントを使っている場合は、[設定の更新](#rea-を更新する)を完了し、再起動してください。同じタスクをもう一度試し、問題が続く場合は [issue を作成](https://github.com/morluto/rea/issues)して、REA のバージョン、対象の種類、再現手順、エラー出力を記載してください。
</details>
## ドキュメント
まずはウェブサイトの[実践ガイド](https://rea.tools/guides/)を参照してください。正確なオプション、前提条件、結果の契約については、以下を参照してください。
- [インストールとセットアップ](docs/installation.md):エージェントの登録、プロバイダーの設定、更新、アンインストール。
- [準備状況の確認とトラブルシューティング](docs/installation.md#check-readiness-for-your-task):特定のエージェントや分析エンジンを診断。
- [CLI と Evidence](docs/cli.md):コマンド、プロバイダーの選択、スナップショット、インポート/エクスポート、終了ステータス。
- [MCP 契約](docs/mcp-contracts.md)と[エージェント向けプロンプト](docs/mcp-prompts.md):ツールの結果、セッション、ガイド付き調査。
- [ツールカタログ](docs/mcp-contracts.md#generated-catalog):ビルド時に生成されるツール、プロバイダー、CLI コマンドの一覧。
- [ロードマップ](docs/roadmap.md):計画中の作業と機能の進捗。
脆弱性の報告は [SECURITY.md](SECURITY.md) に従ってください。
## 貢献する
REA への貢献を歓迎します![issue を作成](https://github.com/morluto/rea/issues)してバグや機能の提案を報告したり、[pull request を送信](https://github.com/morluto/rea/pulls)してコードやドキュメントを改善したりできます。
開発環境とチェックは [CONTRIBUTING.md](CONTRIBUTING.md)、検証レーンは[テストガイド](docs/testing.md)、プロジェクト構造は[アーキテクチャ図](docs/architecture.mermaid)を参照してください。
## プロジェクトリンク
[ウェブサイト](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [セキュリティ](SECURITY.md)
## スター履歴
🎉 **GitHub スター 20,000 件、ありがとうございます!**
REA の利用、バグ報告、ビルドのテスト、修正への貢献に感謝します。
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date&amp;theme=dark&amp;legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date" />
<img alt="REA の GitHub スター履歴" src="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date" />
</picture>
</a>
## 免責事項
REA は、合法的なリバースエンジニアリングの研究、分析、再構築のためのツールを提供します。必要な許可を得て、適用される法律に従う責任は利用者にあります。このプロジェクトは違法または無許可の利用を支持しません。
## ライセンス
+195 -228
View File
@@ -4,316 +4,283 @@
# REA: 무엇이든 리버스 엔지니어링
### 에이전트로 앱의 동작부터 네이티브 바이너리까지 조사하세요.
### 하나의 MCP로 바이너리, 애플리케이션, 런타임 동작을 리버스 엔지니어링합니다.
**마음에 드는 기능을 찾고, 작동 방식을 이해하고, 원하는 방식으로 구현하세요.**
**마음에 드는 기능을 발견했다면, 바이너리 수준까지 작동 원리를 이해하세요.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#조사-도구-카탈로그)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
[![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
[![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM)
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
**[웹사이트(영문)](https://morluto.github.io/rea/) · [가이드](https://morluto.github.io/rea/guides/) · [DX-Ball 사례](https://morluto.github.io/rea/showcase/dx-ball/)**
**[웹사이트](https://rea.tools/) · [가이드](https://rea.tools/guides/) · [사례](https://rea.tools/showcase/)**
[빠른 시작](#빠른-시작) · [현재 지원 범위](#현재-지원-범위) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [조사 도구 카탈로그](#조사-도구-카탈로그) · [로드맵](#로드맵) · [작동 방식](#작동-방식)
[빠른 시작](#빠른-시작) · [REA의 작동 방식](#rea의-작동-방식) · [분석할 수 있는 대상](#분석할-수-있는-대상) · [사례](#사례) · [자주 묻는 질문](#자주-묻는-질문) · [문서](#문서)
<table aria-label="REA community">
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="REA가 Hopper 안에서 분석 브리지를 실행해 네이티브 바이너리를 검사하는 모습" width="1200" />
<br />
<table aria-label="REA 커뮤니티">
<tr>
<td align="center" width="360">
<a href="https://discord.gg/GkcryMnJDM">
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
<strong>리버스 엔지니어링 커뮤니티에 참여하세요</strong>
</a><br />
<sub>Discord · 질문과 답변 · 작업 공유</sub>
<sub>Discord · 질문과 답변 · 결과 공유</sub>
</td>
</tr>
</table>
<br />
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="Hopper에서 분석 브리지를 시작하고 네이티브 바이너리를 조사하는 REA" width="1200" />
</div>
---
앱에서 마음에 드는 기능을 찾았다면 에이전트에게 REA로 조사해 달라고 요청하세요. 소스 코드가 없어도 앱을 분석하고, 기능의 동작과 근거를 설명한 뒤, 프로젝트에 맞는 기능을 구현할 수 있습니다.
앱에서 내 제품에도 넣고 싶은 기능을 발견했나요? 에이전트에게 REA로 조사해 달라고 요청하세요. 소스 코드가 없어도 앱을 검사하고, 기능의 작동 원리를 설명하고, 근거를 보여 주고, 프로젝트에 맞는 기능을 구현할 수 있습니다.
REA는 네이티브 바이너리, JavaScript/Electron 앱, .NET 어셈블리, 웹사이트 분석 도구를 제공합니다. 에이전트와 터미널에서 같은 도구를 사용할 수 있습니다. 분석은 로컬에서 실행되며 결과에는 근거와 제한 사항이 포함됩니다.
REA는 에이전트를 분석 도구에 연결해 네이티브 바이너리, JavaScript와 Electron 앱, .NET 어셈블리, 웹사이트를 검사할 수 있게 합니다. 터미널에서도 같은 도구를 사용할 수 있습니다. 분석은 로컬에서 실행되며, 결과에는 각 결론의 근거와 한계가 포함됩니다.
Setup은 에이전트를 설정하고 기존 Hopper 또는 Ghidra 설치에 연결합니다. 승인 후 Hopper를 설치할 수도 있습니다.
설정 과정은 에이전트에 REA를 등록하고 해당 버전에 맞는 워크플로 지침을 설치합니다. 네이티브 분석에는 기존 Hopper 또는 Ghidra 설치를 사용할 수 있습니다. 승인하면 설정 과정에서 Hopper를 설치할 수도 있습니다. 정적 JavaScript 분석에는 두 엔진 모두 필요하지 않습니다.
## 에이전트에게 바로 요청하기
[설정](#빠른-시작)을 마친 뒤 에이전트를 다시 시작하고 요청하세요.
```text
메모 앱의 검색 기능이 어떻게 동작하는지 조사하고 근거를 보여 주세요.
그다음 제 프로젝트에 비슷한 기능을 구현해 주세요.
```
메모 앱 대신 조사할 앱을 지정하거나 먼저 개요를 요청할 수 있습니다.
## 바이너리에서 동작까지
| 디컴파일 | 이해 | 재현 |
| ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| 네이티브 앱이나 실행 파일에서 프로시저, 의사 코드, 어셈블리, 문자열, 심볼, 세그먼트, 메타데이터를 복구합니다. | 호출자, 피호출자, 상호 참조, 호출 그래프를 따라 기능이나 알고리즘의 실제 동작을 설명합니다. | 에이전트가 배운 내용을 사용자의 기술 스택, 화면, 요구 사항에 맞는 제품 기능으로 만듭니다. |
REA는 분석을 바이너리 증거에 근거하게 합니다. 원본 소스 코드를 복원하거나 앱 전체를 자동으로 복제한다고 주장하지 않습니다.
## REA를 사용하는 이유
| | |
| ------------------- | --------------------------------------------------------------------------------------------------- |
| **에이전트용** | 앱의 동작에 대해 질문하고 추측 대신 에이전트가 조사하게 합니다. |
| **CLI와 MCP** | 터미널과 에이전트에서 동일한 리버스 엔지니어링 기능을 사용합니다. |
| **안내식 설정** | 에이전트 설정, 기존 분석 도구 연결, 승인 후 Hopper 설치를 지원합니다. |
| **통찰에서 코드로** | 기능을 이해한 뒤 같은 코딩 세션에서 자신의 제품에 맞는 버전을 구현합니다. |
| **로컬 분석** | 분석은 지원되는 로컬 호스트에서 실행되며 REA는 바이너리를 호스팅 분석 서비스에 업로드하지 않습니다. |
| **컨텍스트 유지** | 질문마다 분석을 처음부터 시작하지 않고 여러 앱을 연속으로 조사합니다. |
> **[REA 웹사이트](https://rea.tools/)**에서 설정 방법, 그림으로 설명한 가이드, 실제 사례를 확인하세요.
## 빠른 시작
### 설정 실행하기(권장)
### 에이전트 설정
REA를 에이전트에 연결하세요.
Node.js와 npm이 설치된 환경에서 다음 명령을 실행하세요.
```bash
npx rea-agents setup
```
Setup은 먼저 연결할 에이전트를 여러 개 선택하도록 안내합니다. 기존 REA 등록은 기본 선택되며, 감지만 된 클라이언트는 자동 선택되지 않습니다. 아직 설정되지 않은 클라이언트도 직접 선택할 수 있습니다. 정확한 경로와 변경 사항을 검토한 뒤 승인하세요. 선택한 에이전트에는 기본적으로 REA 워크플로가 설치됩니다. Hopper는 별도의 선택 사항이며 따로 승인해야 합니다. 기존 Ghidra 경로도 등록할 수 있습니다.
에이전트를 선택하고, 예정된 변경 사항을 검토한 뒤 승인하세요. 설정 과정은 기존 설정을 백업하고 REA의 MCP 서버와 버전에 맞는 워크플로 지침을 추가합니다. 완료 후 에이전트를 다시 시작하세요.
변경 전에 계획을 표시하고 기존 설정을 백업합니다. 요구 사항과 추가 옵션은 [설치 안내](docs/installation.md)를 참고하세요.
설정은 Claude Code, Codex, Cursor, Gemini CLI, Grok Build, Grok Bot 및 [다른 에이전트](docs/installation.md#supported-agents)를 지원합니다. 제공자 설정과 수동 MCP 등록은 [설치 및 설정](docs/installation.md)을 참고하세요.
### 에이전트에서 사용하기
### 에이전트에게 요청
설정 후 에이전트를 다시 시작하고 조사할 앱이나 기능을 설명하세요. REA는 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, Command Code, VS Code를 지원합니다. 기존 REA 등록은 기본 선택되고, 그 밖의 감지된 클라이언트는 직접 선택해야 합니다. 다른 에이전트는 아래 MCP 설정을 사용할 수 있습니다.
Hopper는 데모 모드로 사용할 수 있습니다. 첫 실행 안내가 나오면 데모를 선택하거나 기존 라이선스를 입력하세요.
### AI 코딩 어시스턴트용 스킬(선택)
AI 코딩 어시스턴트에 스킬을 추가하면 더 풍부한 컨텍스트를 얻을 수 있습니다.
```bash
npx skills add morluto/rea --skill reverse-engineer-anything
```text
Notes 앱의 검색 기능이 어떻게 작동하는지 조사하고, 근거를 보여 준 다음, 내 프로젝트에 비슷한 기능을 구현해 줘.
```
이 스킬은 REA의 조사 워크플로를 제공합니다. 위 setup을 실행해 REA를 에이전트에 연결하고 분석 도구를 설정하세요. Setup은 기본적으로 버전에 맞는 스킬을 설치합니다. 이 명령은 저장소 버전의 스킬을 설치합니다.
Notes를 대상 앱으로 바꾸고 이해하고 싶은 기능을 지정하세요.
압축을 해제한 JavaScript/Electron 앱 트리나 ASAR라면 MCP 설정이나 네이티브 엔진 없이 바로 분석할 수 있습니다.
### 터미널 사용
압축을 푼 JavaScript/Electron 앱 디렉터리 또는 ASAR를 검사합니다.
```bash
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
경로를 조사 대상으로 바꾸세요(Windows에서는 `"D:/apps/example"` 같은 경로). 이 정적 워크플로에서는 이런 경로도 지원됩니다. 네이티브 분석에는 전용 엔진 설정이 필요합니다.
결과에는 모듈, 임포트, Electron 경계와 관련 근거가 포함됩니다. 경로를 분석할 대상으로 바꾸세요. Windows에서는 `"D:/apps/example"` 같은 경로를 사용할 수 있습니다.
### rea 명령 설치하기
명령줄 도구를 설치하세요.
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
```
Node.js와 npm이 먼저 설치되어 있어야 합니다. 터미널에서 실행하면 설치 프로그램이 `rea`를 설치하고 설정을 시작합니다.
npm으로 설치한 뒤 설정을 실행할 수도 있습니다.
```bash
npm install --global rea-agents
rea setup
```
### 요구 사항
- macOS 12 이상
- Ubuntu 24.04+, Fedora 41+, 64비트 Arch Linux 또는 CachyOS
- Node.js 22.x (>=22.19), 24.x (>=24.11) 또는 26+와 npm
네이티브 바이너리 분석에는 Hopper 또는 Ghidra가 필요합니다. Hopper는 별도 소프트웨어입니다. 데모에는 공급업체의 제한이 있지만 유료 라이선스가 필수는 아닙니다.
Ghidra는 Linux x64와 macOS x64/arm64를 지원합니다. Ghidra 12.1.x와 그 설치본이 선언한 완전한 64비트 JDK(`application.java.min`부터 `application.java.max`까지)를 별도로 설치한 뒤 REA가 사용하도록 설정하세요. 현재 12.1 릴리스는 JDK 21 이상을 요구하고 상한은 없습니다. 브리지는 Ghidra 12.1.4와 JDK 21에서 검증됩니다. macOS에서는 호스트 아키텍처에 맞는 네이티브 디컴파일러도 필요합니다.
Setup은 설치를 확인하고 경로를 저장할 수 있습니다. Ghidra, Java, Node.js, npm, Homebrew를 설치하거나 업데이트하지 않습니다.
저장소 main과 npm 4.1.0에는 로컬 NTFS상의 네이티브 x86-64 PE 애플리케이션(비관리, 비 DLL 대상)을 위한 실험적인 Windows x64 Ghidra P0 지원이 포함되어 있습니다. Job Object, 비공개 DACL, 경로 승인 제어가 함께 제공됩니다. 이전 npm 패키지에 이 기능이 있을 것으로 기대하기 전에 [릴리스 경계](docs/installation.md#released-package-and-main)를 확인하세요. 전제 조건과 검증 범위는 [Windows Ghidra P0](docs/windows-ghidra-p0.md)를 참고하세요.
### 문제 해결
`npx -y rea-agents@latest doctor`는 호스트, 의존성, 분석 도구, 에이전트 설정을 변경 없이 확인합니다. 구조화된 진단에는 `--json`을 추가하세요.
Linux에서는 실행 가능한 `/opt/hopper/bin/Hopper`를 우선 사용하고, 사용할 수 없으면 `~/.local/share/rea/hopper/bin/Hopper`를 자동으로 확인합니다. 다른 위치에는 `HOPPER_LAUNCHER_PATH`를 설정하세요. 파일이 있는데도 분석 엔진이 없다고 하면 실제 Hopper 경로에 대해 `ldd /absolute/path/to/Hopper | grep 'not found'`를 실행해 누락된 라이브러리를 확인하세요. 자세한 내용은 [Hopper 안내](docs/installation.md#hopper)를 참고하세요.
### 업데이트와 제거
- `rea update`는 현재 REA 설치를 업데이트합니다.
- `rea uninstall`은 REA가 관리하는 에이전트 등록과 워크플로 파일을 제거합니다. Hopper는 유지합니다.
- `rea uninstall --purge-data`는 REA 캐시와 상태도 삭제합니다. 해당 데이터를 지우려는 경우에만 사용하세요.
## 현재 지원 범위
저장소의 현재 기능과 플랫폼 요구 사항은 [영문 지원 가이드](README.md#current-status)에 설명되어 있습니다. main은 [npm 릴리스](docs/installation.md#released-package-and-main)보다 앞설 수 있습니다.
- Ghidra는 Linux x64, macOS x64/arm64, 실험적인 Windows x64 P0 경계에서 25개의 읽기 전용 작업을 제공합니다. Linux/macOS에서는 원자적 세션 함수 주석도 추가로 지원합니다. Windows P0는 읽기 전용이며 Ghidra에는 GUI 제어가 없습니다.
- 정적 Android 검사는 별도로 준비한 JADX/Java가 필요합니다. 실제 제공자 검증은 Linux를 대상으로 합니다. [Android 분석](docs/android-analysis.md)을 참고하세요.
- 브라우저, Electron, 프로세스 요청은 대상·동작·수명 주기를 직접 지정하며 호스트의 실제 접근에 의존합니다. 별도의 REA 권한 승인은 필요 없습니다. Setup의 설정 쓰기와 Hopper 설치는 여전히 정확한 계획에 대한 승인이 필요합니다.
- `rea capabilities`는 모든 앱/런타임 도구의 목록이 아니라 바이너리 세션 작업을 설명합니다. 연결된 MCP 도구 목록과 관련 가이드를 참고하세요.
## 하나의 프롬프트로 전체 조사
```text
메모 앱을 리버스 엔지니어링해 오프라인 검색 기능의 작동 방식을 설명한 다음,
TypeScript와 SQLite를 사용해 제 프로젝트에 맞는 버전을 구현해 주세요.
```
| 단계 | 에이전트 작업 | REA 도구 |
| ---: | ----------------------- | ---------------------------------------------------------------- |
| 1 | 바이너리 열기 및 식별 | `open_binary`, `binary_overview` |
| 2 | 오프라인 검색 단서 찾기 | `search_strings`, `search_procedures`, `list_names` |
| 3 | 단서를 실행 코드에 연결 | `find_xrefs_to_name`, `xrefs`, `procedure_callers` |
| 4 | 제어 흐름 재구성 | `get_call_graph`, `procedure_callees`, `procedure_info` |
| 5 | 관련 루틴 디컴파일 | `procedure_pseudo_code`, `procedure_assembly`, `batch_decompile` |
| 6 | 프로젝트에 기능 구현 | 기술 스택, 제품, 요구 사항에 맞는 코드 |
REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이전트가 일반 파일 편집 및 테스트 도구로 수행합니다.
## 에이전트가 할 수 있는 일
- 소스 코드가 없는 기능의 동작을 설명합니다.
- 앱의 인증, 저장소, 업데이트 또는 네트워크 흐름을 재구성합니다.
- 문서화되지 않은 형식이나 인터페이스의 구조를 복구합니다.
- 문자열이나 심볼에서 의심스러운 동작을 구현한 코드까지 추적합니다.
- 한 세션에서 두 앱 버전을 전환하며 구현 경로를 비교합니다.
- 마음에 드는 기능을 조사하고 자신의 제품에 맞는 버전으로 구현합니다.
- 복구한 동작을 제품 기능, 테스트, 마이그레이션 문서, 포팅 또는 상호 운용 가능한 대체품으로 변환합니다.
- Swift 및 Objective-C 메타데이터를 분석합니다.
- Hopper에 이름, 주석, 북마크를 남겨 사람과 에이전트의 분석을 연결합니다.
## 조사 도구 카탈로그
| 도구 분류 | 수 | 용도 |
| ----------------- | --: | ------------------------------------------------------------------------------------------------------------------------- |
| 네이티브 검사 | 41 | 함수, 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조, 주석, 바이트 읽기, 파일 오프셋 |
| 조사 워크플로 | 14 | 앱 개요, 함수 기록서, 네이티브 API와 디스패치, 일괄 디컴파일, 기능 추적, 호출 경로, 호출 그래프, Swift와 Objective-C 탐색 |
| macOS 네이티브 | 7 | Hopper 실행 없이 처리하는 Mach-O 메타데이터, 서명, plist, 아키텍처, Swift 이름 복원 |
| 아티팩트 그래프 | 5 | 디렉터리와 패키지 목록, 컴파일된 Interface Builder 파일, Apple 애셋 카탈로그, 추출 |
| 관리 PE/CLI | 7 | .NET 식별, 메타데이터, CIL 명령어, 네이티브 종속성, 재구성 가져오기, 빌드 비교 |
| 펌웨어 | 2 | Linux 펌웨어 영역 검사 및 명시적 추출 |
| Android APK | 5 | 패키지와 manifest 선언, 클래스 검색, 멤버 목록, 메서드 디컴파일, 정적 참조 |
| 브라우저 관찰 | 11 | 페이지 구조, 네트워크 메타데이터, 스크립트, 소스 맵, WebMCP 탐색, 스크린샷, 캡처 비교 |
| Electron 분석 | 5 | 렌더러 관찰, 정적 앱 매핑, 정적·런타임 결과 연결 |
| JavaScript 런타임 | 2 | Node/Electron Inspector 대상 탐색, 스크립트 위치, 실행 컨텍스트 이벤트 |
| 앱 워크플로 | 13 | 캡처한 웹 스크립트 내보내기, 계층 간 기능 추적, 빌드 비교, 히스토리 소스 매핑, 정적 반환 구조 비교, 재구현 검증 |
| 작업 공간과 관찰 | 21 | 세션, 근거 번들, 탐색 컨텍스트, 프로세스·아티팩트·함수 비교, 미해결 항목 기록 |
## 로드맵
다음 작업은 네이티브 대상 검증 확대, JavaScript와 .NET의 버전 비교, 런타임 관찰과 재구현 검증입니다. Setup은 에이전트 연결과 Hopper 설치 선택을 이미 지원합니다. 다른 분석 도구 설치는 향후 작업입니다. [설치 로드맵](docs/roadmap.md)과 [분석 도구 평가](docs/provider-evaluation.md)를 참고하세요.
## 다른 코딩 에이전트에서 사용하기
Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, Command Code, VS Code를 지원합니다. 기존 REA 등록은 기본 선택되고, 그 밖의 감지된 클라이언트는 직접 선택해야 합니다. 로컬 MCP 서버를 지원하는 에이전트는 다음 설정으로 연결할 수 있습니다.
<!-- x-release-please-start-version -->
```json
{
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@5.0.0", "mcp"]
}
}
}
```
<!-- x-release-please-end -->
## 작동 방식
```mermaid
flowchart LR
Agent["코딩 에이전트"] --> REA["REA<br/>CLI + MCP"]
Terminal["터미널"] --> REA
REA --> Hopper["Hopper 분석 작업"]
Hopper --> App["사용자의 앱"]
REA --> Ghidra["Ghidra 읽기 전용 분석<br/>인벤토리 + 함수 분석"]
Ghidra --> App
```
CLI와 MCP 서버는 같은 분석 워크플로를 사용합니다. 터미널 명령은 완료 후 자신의 브리지 세션을 해제하고, 에이전트 세션은 조사 중 연결을 유지할 수 있습니다. REA 세션을 닫아도 사용자가 이용 중인 Hopper 앱은 종료하지 않습니다.
## CLI
위의 에이전트 워크플로가 REA를 사용하는 가장 쉬운 방법입니다. 터미널에서 앱을 한 번 빠르게 살펴보려면 다음을 실행하세요.
```bash
npx -y rea-agents@latest analyze /Applications/Notes.app
```
직접 디컴파일하는 방법과 기타 옵션은 `npx -y rea-agents@latest --help`에서 확인할 수 있습니다.
전역 `rea` 명령으로 설치할 수도 있습니다.
자주 사용할 경우 `rea` 명령을 설치하세요.
```bash
npm install --global rea-agents
rea --help
rea mcp
```
REA는 Mac의 `.app` 폴더를 직접 열 수 있습니다. 에이전트가 앱을 찾지 못하면 설치 위치를 알려 주세요.
네이티브 분석에는 먼저 제공자를 설정해야 합니다. 네이티브 명령, 제공자 선택, 스냅샷, 스크립트 사용은 [CLI 및 Evidence 가이드](docs/cli.md)를 참고하세요.
## Hopper 앱 동작
### REA 업데이트
REA는 필요할 때 Hopper를 시작합니다. Hopper 런처는 내부적으로 앱을 활성화하므로 대상을 열 때 Hopper가 다른 창 앞으로 나타날 수 있습니다. REA는 백그라운드 시작을 요청하지만 항상 뒤에 머무르는 것을 보장할 수 없습니다.
REA는 빠르게 바뀌며 새 릴리스에는 버그 수정이 자주 포함됩니다. 설치된 버전을 최신으로 유지하세요.
명시적인 형식과 아키텍처 인자로 일반적인 FAT/ARM 선택 대화상자를 피하지만, 다른 Hopper 또는 macOS 대화상자에는 사람이 응답해야 할 수 있습니다. 세션 종료 시 브리지와 소켓 디렉터리를 제거하지만 사용자가 작업 중인 Hopper 앱은 종료하지 않습니다.
npm으로 설치한 CLI에서는 다음 명령을 사용합니다.
## 보안 모델
```bash
rea update
```
각 세션은 무작위 capability token과 현재 사용자 전용 Unix 소켓을 사용합니다. Ghidra 세션은 격리된 임시 프로젝트도 사용하며 사용자 소유 Ghidra 프로젝트를 열거나 수정하지 않습니다. 이는 샌드박스가 아니며 같은 운영 체제 사용자로 실행되는 악성 프로세스를 방어하지 않습니다. 신뢰할 수 없는 바이너리를 열면 선택한 로컬 공급자가 현재 사용자 권한으로 분석합니다. 취약점은 [SECURITY.md](SECURITY.md)의 비공개 절차로 신고하세요.
에이전트 등록과 스킬을 갱신하려면 업데이트 과정에서 출력된 설정 명령을 실행하세요.
## FAQ
`npx`를 사용한다면 다음 명령으로 에이전트 설정을 갱신하세요.
<details><summary><strong>Hopper를 미리 실행해야 하나요?</strong></summary>
```bash
npx rea-agents@latest setup
```
아니요. REA가 필요할 때 시작합니다. Hopper가 실행 중이어도 사용할 수 있지만 기존 GUI 문서에 연결하는 대신 새 분석 문서를 엽니다.
설정 변경 사항을 검토하고 에이전트를 다시 시작하세요. 일회성 CLI 작업에는 `npx rea-agents@latest` 뒤에 필요한 명령을 붙여 실행하세요.
## REA의 작동 방식
에이전트는 MCP를 통해 REA를 호출하여 대상을 검사하고 관련 코드를 추적합니다. REA는 조사 결과와 근거를 반환합니다. 에이전트는 이를 바탕으로 추가 질문을 하고, 동작을 설명하거나, 구현을 작성하고 테스트합니다. CLI 명령도 같은 워크플로를 사용합니다.
![REA 조사 흐름: 에이전트가 로컬 대상에 대해 질문하면 REA가 분석 도구로 대상을 검사하고 추적합니다. 에이전트는 반환된 코드, 참조, 확인되지 않은 사항을 이용해 설명하고 구현하고 테스트합니다.](website/public/assets/figures/rea-investigation-flow.svg)
[원본 크기의 그림 열기](website/public/assets/figures/rea-investigation-flow.svg).
<a id="current-status"></a>
## 분석할 수 있는 대상
REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니다. 추가 도구와 호스트 지원은 대상에 따라 다릅니다.
| 대상 | REA가 반환하는 내용 | 요구 사항 및 가이드 |
| --------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| 네이티브 바이너리 | 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조 | Hopper, Ghidra 또는 IDA; [네이티브 분석](https://rea.tools/guides/native/) |
| 오프라인 ELF 레이아웃 | 섹션, 세그먼트, 원본 심볼/재배치 정보, 정적 분석으로 얻은 보안 완화 기법 후보 | Linux x64에서 호출자가 제공하는 pwntools; [바이너리 진단](docs/binary-diagnostics.md) |
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](docs/evm-bytecode.md) |
| 기록된 Linux 크래시 | 원시 note 레코드, 기록된 모든 스레드의 레지스터/시그널, 선택적 매핑 후보 | 호출자가 제공하는 pwntools; GDB/pwndbg는 선택 사항; [기록된 크래시](docs/recorded-crashes.md) |
| JavaScript / Electron | 모듈, 임포트, 소스 맵, 라우트, IPC, 네이티브 애드온 관계 | Node.js와 npm; [애플리케이션 분석](https://rea.tools/guides/javascript/) |
| 웹사이트 | 페이지 구조, 스크립트, 네트워크 관찰 결과, 요청한 스크린샷 | Chrome 계열 브라우저; [브라우저 분석](https://rea.tools/guides/browser/) |
| 저장된 네트워크 캡처 | 요청, 응답, 접근 가능한 페이로드, 소스 위치 | HAR; mitmproxy 네이티브 캡처에는 Linux의 mitmdump; [캡처 가이드](docs/web-network-captures.md) |
| .NET 어셈블리 | 메타데이터, CIL 명령, 선언된 네이티브 의존성, 빌드 비교 | 정적 검사; [관리 코드 가이드](docs/managed-code-analysis.md) |
| Android APK | 매니페스트 선언, 클래스, 디컴파일된 메서드, 참조 | Linux/macOS의 헤드리스 JADX와 전체 JDK; [Android 가이드](docs/android-analysis.md) |
| 펌웨어 | 영역, 추출 결과, 네이티브 분석으로 전달할 정보 | Linux의 Binwalk / Unblob; [펌웨어 가이드](docs/firmware-analysis.md) |
| 패키지 및 리소스 | 파일 목록, 다이제스트, plist, Apple 번들 구조, 추출한 리소스 | [아티팩트 및 JavaScript 가이드](docs/javascript-artifact-reconstruction.md), [Apple 애플리케이션](docs/apple-application-analysis.md) |
| 프로세스 동작 | 터미널 출력, 상호작용, 종료 및 파일 시스템 관찰 결과, 실행 비교 | 네이티브 PTY를 지원하는 Linux/macOS; [프로세스 캡처](docs/process-capture.md) |
정적 JavaScript 및 .NET 검사는 제공된 파일을 읽으며 애플리케이션을 실행하지 않습니다. 런타임 캡처는 사용자의 권한으로 선택한 대상을 실행하거나 대상과 상호작용합니다. 각 런타임 가이드에서 그 영향을 설명합니다.
<a id="choosing-a-deep-analysis-provider"></a>
지원하는 네이티브 형식과 호스트는 제공자마다 다릅니다. [Hopper 및 Ghidra 설정](docs/installation.md#hopper), [IDA 가이드](docs/ida-provider.md), [실험적 Windows Ghidra 지원](docs/windows-ghidra-p0.md)을 참고하세요. Ghidra는 [16비트 DOS 분석](docs/ghidra-dos.md)도 지원합니다. 제공자 선택은 [CLI 가이드](docs/cli.md#choose-a-provider)를 참고하세요. 최신 npm 릴리스 이후 추가된 기능은 [릴리스에서의 사용 가능 여부](docs/installation.md#released-package-and-main)를 확인하세요.
## 사례
### DX-Ball: 사운드 패닝 계산 재구현
사운드 호출에서 위치를 패닝 값으로 변환하는 보조 함수까지 추적하고, 명령을 검사하여 불완전한 의사 코드를 C로 옮깁니다. 재구현한 코드는 원본 x86에 대한 3,205개 테스트 케이스를 통과했으며, 컴파일된 함수의 63바이트 전체를 재현합니다.
[사례 읽기](https://rea.tools/showcase/dx-ball/) ·
[재구현 저장소](https://github.com/N0zoM1z0/dx-ball)
### Notion: Electron 클립보드 브리지 추적
렌더러의 클립보드 API를 찾고, preload와 IPC를 거쳐 메인 프로세스까지 추적한 뒤, 서식 있는 클립보드 형식을 검사합니다.
[사례 읽기](https://rea.tools/showcase/notion/)
### TH04: DOS 원형 탄막 계산 복원
원본 PC-98 게임의 16비트 명령을 검사하고 고정 각도와 조준 각도의 계산을 복원합니다. 재구현한 C++를 당시 컴파일러의 출력과 비교합니다.
[사례 읽기](https://rea.tools/showcase/th04/) ·
[재구현 저장소](https://github.com/N0zoM1z0/th04)
REA로 흥미로운 대상을 분석했다면 알려 주세요. [issue](https://github.com/morluto/rea/issues) 또는 [pull request](https://github.com/morluto/rea/pulls)에 대상, 조사하려던 질문, REA가 어떻게 도움이 되었는지, 무엇을 알아냈는지를 담아 공유하세요.
## 자주 묻는 질문
<details>
<summary><strong>어떤 에이전트에서 REA를 사용할 수 있나요?</strong></summary>
로컬 MCP 서버를 지원하는 에이전트라면 사용할 수 있습니다. 설정 과정은 [지원하는 에이전트](docs/installation.md#supported-agents)를 구성합니다. 다른 클라이언트는 [수동 MCP 등록](docs/installation.md#mcp-registry)을 이용할 수 있습니다.
</details>
<details><summary><strong>REA에 Hopper가 포함되나요?</strong></summary>
<details>
<summary><strong>Hopper, Ghidra 또는 IDA가 필요한가요?</strong></summary>
아니요. Setup이 Hopper를 설치할 수 있지만 Hopper는 자체 라이선스가 필요한 별도 소프트웨어입니다. REA는 CLI, MCP 서버, 에이전트용 워크플로를 제공합니다.
심층 네이티브 분석에는 이 중 하나를 사용합니다. 정적 JavaScript 및 .NET 검사에는 네이티브 분석 엔진이 필요하지 않습니다. 설정 과정에서 승인 후 Hopper를 설치할 수 있으며, Ghidra와 IDA는 기존 설치를 사용합니다. [제공자 설정](docs/installation.md#hopper)을 참고하세요.
</details>
<details><summary><strong>바이너리가 업로드되나요?</strong></summary>
<details>
<summary><strong>Hopper를 먼저 실행해야 하나요?</strong></summary>
REA에는 호스팅 분석 서비스가 없습니다. 로컬 Unix 소켓을 통해 Hopper를 조작합니다. 에이전트나 모델 공급자의 데이터 정책은 별도로 확인하세요.
REA는 작업에 필요할 때 Hopper를 실행합니다. macOS에서는 처음 실행할 때 데모 모드를 선택하거나 라이선스를 활성화하라는 대화 상자가 나타날 수 있습니다. [Hopper 실행 및 문제 해결](docs/installation.md#launcher-paths-and-troubleshooting)을 참고하세요.
</details>
<details><summary><strong>원본 소스 코드를 복구할 수 있나요?</strong></summary>
<details>
<summary><strong>skills.sh에서 설치하는 스킬은 어떤 역할을 하나요?</strong></summary>
보장할 수 없습니다. REA는 의사 코드, 어셈블리, 심볼, 문자열, 메타데이터 및 관계를 제공하여 에이전트가 관찰된 동작을 설명하거나 호환되게 재현하도록 돕습니다.
스킬은 에이전트에게 조사 지침을 제공합니다. `rea setup`으로 REA의 MCP 서버를 등록하고 버전에 맞는 지침을 설치한 뒤 에이전트를 다시 시작하세요. [스킬만 설치하기](docs/installation.md#skill-only-installation)를 참고하세요.
</details>
## 개발
<details>
<summary><strong>REA는 어떤 코드를 반환하나요?</strong></summary>
개발 환경, 아키텍처, 테스트, 릴리스 지침은 [CONTRIBUTING.md](CONTRIBUTING.md)를 참고하세요.
네이티브 분석은 의사 코드와 어셈블리를 반환합니다. JavaScript/Electron 분석은 모듈과 그 관계를 복원합니다. 에이전트는 이 결과를 사용해 구현을 작성하고 테스트합니다. [사례](#사례)에서 구체적인 예를 볼 수 있습니다.
</details>
<details>
<summary><strong>REA가 내 앱을 업로드하나요?</strong></summary>
REA는 대상을 로컬에서 분석합니다. 에이전트는 도구 결과를 받으며, 에이전트의 모델 제공자에는 별도의 데이터 정책이 있습니다.
</details>
<details>
<summary><strong>버그가 발생하면 어떻게 해야 하나요?</strong></summary>
먼저 업데이트하세요. 최근 릴리스에서 이미 수정되었을 수 있습니다.
npm으로 설치한 CLI에서는 다음 명령을 사용합니다.
```bash
rea update
```
`npx`로 에이전트를 설정했다면 다음 명령을 사용합니다.
```bash
npx rea-agents@latest setup
```
에이전트를 사용 중이라면 [설정 갱신](#rea-업데이트)을 완료하고 다시 시작하세요. 같은 작업을 다시 시도하세요. 문제가 계속되면 REA 버전, 대상 유형, 재현 단계, 오류 출력을 포함해 [issue를 등록](https://github.com/morluto/rea/issues)하세요.
</details>
## 문서
웹사이트의 [실습 가이드](https://rea.tools/guides/)부터 읽어 보세요. 정확한 옵션, 사전 요구 사항, 결과 계약은 다음 문서를 참고하세요.
- [설치 및 설정](docs/installation.md): 에이전트 등록, 제공자 설정, 업데이트, 제거.
- [준비 상태 확인 및 문제 해결](docs/installation.md#check-readiness-for-your-task): 특정 에이전트 또는 분석 엔진 진단.
- [CLI 및 Evidence](docs/cli.md): 명령, 제공자 선택, 스냅샷, 가져오기/내보내기, 종료 상태.
- [MCP 계약](docs/mcp-contracts.md) 및 [에이전트 프롬프트](docs/mcp-prompts.md): 도구 결과, 세션, 안내에 따른 조사.
- [도구 카탈로그](docs/mcp-contracts.md#generated-catalog): 빌드 시 생성되는 도구, 제공자, CLI 명령 목록.
- [로드맵](docs/roadmap.md): 계획된 작업과 기능 진행 상황.
취약점은 [SECURITY.md](SECURITY.md)에 따라 보고하세요.
## 기여
REA에 도움을 보태 주세요! [issue를 등록](https://github.com/morluto/rea/issues)해 버그나 기능을 제안하거나, [pull request를 보내](https://github.com/morluto/rea/pulls) 코드와 문서를 개선할 수 있습니다.
개발 환경과 검사 항목은 [CONTRIBUTING.md](CONTRIBUTING.md), 검증 절차는 [테스트 가이드](docs/testing.md), 프로젝트 구조는 [아키텍처 지도](docs/architecture.mermaid)를 참고하세요.
## 프로젝트 링크
[웹사이트](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [보안](SECURITY.md)
## 스타 기록
🎉 **GitHub 스타 20,000개, 감사합니다!**
REA를 사용하고, 버그를 보고하고, 빌드를 테스트하고, 수정에 기여해 주신 모든 분께 감사드립니다.
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date&amp;theme=dark&amp;legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date" />
<img alt="REA GitHub 스타 기록" src="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date" />
</picture>
</a>
## 면책 조항
REA는 합법적인 리버스 엔지니어링 연구, 분석, 재구성을 위한 도구를 제공합니다. 필요한 권한을 얻고 관련 법률을 준수할 책임은 사용자에게 있습니다. 이 프로젝트는 불법적이거나 허가받지 않은 사용을 지지하지 않습니다.
## 라이선스
+193 -227
View File
@@ -2,26 +2,35 @@
[English](README.md) · **简体中文** · [日本語](README_ja.md) · [한국어](README_ko.md) · [العربية](README_ar.md)
# REA:逆向工程一切
# REA:逆向分析一切
### 使用智能体逆向工程任何程序,从应用行为到原生二进制文件。
### 通过一个 MCP 服务,逆向分析二进制文件、应用程序和运行时行为。
**看到喜欢的功能。理解它的原理。按照你的方式实现。**
**看到喜欢的功能,弄清它的工作原理,深入到二进制层面。**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#调查工具目录)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
[![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
[![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM)
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
**[网站(英文)](https://morluto.github.io/rea/) · [使用指南](https://morluto.github.io/rea/guides/) · [DX-Ball 案例](https://morluto.github.io/rea/showcase/dx-ball/)**
**[网站](https://rea.tools/) · [指南](https://rea.tools/guides/) · [案例](https://rea.tools/showcase/)**
[快速开始](#快速开始) · [当前状态](#当前状态) · [从二进制到行为](#从二进制到行为) · [调查工具目录](#调查工具目录) · [路线图](#路线图) · [工作原理](#工作原理)
[快速开始](#快速开始) · [REA 的工作原理](#rea-的工作原理) · [可以分析什么](#可以分析什么) · [案例](#案例) · [常见问题](#常见问题) · [文档](#文档)
<table aria-label="REA community">
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="REA 在 Hopper 中启动分析桥,检查原生二进制文件" width="1200" />
<br />
<table aria-label="REA 社区">
<tr>
<td align="center" width="360">
<a href="https://discord.gg/GkcryMnJDM">
@@ -35,286 +44,243 @@
<br />
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="REA 在 Hopper 中启动分析桥并检查原生二进制文件" width="1200" />
</div>
---
看到某个应用中想加入自己产品的功能?让智能体用 REA 调查它。即使没有源代码,智能体也可以检查应用、解释功能的工作方式、展示证据,并为你的项目实现类似功能。
在应用中看到一个想加入自己产品的功能?让智能体用 REA 调查它。即使没有源代码,智能体也可以检查应用、解释功能的工作方式、展示证据,并为你的项目实现类似功能。
REA 提供原生二进制、JavaScript 与 Electron 应用、.NET 程序集和网站的分析工具。你可以从智能体或终端使用它们。分析在本机运行,结果会说明证据和限制。
REA 将智能体连接到分析工具,用于检查原生二进制文件、JavaScript 和 Electron 应用、.NET 程序集以及网站。你也可以在终端中使用同样的工具。分析在本机运行,结果包含支撑各项结论的证据和相关限制。
Setup 可以配置智能体、连接已安装的 Hopper 或 Ghidra,也可以在你批准后安装 Hopper。
设置流程会向智能体注册 REA,并安装匹配的工作流指引。原生分析可以使用已有的 Hopper 或 Ghidra;设置流程也可以在你批准后安装 Hopper。静态 JavaScript 分析不需要这两个引擎。
## 直接询问智能体
完成[设置](#快速开始)后,重启智能体并提出问题:
```text
解释“备忘录”应用的搜索功能,展示证据,然后为我的项目实现类似功能。
```
把“备忘录”替换成你想了解的应用,也可以先请求概览。
## 从二进制到行为
| 反编译 | 理解 | 重建 |
| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| 打开原生应用或可执行文件,恢复过程、伪代码、汇编、字符串、符号、段和元数据。 | 沿调用者、被调用者、交叉引用和调用图追踪,直到智能体能够解释功能或算法的实际工作方式。 | 将智能体学到的内容变成适合你的技术栈、界面和需求的产品功能。 |
REA 让调查始终以二进制证据为依据。它不会声称能恢复原始源代码,也不会自动克隆整个应用。
## 为什么选择 REA
| | |
| ---------------- | -------------------------------------------------------------------- |
| **为智能体设计** | 直接询问应用的行为,让智能体去检查,而不是猜测。 |
| **CLI 与 MCP** | 在终端或智能体中使用同一套逆向工程能力。 |
| **引导式设置** | 配置智能体、连接已有分析工具,或在你批准后安装 Hopper。 |
| **从洞察到代码** | 理解功能后,在同一次编程会话中构建属于你自己的版本。 |
| **本地运行** | 分析在受支持的本机系统上运行;REA 不会把二进制上传到托管式分析服务。 |
| **保留上下文** | 连续调查多个应用,无需为每个问题重新开始整个分析过程。 |
> **[访问 REA 网站](https://rea.tools/)**,查看设置说明、图解指南和真实案例。
## 快速开始
### 运行设置(推荐)
### 设置智能体
配置 REA 与智能体的连接:
安装 Node.js 和 npm 后,运行:
```bash
npx rea-agents setup
```
Setup 首先让你多选要连接的智能体。已有 REA 注册默认选中;仅被检测到的客户端不会自动选中,也可以手动选择尚未配置的客户端。查看具体路径和变更后再批准。所选智能体默认安装 REA 工作流;Hopper 是单独的可选操作,需要单独批准。Setup 也可以记录现有 Ghidra 的路径。
选择智能体,检查计划中的变更并批准。设置流程会添加 REA 的 MCP 服务和匹配的工作流指引,并备份已有配置。完成后重启智能体。
Setup 会先展示变更并备份已有配置。要求与更多选项见[安装与设置](docs/installation.md)。
设置流程支持 Claude Code、Codex、Cursor、Gemini CLI、Grok Build、Grok Bot 和[其他智能体](docs/installation.md#supported-agents)。提供方配置和手动注册 MCP 的方法见[安装与设置](docs/installation.md)。
### 使用智能体
### 询问智能体
设置完成后,重启智能体并描述你想了解的应用或功能。REA 支持 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code 和 VS Code。已有 REA 注册默认选中;其他检测到的客户端需手动选择。其他智能体可以使用下方的 MCP 配置。
Hopper 支持演示模式。如果首次启动时出现提示,选择演示模式或输入已有许可证。
### AI 编程助手技能(可选)
为你的 AI 编程助手添加 skill,以获得更丰富的上下文:
```bash
npx skills add morluto/rea --skill reverse-engineer-anything
```text
弄清 Notes 应用的搜索功能如何工作,展示证据,并为我的项目实现类似功能。
```
该 skill 提供 REA 的调查工作流。先运行上文的 setup,把 REA 连接到你的智能体并配置分析工具。Setup 默认会安装版本匹配的 skill;这条命令安装的是仓库版本。
把 Notes 替换为目标应用,并描述你想了解的功能。
对于已解包的 JavaScript/Electron 应用目录或 ASAR,无需 MCP 配置,也无需原生引擎:
### 使用终端
检查已解包的 JavaScript/Electron 应用目录或 ASAR 文件:
```bash
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
把路径换成你的目标(例如 Windows 上的 `"D:/apps/example"`)。这条静态流程支持此类路径。原生分析仍需要先配置对应的分析引擎。
结果包含模块、导入、Electron 边界及相关证据。将路径替换为你的目标路径,例如 Windows 上的 `"D:/apps/example"`。
### 安装 rea 命令
安装命令行工具:
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
```
需要预先安装 Node.js 和 npm。在终端中运行时,安装程序会安装 `rea` 并启动设置。
也可以用 npm 安装,再运行设置:
```bash
npm install --global rea-agents
rea setup
```
### 系统要求
- macOS 12 或更高版本
- Ubuntu 24.04+、Fedora 41+、64 位 Arch Linux 或 CachyOS
- Node.js 22.x (>=22.19)、24.x (>=24.11) 或 26+,以及 npm
原生二进制分析需要 Hopper 或 Ghidra。Hopper 是独立软件;演示模式有厂商规定的限制,不要求购买许可证。
Ghidra 支持 Linux x64 和 macOS x64/arm64。单独安装 Ghidra 12.1.x,以及该安装在 `application.java.min` 到 `application.java.max` 中声明的完整 64 位 JDK,然后配置 REA 使用它们。当前 12.1 发行版要求 JDK 21 或更新版本,并且不设置上限。桥接在 Ghidra 12.1.4 和 JDK 21 上完成验证。macOS 还需要对应架构的原生反编译器。
Setup 可以验证安装并保存路径,不会安装或升级 Ghidra、Java、Node.js、npm 或 Homebrew。
仓库 main 分支与 npm 4.1.0 已包含实验性的 Windows x64 Ghidra P0 支持,适用于本地 NTFS 上的原生 x86-64 PE 应用(非托管、非 DLL),并附带 Job Object、私有 DACL 与路径准入控制。请先确认[发布边界](docs/installation.md#released-package-and-main),再假定旧版 npm 包具备该能力。详见 [Windows Ghidra P0](docs/windows-ghidra-p0.md) 的前置条件与已验证范围。
### 故障排除
运行 `npx -y rea-agents@latest doctor`,检查主机、依赖、分析工具和智能体配置。该命令不会修改文件。添加 `--json` 可获取结构化诊断。
Linux 上 REA 会优先使用可执行的 `/opt/hopper/bin/Hopper`;如果不可用,则自动检查 `~/.local/share/rea/hopper/bin/Hopper`。其他路径可通过 `HOPPER_LAUNCHER_PATH` 指定。如果文件存在但仍报告缺少分析引擎,请对实际 Hopper 路径运行 `ldd /absolute/path/to/Hopper | grep 'not found'` 检查缺少的系统库。安装详情见 [Hopper 指南](docs/installation.md#hopper)。
### 更新与卸载
- `rea update` 更新当前 REA 安装。
- `rea uninstall` 移除 REA 管理的智能体配置项和工作流文件,保留 Hopper。
- `rea uninstall --purge-data` 还会删除 REA 的缓存和状态。仅在需要移除这些数据时使用。
## 当前状态
仓库当前能力与平台要求见[英文支持指南](README.md#current-status)。main 分支可能领先于 [npm 发布版](docs/installation.md#released-package-and-main)。
- Ghidra 在 Linux x64、macOS x64/arm64 以及实验性的 Windows x64 P0 边界上提供 25 个只读操作。Linux/macOS 额外支持原子化的会话级函数注释。Windows P0 为只读;Ghidra 不提供 GUI 控制。
- 静态 Android 检查需要自备 JADX/Java,前置条件独立;真机验证覆盖 Linux。详见 [Android 分析](docs/android-analysis.md)。
- 浏览器、Electron 与进程请求需直接声明目标、动作与生命周期,并依赖宿主的实际访问权限,无需 REA 另行授权。Setup 的配置写入与 Hopper 安装仍需逐项批准确切计划。
- `rea capabilities` 描述的是二进制会话操作,而非全部应用/运行时工具。请以已连接 MCP 工具列表与相关指南为准。
## 一个提示词,完成一次完整调查
```text
逆向工程“备忘录”应用,找到离线搜索功能的工作方式,解释其控制流,
并使用 TypeScript 和 SQLite 为我的项目构建一个版本。
```
| 步骤 | 智能体的操作 | REA 工具 |
| ---: | ---------------------- | ---------------------------------------------------------------- |
| 1 | 打开并识别二进制文件 | `open_binary`, `binary_overview` |
| 2 | 搜索可能的离线搜索线索 | `search_strings`, `search_procedures`, `list_names` |
| 3 | 将线索连接到可执行代码 | `find_xrefs_to_name`, `xrefs`, `procedure_callers` |
| 4 | 重建相关控制流 | `get_call_graph`, `procedure_callees`, `procedure_info` |
| 5 | 反编译相关程序 | `procedure_pseudo_code`, `procedure_assembly`, `batch_decompile` |
| 6 | 在你的项目中构建该功能 | 适合你的技术栈、产品和需求的代码 |
REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常规文件编辑与测试工具完成。
## 智能体可以完成什么
- 在没有源代码时解释某项功能的实现方式。
- 重建应用的身份验证、存储、更新或网络流程。
- 恢复足够的结构,以记录未公开的格式或接口。
- 从字符串或符号追踪到实现可疑行为的代码。
- 在一个会话中切换两个应用版本并比较实现路径。
- 调查你喜欢的功能,并为自己的产品构建量身定制的版本。
- 将恢复的行为转换为产品功能、测试、迁移说明、移植代码或互操作替代品。
- 分析 Swift 和 Objective-C 元数据。
- 在 Hopper 中留下名称、注释与书签,使人与智能体的分析互相增强。
## 调查工具目录
| 工具类别 | 数量 | 用途 |
| ----------------- | ---: | ------------------------------------------------------------------------------------------------------ |
| 原生检查 | 41 | 函数、伪代码、汇编、字符串、符号、调用、引用、注释、字节读取与文件偏移 |
| 调查工作流 | 14 | 应用概览、函数档案、原生 API 与分发、批量反编译、功能追踪、调用路径与调用图、Swift 与 Objective-C 发现 |
| macOS 原生工具 | 7 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原,无需启动 Hopper |
| 产物图 | 5 | 目录与软件包清单、编译后的 Interface Builder 文件、Apple 资源目录与提取 |
| 托管 PE/CLI | 7 | .NET 程序集身份、元数据、CIL 指令、原生依赖声明、重建导入与构建比较 |
| 固件 | 2 | Linux 固件区域检查与显式提取 |
| Android APK | 5 | 包与 manifest 声明、类搜索、成员清单、方法反编译与静态引用 |
| 浏览器观察 | 11 | 页面结构、网络元数据、脚本、来源映射、WebMCP 发现、截图与捕获比较 |
| Electron 分析 | 5 | 渲染进程观察、静态应用映射、静态/运行时结果关联 |
| JavaScript 运行时 | 2 | Node/Electron Inspector 目标发现、脚本位置与执行上下文事件 |
| 应用工作流 | 13 | 捕获的网站脚本导出、跨层功能追踪、构建比较、历史源码映射、静态返回结构比较与重建验证 |
| 工作区与观察 | 21 | 会话、证据包、导航上下文、进程/产物/函数比较与待解决问题跟踪 |
## 路线图
接下来的工作包括更多原生目标验证、JavaScript 与 .NET 跨版本比较,以及运行时观察和重建验证。当前设置已支持选择智能体集成和 Hopper 安装;安装其他分析工具仍是后续工作。详见[安装路线图](docs/roadmap.md)和[分析工具评估](docs/provider-evaluation.md)。
## 与其他编程智能体一起使用
Setup 支持 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code 和 VS Code。已有 REA 注册默认选中;其他检测到的客户端需手动选择。任何支持本地 MCP 服务器的智能体都可以使用以下配置连接 REA。
<!-- x-release-please-start-version -->
```json
{
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@5.0.0", "mcp"]
}
}
}
```
<!-- x-release-please-end -->
## 工作原理
```mermaid
flowchart LR
Agent["编程智能体"] --> REA["REA<br/>CLI + MCP"]
Terminal["终端"] --> REA
REA --> Hopper["Hopper 分析操作"]
Hopper --> App["你的应用"]
REA --> Ghidra["Ghidra 只读分析<br/>清单 + 函数分析"]
Ghidra --> App
```
CLI 与 MCP 服务器使用相同的分析流程。终端命令完成后释放自己创建的桥接会话;智能体会话可以在调查期间保持连接。关闭 REA 会话不会退出用户正在使用的 Hopper 应用。
## CLI
上面的智能体工作流是使用 REA 最简单的方式。如果只想在终端中快速了解一个应用:
```bash
npx -y rea-agents@latest analyze /Applications/Notes.app
```
运行 `npx -y rea-agents@latest --help` 查看直接反编译和其他选项。
也可以全局安装 `rea` 命令:
如果需要经常使用,可以安装 `rea` 命令:
```bash
npm install --global rea-agents
rea --help
rea mcp
```
REA 可以直接打开 Mac 的 `.app` 文件夹。如果智能体找不到应用,请告诉它应用安装在哪里。
原生分析需要先配置分析提供方。原生命令、提供方选择、快照和脚本用法见 [CLI 与 Evidence 指南](docs/cli.md)。
## Hopper 应用行为
### 更新 REA
REA 会在需要时启动 Hopper,无需预先运行。Hopper 启动器内部会激活应用,因此打开目标时 Hopper 可能出现在其他窗口前。REA 会请求 macOS 在后台启动 Hopper,但无法保证窗口始终位于后台。
REA 更新较快,新版本经常包含错误修复。请保持安装的版本为最新。
REA 会推导明确的格式和架构参数,以避免常见的 FAT 与 ARM 选择对话框。其他 Hopper 或 macOS 对话框仍可能需要人工响应。关闭 REA 会话会终止桥并删除私有套接字目录,但不会退出用户正在使用的 Hopper 应用。
通过 npm 安装的 CLI 使用:
## 安全模型
```bash
rea update
```
每个桥会话都使用随机能力令牌和仅限当前用户的 Unix 套接字。诊断保留本机路径、摘要和错误位置等排查信息,同时移除凭据和认证令牌。Ghidra 会话还使用隔离的临时项目,不会打开或修改用户自己的 Ghidra 项目。
要更新智能体注册和 skill,请运行更新命令输出的设置命令。
这不是沙箱,也无法防御以同一操作系统用户身份运行的恶意进程。打开不可信二进制文件会让所选本地提供商以当前用户权限进行解析和分析。请按照 [SECURITY.md](SECURITY.md) 中的私密流程报告漏洞。
如果使用 `npx`,通过以下命令更新智能体设置:
```bash
npx rea-agents@latest setup
```
检查设置变更并重启智能体。单次 CLI 操作可使用 `npx rea-agents@latest`,后接所需命令。
## REA 的工作原理
智能体通过 MCP 调用 REA,检查目标并追踪相关代码。REA 返回发现及对应证据。智能体据此提出后续问题、解释行为,或编写并测试实现。CLI 命令使用同样的工作流。
![REA 调查流程:智能体询问本地目标的行为,REA 使用分析工具检查和追踪目标,智能体再利用返回的代码、引用和未知项进行解释、实现与测试。](website/public/assets/figures/rea-investigation-flow.svg)
[打开完整尺寸的图示](website/public/assets/figures/rea-investigation-flow.svg)。
<a id="current-status"></a>
## 可以分析什么
REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。额外工具和主机支持取决于分析目标:
| 目标 | REA 返回的内容 | 要求与指南 |
| --------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](https://rea.tools/guides/native/) |
| 离线 ELF 布局 | 节、段、原始符号/重定位信息及静态防护机制候选项 | Linux x64 上由调用方提供的 pwntools;[二进制诊断](docs/binary-diagnostics.md) |
| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | 本地原始字节/十六进制输入载体;[离线 EVM 指南](docs/evm-bytecode.md) |
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | 调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](docs/recorded-crashes.md) |
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](https://rea.tools/guides/javascript/) |
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](https://rea.tools/guides/browser/) |
| 已保存的网络捕获 | 请求、响应、可访问的载荷和来源位置 | HAR;原生 mitmproxy 捕获需要 Linux 上的 mitmdump;[捕获指南](docs/web-network-captures.md) |
| .NET 程序集 | 元数据、CIL 指令、声明的原生依赖和构建对比 | 静态检查;[托管代码指南](docs/managed-code-analysis.md) |
| Android APK | 清单声明、类、反编译的方法和引用 | Linux/macOS 上的无界面 JADX 和完整 JDK;[Android 指南](docs/android-analysis.md) |
| 固件 | 区域、提取结果和转交原生分析的内容 | Linux 上的 Binwalk / Unblob;[固件指南](docs/firmware-analysis.md) |
| 软件包与资源 | 文件清单、摘要、plist、Apple bundle 结构和提取的资源 | [制品与 JavaScript 指南](docs/javascript-artifact-reconstruction.md)、[Apple 应用](docs/apple-application-analysis.md) |
| 进程行为 | 终端输出、交互、退出和文件系统观察结果,以及运行对比 | 支持原生 PTY 的 Linux/macOS;[进程捕获](docs/process-capture.md) |
静态 JavaScript 和 .NET 检查读取提供的文件,不运行应用。运行时捕获会以你的用户权限运行目标或与之交互;各运行时指南说明了具体影响。
<a id="choosing-a-deep-analysis-provider"></a>
不同提供方支持的原生格式和主机系统有所不同。请参阅 [Hopper 与 Ghidra 设置](docs/installation.md#hopper)、[IDA 指南](docs/ida-provider.md)和[实验性 Windows Ghidra 支持](docs/windows-ghidra-p0.md)。Ghidra 也支持 [16 位 DOS 分析](docs/ghidra-dos.md)。提供方选择见 [CLI 指南](docs/cli.md#choose-a-provider)。对于最新 npm 版本发布后新增的功能,请查看[发布可用性](docs/installation.md#released-package-and-main)。
## 案例
### DX-Ball:重建声像计算
沿声音调用追踪到根据位置计算声像的辅助函数,检查指令,将不完整的伪代码转为 C。重建结果通过了 3,205 个原始 x86 测试用例,并复现了编译后函数的全部 63 个字节。
[阅读案例](https://rea.tools/showcase/dx-ball/) ·
[重建项目仓库](https://github.com/N0zoM1z0/dx-ball)
### Notion:追踪 Electron 剪贴板桥
找到渲染进程的剪贴板 API,沿 preload 和 IPC 追踪到主进程,并检查富格式剪贴板数据。
[阅读案例](https://rea.tools/showcase/notion/)
### TH04:恢复 DOS 环形子弹的计算
检查原始 PC-98 游戏的 16 位指令,恢复固定角度和瞄准角度的计算,并将重建的 C++ 与当年的编译器输出进行对比。
[阅读案例](https://rea.tools/showcase/th04/) ·
[重建项目仓库](https://github.com/N0zoM1z0/th04)
如果你用 REA 分析了有趣的目标,我们很想了解。欢迎通过 [issue](https://github.com/morluto/rea/issues) 或 [pull request](https://github.com/morluto/rea/pulls) 分享案例,说明目标、你的问题、REA 如何提供帮助,以及你的发现。
## 常见问题
<details><summary><strong>Hopper 是否需要提前运行?</strong></summary>
<details>
<summary><strong>哪些智能体可以使用 REA?</strong></summary>
不需要。REA 会在操作需要时启动 Hopper。Hopper 已经运行时也可以使用,但 REA 会打开新的分析文档,不会连接并复用现有 GUI 文档。
任何支持本地 MCP 服务的智能体都可以使用。设置流程会配置[受支持的智能体](docs/installation.md#supported-agents);其他客户端可通过[手动注册 MCP](docs/installation.md#mcp-registry) 使用。
</details>
<details><summary><strong>REA 是否包含 Hopper?</strong></summary>
<details>
<summary><strong>需要 Hopper、Ghidra 或 IDA 吗?</strong></summary>
不包含。Setup 可以为你安装 Hopper,但 Hopper 仍是需要单独授权的软件。REA 提供 CLI、MCP 服务器和面向智能体的工作流。
深入的原生分析需要其中一个引擎。静态 JavaScript 和 .NET 检查不需要原生分析引擎。设置流程可以在你批准后安装 Hopper;Ghidra 和 IDA 则使用已有安装。见[提供方设置](docs/installation.md#hopper)。
</details>
<details><summary><strong>REA 会上传我的二进制文件吗?</strong></summary>
<details>
<summary><strong>需要先启动 Hopper 吗?</strong></summary>
REA 不提供托管分析服务,而是通过本地 Unix 套接字把操作交给 Hopper。你的智能体或模型服务商可能有自己的数据政策,请单独核查。
REA 会在操作需要时启动 Hopper。在 macOS 上,首次运行时可能会出现对话框,让你选择演示模式或激活许可证。见 [Hopper 启动与故障排查](docs/installation.md#launcher-paths-and-troubleshooting)。
</details>
<details><summary><strong>REA 能恢复原始源代码吗?</strong></summary>
<details>
<summary><strong>从 skills.sh 安装 skill 有什么作用?</strong></summary>
不能保证。REA 提供伪代码、汇编、符号、字符串、元数据和关系,智能体可据此解释或兼容地重建观察到的行为。
skill 为智能体提供调查指引。使用 `rea setup` 注册 REA 的 MCP 服务并安装匹配的指引,然后重启智能体。见[仅安装 skill](docs/installation.md#skill-only-installation)。
</details>
## 开发
<details>
<summary><strong>REA 返回什么代码?</strong></summary>
开发环境、架构、测试和发布说明请参阅 [CONTRIBUTING.md](CONTRIBUTING.md)。
原生分析返回伪代码和汇编。JavaScript/Electron 分析恢复模块及其关系。智能体利用这些发现编写并测试实现;[案例](#案例)提供了完整示例。
</details>
<details>
<summary><strong>REA 会上传我的应用吗?</strong></summary>
REA 在本机分析目标。智能体会接收工具结果,其模型提供商有自己的数据政策。
</details>
<details>
<summary><strong>遇到错误时该怎么办?</strong></summary>
先更新;最近的版本可能已经修复了问题。
通过 npm 安装的 CLI 使用:
```bash
rea update
```
通过 `npx` 设置智能体时使用:
```bash
npx rea-agents@latest setup
```
如果正在使用智能体,请完成[设置更新](#更新-rea)并重启智能体。重试同一任务。如果问题仍然存在,请[提交 issue](https://github.com/morluto/rea/issues),附上 REA 版本、目标类型、复现步骤和错误输出。
</details>
## 文档
可以先阅读网站上的[实操指南](https://rea.tools/guides/)。具体选项、前提条件和结果契约见:
- [安装与设置](docs/installation.md):智能体注册、提供方配置、更新和卸载。
- [就绪检查与故障排查](docs/installation.md#check-readiness-for-your-task):诊断特定智能体或分析引擎。
- [CLI 与 Evidence](docs/cli.md):命令、提供方选择、快照、导入/导出和退出状态。
- [MCP 契约](docs/mcp-contracts.md)和[智能体提示词](docs/mcp-prompts.md):工具结果、会话和引导式调查。
- [工具目录](docs/mcp-contracts.md#generated-catalog):构建时生成的工具、提供方和 CLI 命令清单。
- [路线图](docs/roadmap.md):计划中的工作和能力跟踪。
请按照 [SECURITY.md](SECURITY.md) 报告漏洞。
## 参与贡献
欢迎帮助改进 REA!你可以[提交 issue](https://github.com/morluto/rea/issues) 报告错误或建议功能,也可以[提交 pull request](https://github.com/morluto/rea/pulls) 改进代码或文档。
开发设置和检查见 [CONTRIBUTING.md](CONTRIBUTING.md),验证流程见[测试指南](docs/testing.md),项目结构见[架构图](docs/architecture.mermaid)。
## 项目链接
[网站](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [安全](SECURITY.md)
## Star 历史
🎉 **GitHub Star 达到 20,000 个,感谢大家!**
感谢每一位使用 REA、报告错误、测试构建和贡献修复的朋友。
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date&amp;theme=dark&amp;legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date" />
<img alt="REA 的 GitHub Star 历史" src="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date" />
</picture>
</a>
## 免责声明
REA 为合法的逆向工程研究、分析和重建提供工具。你有责任取得所需授权并遵守适用法律。项目不支持非法或未经授权的使用。
## 许可证
+136 -45
View File
@@ -52,6 +52,8 @@ import ghidra.app.decompiler.ClangCaseToken;
import ghidra.app.decompiler.ClangNode;
import ghidra.framework.Application;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressIterator;
import ghidra.program.model.address.AddressSet;
import ghidra.program.model.address.AddressSpace;
import ghidra.program.model.address.AddressRange;
import ghidra.program.model.address.AddressRangeIterator;
@@ -723,8 +725,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
if (!direction.equals("incoming") && !direction.equals("outgoing")) {
throw new RequestFailure("invalid_request", "direction must be incoming or outgoing");
}
InstructionScan scan = scanInstructions(function, Integer.MAX_VALUE);
List<Reference> references = collectReferences(scan.instructions, direction);
List<Reference> references = collectReferences(function, direction);
JsonArray edges = new JsonArray();
for (Reference reference : references) {
edges.add(referenceEdge(reference));
@@ -735,7 +736,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
result.add("references", edges);
result.addProperty("reference_kinds_available", true);
JsonArray unresolvedCalls = new JsonArray();
if (direction.equals("outgoing")) for (Instruction instruction : scan.instructions) {
if (direction.equals("outgoing")) for (Instruction instruction : scanInstructions(function, Integer.MAX_VALUE).instructions) {
if (!instruction.getFlowType().isCall()) continue;
boolean resolved = false;
for (Reference reference : instruction.getReferencesFrom()) if (reference.getReferenceType().isCall()) resolved = true;
@@ -1007,6 +1008,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
Address entry = function.getEntryPoint();
if (function.isExternal() || !currentProgram.getMemory().contains(entry))
throw new RequestFailure("invalid_request", "Annotations require a local function entry: " + canonicalAddress(entry));
for (String field : List.of("name", "comment", "inline_comment")) {
if (params.has(field)) validateAnnotationText(requireText(params, field), field);
}
String leafName = params.has("name")
? annotationLeafName(function, requireString(params, "name")) : null;
int transaction = currentProgram.startTransaction("REA function annotations");
boolean commit = false;
try {
@@ -1020,7 +1026,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
}
}
if (params.has("name"))
function.setName(requireString(params, "name"), SourceType.USER_DEFINED);
function.setName(leafName, SourceType.USER_DEFINED);
monitor.checkCancelled();
invalidateAnalysisCaches();
JsonObject readback = annotationReadback(function);
@@ -1028,7 +1034,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
if (!params.has(field)) continue;
String requested = requireText(params, field);
JsonElement measured = readback.get(field);
String expected = !field.equals("name") && requested.isEmpty() ? null : requested;
String expected = field.equals("name") ? leafName : requested.isEmpty() ? null : requested;
String observed = field.equals("name") ? function.getName() :
(measured.isJsonNull() ? null : measured.getAsString());
if (!java.util.Objects.equals(expected, observed))
@@ -1058,6 +1064,22 @@ public final class ReaGhidraBridge extends HeadlessScript {
}
}
private String annotationLeafName(Function function, String requested) {
// Fully qualified readback must be usable as an idempotent rename input.
if (requested.equals(procedureName(function))) return function.getName();
String namespace = function.getParentNamespace().isGlobal()
? "" : function.getParentNamespace().getName(true);
String prefix = namespace + "::";
if (!namespace.isEmpty() && requested.startsWith(prefix)) {
String leaf = requested.substring(prefix.length());
if (!leaf.isEmpty()) return leaf;
throw new RequestFailure("invalid_function_name",
"Function name has an empty leaf within namespace " + namespace + " at " +
canonicalAddress(function.getEntryPoint()) + ": " + requested);
}
return requested;
}
private static String requireText(JsonObject params, String field) {
JsonElement value = params.get(field);
if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString())
@@ -1065,6 +1087,21 @@ public final class ReaGhidraBridge extends HeadlessScript {
return value.getAsString();
}
private static void validateAnnotationText(String text, String field) {
for (int index = 0; index < text.length(); index++) {
char character = text.charAt(index);
if (character == 0)
throw new RequestFailure("invalid_request", "Annotation " + field + " contains NUL at UTF-16 index " + index + "; Ghidra's decompiler cannot marshal NUL text");
if (Character.isHighSurrogate(character) && index + 1 < text.length() &&
Character.isLowSurrogate(text.charAt(index + 1))) {
index++;
continue;
}
if (Character.isSurrogate(character))
throw new RequestFailure("invalid_request", "Annotation " + field + " contains an unpaired Unicode surrogate at UTF-16 index " + index + "; Ghidra cannot preserve it as annotation text");
}
}
private JsonObject analyzeFunction(JsonObject params) throws Exception {
requireKeys(params, Set.of("procedure"));
Function function = resolveProcedure(requireString(params, "procedure"));
@@ -1074,11 +1111,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
if (pseudocode == null) {
pseudocode = "";
}
List<Reference> incomingReferences = collectReferences(scan.instructions, "incoming")
List<Reference> incomingReferences = collectReferences(function, "incoming")
.stream()
.filter(reference -> !function.getBody().contains(reference.getFromAddress()))
.toList();
List<Reference> outgoingReferences = collectReferences(scan.instructions, "outgoing");
List<Reference> outgoingReferences = collectReferences(function, "outgoing");
JsonArray incoming = referenceEdges(incomingReferences);
JsonArray outgoing = referenceEdges(outgoingReferences);
JsonArray comments = comments(scan.instructions);
@@ -1471,6 +1508,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
: regexMatcher(expression, caseSensitive);
JsonArray items = new JsonArray();
for (InventoryItem item : inventory) {
monitor.checkCancelled();
if (!matcher.matches(item.value)) {
continue;
}
@@ -1581,33 +1619,37 @@ public final class ReaGhidraBridge extends HeadlessScript {
}
private Function resolveProcedure(String value) throws Exception {
Address address = tryParseAddress(value);
if (address != null) {
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
if (function == null && address.isMemoryAddress()) {
function = currentProgram.getFunctionManager().getFunctionContaining(address);
// Explicit address syntax wins; an unprefixed identifier is first an
// exact symbol name, even when every character is a hexadecimal digit.
boolean explicitAddress = value.matches("(?i)^0x[0-9a-f]+$") ||
value.matches("(?i)^(?:[a-z0-9._~-]|%[0-9a-f]{2})+:0x[0-9a-f]+$");
if (!explicitAddress) {
List<Function> matches = new ArrayList<>();
for (FunctionEntry entry : procedures()) {
monitor.checkCancelled();
Function function = entry.function;
Symbol symbol = function.getSymbol();
String qualified = symbol == null ? function.getName() : symbol.getName(true);
if (function.getName().equals(value) || qualified.equals(value)) {
matches.add(function);
}
}
if (function == null) {
throw new RequestFailure("not_found", "No procedure exists at the requested address");
}
return function;
if (matches.size() > 1)
throw new RequestFailure("ambiguous", "Ghidra procedure name is ambiguous: " + value +
"; select an exact entry address from " + matches.stream()
.map(function -> canonicalAddress(function.getEntryPoint())).toList());
if (matches.size() == 1) return matches.get(0);
}
List<Function> matches = new ArrayList<>();
for (FunctionEntry entry : procedures()) {
Function function = entry.function;
Symbol symbol = function.getSymbol();
String qualified = symbol == null ? function.getName() : symbol.getName(true);
if (function.getName().equals(value) || qualified.equals(value)) {
matches.add(function);
}
}
if (matches.isEmpty()) {
throw new RequestFailure("not_found", "Unknown Ghidra procedure name");
}
if (matches.size() != 1) {
throw new RequestFailure("ambiguous", "Ghidra procedure name is ambiguous");
}
return matches.get(0);
// Retain legacy bare-hex address input only when no symbol matches.
Address address = explicitAddress ? parseReaAddress(value) : tryParseAddress(value);
if (address == null)
throw new RequestFailure("not_found", "Unknown Ghidra procedure name or address: " + value);
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
if (function == null && address.isMemoryAddress())
function = currentProgram.getFunctionManager().getFunctionContaining(address);
if (function == null)
throw new RequestFailure("not_found", "No procedure exists at the requested address: " + value);
return function;
}
private JsonObject nativeApiBoundary(
@@ -2530,21 +2572,32 @@ public final class ReaGhidraBridge extends HeadlessScript {
}
private List<Reference> collectReferences(
List<Instruction> instructions,
Function function,
String direction) throws Exception {
// References can target instruction interiors or originate in embedded
// data. Use actual body ownership, including an external's exact entry.
AddressSet scope = new AddressSet(function.getBody());
scope.add(function.getEntryPoint());
boolean outgoing = direction.equals("outgoing");
AddressIterator addresses = outgoing
? currentProgram.getReferenceManager().getReferenceSourceIterator(scope, true)
: currentProgram.getReferenceManager().getReferenceDestinationIterator(scope, true);
TreeMap<String, Reference> observed = new TreeMap<>();
for (Instruction instruction : instructions) {
while (addresses.hasNext()) {
monitor.checkCancelled();
if (direction.equals("outgoing")) {
Address address = addresses.next();
if (outgoing) {
for (Reference reference : currentProgram.getReferenceManager()
.getReferencesFrom(instruction.getAddress())) {
.getReferencesFrom(address)) {
monitor.checkCancelled();
addReference(observed, reference);
}
}
else {
ReferenceIterator iterator = currentProgram.getReferenceManager()
.getReferencesTo(instruction.getAddress());
.getReferencesTo(address);
while (iterator.hasNext()) {
monitor.checkCancelled();
addReference(observed, iterator.next());
}
}
@@ -2947,7 +3000,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
private Address parseReaAddress(String value) {
Address address = tryParseAddress(value);
if (address == null) {
throw new RequestFailure("invalid_request", "Unknown or invalid Ghidra address");
throw new RequestFailure("invalid_request", "Unknown or invalid Ghidra address: " + value);
}
return address;
}
@@ -2956,12 +3009,12 @@ public final class ReaGhidraBridge extends HeadlessScript {
try {
String spaceName = null;
String offset = value;
int separator = value.lastIndexOf(":0x");
int separator = value.toLowerCase(Locale.ROOT).lastIndexOf(":0x");
if (separator >= 0) {
spaceName = decodeAddressSpace(value.substring(0, separator));
offset = value.substring(separator + 3);
}
else if (value.startsWith("0x")) {
else if (value.startsWith("0x") || value.startsWith("0X")) {
offset = value.substring(2);
}
if (!offset.matches("[0-9A-Fa-f]+")) {
@@ -2970,8 +3023,17 @@ public final class ReaGhidraBridge extends HeadlessScript {
AddressSpace space = spaceName == null
? currentProgram.getAddressFactory().getDefaultAddressSpace()
: currentProgram.getAddressFactory().getAddressSpace(spaceName);
return space == null ? null : space.getAddress(offset);
if (space == null) return null;
Address address = space.getAddress(offset);
BigInteger requestedOffset = new BigInteger(offset, 16);
BigInteger parsedOffset = new BigInteger(Long.toUnsignedString(address.getOffset()));
if (!requestedOffset.equals(parsedOffset))
throw new RequestFailure("invalid_request",
"Ghidra address offset cannot be represented without truncation in space " +
space.getName() + ": requested " + value + ", parsed " + canonicalAddress(address));
return address;
}
catch (RequestFailure failure) { throw failure; }
catch (Exception exception) {
return null;
}
@@ -3055,9 +3117,29 @@ public final class ReaGhidraBridge extends HeadlessScript {
);
}
catch (PatternSyntaxException exception) {
throw new RequestFailure("invalid_request", "Invalid regex pattern");
// Java wraps compiler stack exhaustion in PatternSyntaxException
// without retaining its cause. This producer reason is not bad syntax.
if (exception.getDescription().equals("Stack overflow during pattern compilation"))
throw regexStackExhausted(expression, "compiling");
throw new RequestFailure("invalid_request", "Invalid regex pattern " + expression + ": " + exception.getDescription() + " at index " + exception.getIndex());
}
return value -> pattern.matcher(value).find();
catch (StackOverflowError exhausted) {
throw regexStackExhausted(expression, "compiling");
}
return value -> {
try {
return pattern.matcher(value).find();
}
catch (StackOverflowError exhausted) {
throw regexStackExhausted(expression, "matching");
}
};
}
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 + ". Use literal mode or simplify the regex; the session remains usable.");
}
private static SessionDescriptor readDescriptor(Path path) throws IOException {
@@ -3114,7 +3196,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
private void requireDocument(JsonObject params) {
String document = optionalString(params, "document");
if (document != null && !document.equals(currentProgram.getName())) {
throw new RequestFailure("not_found", "Unknown Ghidra Program identity");
throw new RequestFailure("not_found", "Unknown Ghidra Program identity: " + document + "; active program: " + currentProgram.getName());
}
}
@@ -3226,7 +3308,16 @@ public final class ReaGhidraBridge extends HeadlessScript {
private static void writeResponse(BufferedWriter writer, JsonObject response) throws IOException {
String encoded = GSON.toJson(response);
writer.write(encoded);
// Gson leaves UTF-16 surrogates literal. Escape them in JSON so even
// diagnostics quoting ill-formed caller text remain lossless UTF-8.
int start = 0;
for (int index = 0; index < encoded.length(); index++) {
if (!Character.isSurrogate(encoded.charAt(index))) continue;
writer.write(encoded, start, index - start);
writer.write("\\u" + Integer.toHexString(encoded.charAt(index)));
start = index + 1;
}
writer.write(encoded, start, encoded.length() - start);
writer.newLine();
writer.flush();
}
+323 -98
View File
@@ -8,12 +8,13 @@ API access on this thread: moving dispatch to a worker can deadlock Hopper.
import json
import hmac
import os
import re
import socket
import struct
from typing import Any, Optional, Protocol, Sequence
BAD_ADDRESSES = (-1, 0xFFFFFFFFFFFFFFFF, None)
_selected_document = None
# The native label setter truncates NSString names at this UTF-16 extent.
HOPPER_SYMBOL_NAME_UTF16_UNITS = 1024
_rea_session_document = None
_search_inventory_cache = {}
_pseudocode_cache = {}
@@ -89,9 +90,11 @@ def _session_document():
global _rea_session_document
documents = _api().documents()
if _rea_session_document is not None:
# Hopper recreates Python wrappers; equality compares native handles.
# Retain our bound wrapper so cache identity stays stable across calls.
return (
_rea_session_document
if any(document is _rea_session_document for document in documents)
if any(document == _rea_session_document for document in documents)
else None
)
return _bind_session_document(documents)
@@ -174,46 +177,89 @@ def _json_safe(value):
def _document(name=None):
"""Resolve an explicit or session-selected document without changing Hopper UI."""
global _selected_document
api = _api()
documents = api.documents()
if name is not None:
matches = [candidate for candidate in documents if candidate.getDocumentName() == name]
if len(matches) == 1:
return matches[0]
if len(matches) > 1:
current = api.current_document()
if current is not None and current.getDocumentName() == name:
return current
raise InvalidRequestError("Hopper document name is ambiguous")
raise InvalidRequestError("Unknown Hopper document")
if _selected_document is not None:
for candidate in documents:
if candidate.getDocumentName() == _selected_document:
return candidate
"""Keep observations and mutations bound to the active REA target."""
documents = _api().documents()
session_document = _session_document()
if session_document is not None:
return session_document
current = api.current_document()
if current is None:
raise CapabilityUnavailableError("No Hopper document is loaded")
return current
if session_document is None:
raise CapabilityUnavailableError("The active REA target's Hopper document is no longer open; reopen the target with open_binary")
if name is not None:
# A document name is display metadata; scope it to the bound native
# handle even when another open target happens to have the same name.
if session_document.getDocumentName() == name:
return session_document
if any(candidate.getDocumentName() == name for candidate in documents):
raise InvalidRequestError("Hopper document belongs to another target; use open_binary to select that target")
raise InvalidRequestError("Unknown Hopper document")
return session_document
def _api_text(value, field):
"""Validate text before passing it to Hopper's native UTF-8 string API."""
if not isinstance(value, str):
raise InvalidRequestError("%s must be a string" % field)
if "\0" in value:
raise InvalidRequestError("%s contains a NUL character, which Hopper's Python API cannot represent" % field)
try:
value.encode("utf-8")
except UnicodeEncodeError as error:
raise InvalidRequestError("%s contains an unpaired Unicode surrogate, which Hopper's Python API cannot represent" % field) from error
return value
def _validate_name_targets(document, names):
"""Keep Hopper's unique-label moves within the explicitly selected addresses."""
targets = {address for _, address, _ in names}
if len(targets) != len(names):
raise InvalidRequestError("Batch rename selects the same address more than once; use one canonical address per target")
labels = set()
for _, address, name in names:
_segment(document, address)
if not name:
continue
if name in labels:
raise InvalidRequestError("Batch rename assigns the same name more than once: %s" % name)
labels.add(name)
owner = document.getAddressForName(name)
if owner not in BAD_ADDRESSES and owner != address and owner not in targets:
raise InvalidRequestError("Name %s is already assigned to %s; include that address in the batch with a replacement name" % (name, _hex(owner)))
def _symbol_name(value, field):
"""Reject native label truncation without altering the caller's name."""
value = _api_text(value, field)
if len(value.encode("utf-16-le")) // 2 > HOPPER_SYMBOL_NAME_UTF16_UNITS:
raise InvalidRequestError("%s exceeds Hopper's %s UTF-16 code-unit symbol-name limit" % (field, HOPPER_SYMBOL_NAME_UTF16_UNITS))
return value
def _name_matches(document, address, name):
observed = _segment(document, address).getNameAtAddress(address)
return observed == name or (not name and observed is None)
def _address(document, value=None):
"""Resolve hexadecimal addresses first, then fall back to Hopper symbol names."""
"""Keep explicit hexadecimal coordinates distinct from symbol names."""
if value is None:
return document.getCurrentAddress()
if not isinstance(value, str):
raise InvalidRequestError("Address must be a string")
try:
return int(value, 16)
except ValueError:
result = document.getAddressForName(value)
if result in BAD_ADDRESSES:
raise InvalidRequestError("Unknown Hopper address or name")
return result
_api_text(value, "Address")
if value.lower().startswith("0x"):
try:
address = int(value, 16)
except ValueError as error:
raise InvalidRequestError("Invalid hexadecimal Hopper address: %s" % value) from error
else:
# Names such as `add` and `face` are valid hexadecimal strings too.
# Prefer an observed symbol, retaining bare-hex compatibility only
# when Hopper has no symbol with the supplied name.
address = document.getAddressForName(value)
if address in BAD_ADDRESSES:
try:
address = int(value, 16)
except ValueError as error:
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
def _segment(document, address):
@@ -231,6 +277,76 @@ def _procedure(document, value=None):
return result
def _original_file_offset(document, offset):
"""Translate Hopper's image-relative mapping using the observed loaded header."""
if offset > 9007199254740991:
raise CapabilityUnavailableError("Provider file offset exceeds the exact JSON integer range")
path = globals().get("REA_ORIGINAL_EXECUTABLE_PATH") or document.getExecutableFilePath()
if not path:
raise CapabilityUnavailableError("Hopper did not identify the original executable for file-offset verification")
try:
with open(path, "rb") as source:
size = os.fstat(source.fileno()).st_size
header = source.read(8)
formats = {
b"\xca\xfe\xba\xbe": (">", False),
b"\xbe\xba\xfe\xca": ("<", False),
b"\xca\xfe\xba\xbf": (">", True),
b"\xbf\xba\xfe\xca": ("<", True),
}
fat = formats.get(header[:4])
if fat is None:
if offset >= size:
raise InvalidRequestError("Mapped offset is outside the original executable file at %s" % path)
return offset, path, 0
if len(header) != 8:
raise CapabilityUnavailableError("Original FAT executable has a truncated header at %s" % path)
byte_order, wide = fat
count = struct.unpack(byte_order + "I", header[4:])[0]
stride = 32 if wide else 20
table_end = 8 + count * stride
if count == 0 or table_end > size:
raise CapabilityUnavailableError("Original FAT executable has an invalid architecture table at %s" % path)
header_address = document.getAddressFromFileOffset(0)
reader = getattr(document, "readBytes", None)
if header_address in BAD_ADDRESSES or document.getSegmentAtAddress(header_address) is None or not callable(reader):
raise CapabilityUnavailableError("Hopper did not expose a readable loaded Mach-O header to identify its FAT slice")
loaded = reader(header_address, 12)
if not isinstance(loaded, (bytes, bytearray)) or len(loaded) != 12:
raise CapabilityUnavailableError("Hopper did not expose the loaded Mach-O header to identify its FAT slice")
loaded_order = {b"\xce\xfa\xed\xfe": "<", b"\xcf\xfa\xed\xfe": "<", b"\xfe\xed\xfa\xce": ">", b"\xfe\xed\xfa\xcf": ">"}.get(bytes(loaded[:4]))
if loaded_order is None:
raise CapabilityUnavailableError("Hopper's loaded header is not a supported Mach-O image")
identity = struct.unpack(loaded_order + "II", loaded[4:12])
selected = None
for index in range(count):
source.seek(8 + index * stride)
entry = source.read(stride)
if len(entry) != stride:
raise CapabilityUnavailableError("Original FAT executable architecture table changed during verification")
cpu, subtype, start, length, alignment = struct.unpack(byte_order + ("IIQQI" if wide else "IIIII"), entry[:28] if wide else entry)
if length < 12 or start < table_end or start > size or length > size - start or alignment > 52 or start % (1 << alignment):
raise CapabilityUnavailableError("Original FAT executable has an invalid slice range at index %s" % index)
if (cpu, subtype) != identity:
continue
source.seek(start)
if source.read(12) != bytes(loaded):
raise CapabilityUnavailableError("FAT slice declaration disagrees with Hopper's loaded header")
if selected is not None:
raise CapabilityUnavailableError("Multiple FAT slices match Hopper's loaded header; original-file mapping is ambiguous")
selected = (start, length)
if selected is None:
raise CapabilityUnavailableError("No original FAT slice matches Hopper's loaded header")
start, length = selected
if offset >= length:
raise InvalidRequestError("Mapped offset is outside Hopper's loaded FAT slice")
if start + offset > 9007199254740991:
raise CapabilityUnavailableError("Original file offset exceeds the exact JSON integer range")
return start + offset, path, start
except OSError as error:
raise CapabilityUnavailableError("Cannot read the original executable for file-offset verification at %s: %s" % (path, error)) from error
def _procedure_name(procedure):
entry = procedure.getEntryPoint()
return procedure.getSegment().getNameAtAddress(entry) or _hex(entry)
@@ -249,14 +365,18 @@ def _procedure_identity(procedure):
def _procedure_locals(procedure):
"""Project opaque Hopper local-variable objects into an exact public shape."""
return [
{
"description": str(local),
"""Preserve public local-variable names and observed stack displacements."""
result = []
for local in procedure.getLocalVariableList():
name = local.name()
displacement = local.displacement()
result.append({
"description": "%s (stack displacement %s)" % (name if name is not None else "Unnamed local", displacement),
"name": name,
"stack_displacement": str(displacement) if displacement is not None else None,
"provenance": "hopper-public-python-api",
}
for local in procedure.getLocalVariableList()
]
})
return result
def _containing_procedure(document, address):
@@ -287,6 +407,22 @@ def _instruction_addresses(procedure):
return result
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:
continue
if target in BAD_ADDRESSES:
unresolved.append({"address": _hex(source), "reason": "Native CallReference has no resolved target (type %s)" % kind})
continue
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))
def _procedure_references(document, params):
procedure = _procedure(document, params.get("procedure"))
direction = params.get("direction", "outgoing")
@@ -299,6 +435,8 @@ def _procedure_references(document, params):
references = segment.getReferencesFromAddress(address) if direction == "outgoing" else segment.getReferencesOfAddress(address)
for reference in references:
edges.add((address, reference) if direction == "outgoing" else (reference, address))
calls, unresolved = _native_calls(procedure, direction)
edges.update(calls)
ordered = sorted(edges)
items = []
for source, target in ordered:
@@ -309,16 +447,15 @@ def _procedure_references(document, params):
"target_address": _hex(target),
"source_procedure": _procedure_identity(source_procedure) if source_procedure is not None else None,
"target_procedure": _procedure_identity(target_procedure) if target_procedure is not None else None,
"kind": _unavailable("Hopper's public Python API does not classify reference kinds"),
"kind": _unavailable("Hopper exposes partial CallReference classification, but not detailed reference flags"),
**({"call": calls[(source, target)]} if (source, target) in calls else {}),
})
return {
"procedure": _procedure_identity(procedure), "direction": direction,
"reference_kinds_available": False,
# The public Hopper API returns observed references but does not expose
# enough flow metadata to enumerate calls with no resolved target.
# Keep the required list field explicit; the provider records this as
# unknown coverage rather than claiming the list is exhaustive.
"unresolved_calls": [],
# These are native CallReference objects, not proof that every unresolved
# dispatch site has been enumerated by Hopper.
"unresolved_calls": unresolved,
"references": items,
}
@@ -333,11 +470,61 @@ def _procedure_map(document):
def _strings(document):
result = {}
for segment in document.getSegmentsList():
for value, address in segment.getStringsList():
result[_hex(address)] = value
return result
return {address: record["value"] for address, record in _string_inventory(document).items()}
def _string_record(segment, address, display):
"""Read one native typed object, retaining its shortened display separately."""
length = segment.getObjectLength(address)
end = segment.getStartingAddress() + segment.getLength()
if isinstance(length, bool) or not isinstance(length, int) or length <= 0 or length > end - address:
raise CapabilityUnavailableError("Hopper reported an invalid string object extent at %s" % _hex(address))
raw = segment.readBytes(address, length)
if not isinstance(raw, (bytes, bytearray)) or len(raw) != length:
raise CapabilityUnavailableError("Hopper could not read the complete string object at %s" % _hex(address))
kind = segment.getTypeAtAddress(address)
encodings = ("utf-8", "latin-1") if kind == segment.TYPE_ASCII else ("utf-16-le", "utf-16-be") if kind == segment.TYPE_UNICODE else ()
# Native display escaping is not invertible (a literal backslash-n and a
# newline can render alike). Decode byte strings directly; for UTF-16,
# compare rendered candidates to establish an unambiguous byte order.
candidates = []
for encoding in encodings:
terminator = b"\x00\x00" if encoding.startswith("utf-16") else b"\x00"
terminated = raw.endswith(terminator)
payload = raw[:-len(terminator)] if terminated else raw
try:
value = payload.decode(encoding)
except UnicodeDecodeError:
continue
record = {"value": value, "provider_value": display, "string": {"encoding": encoding, "encoding_status": "inferred", "termination": "present_or_not_required" if terminated else "missing", "byte_length": length}}
if kind == segment.TYPE_ASCII:
return record
rendered = value.replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
if rendered == display or (display.endswith("\u2026") and display[:-1] and rendered.startswith(display[:-1])):
candidates.append(record)
if candidates and len({candidate["value"] for candidate in candidates}) == 1:
return candidates[0]
raise CapabilityUnavailableError("Hopper's string display cannot be reconciled with its typed bytes at %s; inspect the bytes and encoding explicitly" % _hex(address))
def _string_inventory(document, addresses=None):
key = (id(document), "string_objects")
objects = _search_inventory_cache.get(key)
if objects is None:
objects = {address: (segment, display) for segment in document.getSegmentsList() for display, address in segment.getStringsList()}
_search_inventory_cache[key] = objects
key = (id(document), "string_records")
records = _search_inventory_cache.setdefault(key, {})
selected = sorted(objects if addresses is None else objects.keys() & addresses)
for address in selected:
name = _hex(address)
if name not in records:
segment, display = objects[address]
try:
records[name] = _string_record(segment, address, display)
except CapabilityUnavailableError as error:
records[name] = {"value": display, "provider_value": display, "decoding": _unavailable(str(error))}
return {_hex(address): records[_hex(address)] for address in selected}
def _invalidate_search_inventory(document):
@@ -397,18 +584,14 @@ def _search_results(document, kind, params):
needle = pattern if case_sensitive else pattern.casefold()
matches = lambda value: needle in (value if case_sensitive else value.casefold())
else:
try:
expression = re.compile(pattern, 0 if case_sensitive else re.IGNORECASE)
except (re.error, OverflowError) as error:
raise InvalidRequestError("Invalid regex pattern") from error
matches = lambda value: expression.search(value) is not None
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)
return [
return [{"address": address, **_string_inventory(document)[address]} for address, _ in selected] if kind == "string" else [
{
"address": address,
"value": value,
@@ -529,6 +712,10 @@ def _analyze_function(document, params):
incoming = []
outgoing = []
procedure_addresses = set(addresses)
outgoing_calls, unresolved_calls = _native_calls(procedure, "outgoing")
incoming_calls, _ = _native_calls(procedure, "incoming")
calls = {**incoming_calls, **outgoing_calls}
edges.update(calls)
for source, target in sorted(edges):
source_procedure, _ = _containing_procedure(document, source)
target_procedure, _ = _containing_procedure(document, target)
@@ -537,16 +724,14 @@ def _analyze_function(document, params):
"target_address": _hex(target),
"source_procedure": _procedure_identity(source_procedure) if source_procedure is not None else None,
"target_procedure": _procedure_identity(target_procedure) if target_procedure is not None else None,
"kind": _unavailable("Hopper's public Python API does not classify reference kinds"),
"kind": _unavailable("Hopper exposes partial CallReference classification, but not detailed reference flags"),
**({"call": calls[(source, target)]} if (source, target) in calls else {}),
}
if target in procedure_addresses and source not in procedure_addresses:
incoming.append(item)
if source in procedure_addresses:
outgoing.append(item)
string_map = {
int(address, 16): value
for address, value in _search_inventory(document, "string")
}
string_map = {int(address, 16): record for address, record in _string_inventory(document, {int(edge["target_address"], 16) for edge in outgoing}).items()}
name_map = {
int(address, 16): value
for address, value in _search_inventory(document, "name")
@@ -556,7 +741,7 @@ def _analyze_function(document, params):
for edge in outgoing:
target = int(edge["target_address"], 16)
if target in string_map:
referenced_strings.append({"address": edge["target_address"], "value": string_map[target], "source_address": edge["source_address"]})
referenced_strings.append({"address": edge["target_address"], **string_map[target], "source_address": edge["source_address"]})
if target in name_map:
referenced_names.append({"address": edge["target_address"], "value": name_map[target], "source_address": edge["source_address"]})
comments.sort(key=lambda item: (int(item["address"], 16), item["kind"]))
@@ -574,13 +759,14 @@ def _analyze_function(document, params):
"callers": callers, "callees": callees,
"incoming_references": incoming,
"outgoing_references": outgoing,
"unresolved_calls": unresolved_calls,
"referenced_strings": referenced_strings,
"referenced_names": referenced_names,
"basic_blocks": blocks,
"limitations": [
"Hopper's public Python API does not classify reference kinds.",
"Native CallReference classifications are observed where available; detailed reference flags remain unknown.",
"Hopper's public Python API does not expose equivalent external or thunk classification in this dossier.",
"Unresolved indirect calls without reported target addresses are not represented as call edges.",
"Native calls without resolved target addresses are not represented as edges; enumeration beyond reported CallReference objects is unknown.",
"Pseudocode and assembly are provider-specific representations, not original source.",
],
}
@@ -588,7 +774,6 @@ def _analyze_function(document, params):
def _dispatch(method, params):
"""Dispatch only the closed operation set implemented by REA's public tools."""
global _selected_document
if method == "health":
return {"name": "REA Hopper bridge", "version": "1.0.0", "run_id": REA_RUN_ID}
if method in ("shutdown", "shutdown_document"):
@@ -622,14 +807,9 @@ def _dispatch(method, params):
"document_closed": document_closed,
}
if method == "list_documents":
return [document.getDocumentName() for document in Document.getAllDocuments()]
return [document.getDocumentName() for document in _api().documents()]
if method == "current_document":
return _document().getDocumentName()
if method == "set_current_document":
document = _document(params.get("document"))
_selected_document = document.getDocumentName()
return _selected_document
document = _document(params.get("document"))
if method in EXHAUSTIVE_ANALYSIS_METHODS:
_api().require_analysis_complete(document, method)
@@ -650,10 +830,17 @@ def _dispatch(method, params):
"Hopper's public Python API does not expose readBytes"
)
address = _address(document, params.get("address"))
value = reader(address, length)
segment = _segment(document, address)
# Document.readBytes delegates to one Segment and returns False when
# the requested range crosses its end. Preserve the readable prefix.
available = segment.getStartingAddress() + segment.getLength() - address
value = reader(address, min(length, available))
if value is False or value is None:
raise InvalidRequestError("Hopper could not read the requested byte range at %s" % _hex(address))
if not isinstance(value, (bytes, bytearray)):
raise CapabilityUnavailableError(
"Hopper's public Python API returned an unsupported byte representation"
"Hopper readBytes returned %s for the requested %s-byte range at %s"
% (type(value).__name__, length, _hex(address))
)
data = bytes(value)
return {
@@ -678,7 +865,17 @@ def _dispatch(method, params):
or offset < 0
):
raise InvalidRequestError("Address has no authoritative file-offset mapping")
return {"address": _hex(address), "file_offset": offset}
inverse = getattr(document, "getAddressFromFileOffset", None)
if not callable(inverse):
raise CapabilityUnavailableError(
"Hopper's public Python API cannot verify the file-offset mapping by reverse lookup"
)
# Synthetic memory can report offset zero, which belongs to the actual
# file header. An integer alone does not prove source-file provenance.
if inverse(offset) != address:
raise InvalidRequestError("Address has no authoritative file-offset mapping: reverse lookup disagrees at %s" % _hex(address))
original_offset, source_path, image_base = _original_file_offset(document, offset)
return {"address": _hex(address), "file_offset": original_offset, "provider_file_offset": offset, "image_base_file_offset": image_base, "source_path": source_path}
if method == "resolve_containing_procedure":
address = _address(document, params.get("address"))
procedure, reason = _containing_procedure(document, address)
@@ -694,8 +891,13 @@ def _dispatch(method, params):
return _procedure_name(_procedure(document))
if method == "goto_address":
address = _address(document, params.get("address"))
segment = _segment(document, address)
expected = segment.getInstructionStart(address)
document.moveCursorAtAddress(address)
return _hex(address)
observed = document.getCurrentAddress()
if observed != (address if expected in BAD_ADDRESSES else expected):
raise InvalidRequestError("Hopper cursor did not reach the requested object at %s; observed %s" % (_hex(address), _hex(observed)))
return _hex(observed)
if method in ("address_name", "comment", "inline_comment", "xrefs"):
target = _address(document, params.get("address"))
segment = _segment(document, target)
@@ -708,12 +910,22 @@ def _dispatch(method, params):
return [_hex(value) for value in segment.getReferencesOfAddress(target)]
if method in ("next_address", "prev_address"):
target = _address(document, params.get("address"))
segment = _segment(document, target)
if method == "next_address":
result = target + max(1, document.getObjectLength(target))
start = segment.getInstructionStart(target)
if start in BAD_ADDRESSES:
raise InvalidRequestError("No analyzed object exists at the requested address")
length = segment.getObjectLength(start)
if length in BAD_ADDRESSES or length <= 0:
raise InvalidRequestError("No next analyzed object exists at the requested address")
result = start + length
else:
result = document.getInstructionStart(max(0, target - 1))
if result in BAD_ADDRESSES:
raise InvalidRequestError("No adjacent address")
previous_segment = document.getSegmentAtAddress(target - 1) if target > 0 else None
if previous_segment is None:
raise InvalidRequestError("No previous address exists in mapped memory")
result = previous_segment.getInstructionStart(target - 1)
if result in BAD_ADDRESSES or document.getSegmentAtAddress(result) is None:
raise InvalidRequestError("No adjacent analyzed object exists in mapped memory")
return _hex(result)
if method == "list_segments":
result = []
@@ -747,12 +959,9 @@ def _dispatch(method, params):
if method == "list_procedures":
return [{"address": address, "value": value} for address, value in _search_inventory(document, "procedure")]
if method == "list_strings":
values = dict(_search_inventory(document, "string"))
requested = params.get("address")
if requested is not None:
key = _hex(_address(document, requested))
values = {key: values[key]} if key in values else {}
return [{"address": address, "value": value} for address, value in values.items()]
values = _string_inventory(document, None if requested is None else {_address(document, requested)})
return [{"address": address, **record} for address, record in values.items()]
if method == "list_names":
result = dict(_search_inventory(document, "name"))
requested = params.get("address")
@@ -790,35 +999,51 @@ def _dispatch(method, params):
}
if method == "set_address_name":
address = _address(document, params.get("address"))
name = _symbol_name(params["name"], "Name")
_validate_name_targets(document, [(params.get("address"), address, name)])
try:
result = document.setNameAtAddress(address, params["name"])
result = document.setNameAtAddress(address, name)
finally:
_invalidate_search_inventory(document)
_invalidate_pseudocode(document)
return result
return bool(result) and _name_matches(document, address, name)
if method == "set_addresses_names":
# Validate all destinations before applying any annotation. A malformed
# later address must not discard the result of an earlier mutation.
names = [(key, _address(document, key), _symbol_name(value, "Name for %s" % key)) for key, value in params["names"].items()]
_validate_name_targets(document, names)
try:
result = {key: document.setNameAtAddress(_address(document, key), value) for key, value in params["names"].items()}
result = {key: document.setNameAtAddress(address, value) for key, address, value in names}
finally:
_invalidate_search_inventory(document)
_invalidate_pseudocode(document)
return result
return {key: bool(result[key]) and _name_matches(document, address, value) for key, address, value in names}
if method in ("set_comment", "set_inline_comment"):
address = _address(document, params.get("address"))
comment = _api_text(params["comment"], "Comment")
segment = _segment(document, address)
setter = segment.setCommentAtAddress if method == "set_comment" else segment.setInlineCommentAtAddress
getter = segment.getCommentAtAddress if method == "set_comment" else segment.getInlineCommentAtAddress
setter(address, params["comment"])
setter(address, comment)
_invalidate_pseudocode(document)
return getter(address) == params["comment"]
observed = getter(address)
return observed == comment or (comment == "" and observed is None)
if method == "list_bookmarks":
return [{"address": _hex(item), "name": document.getBookmarkName(item)} for item in document.getBookmarks()]
if method == "set_bookmark":
address = _address(document, params.get("address"))
document.setBookmarkAtAddress(address, params.get("name"))
_segment(document, address)
name = params.get("name")
if name is not None:
_api_text(name, "Bookmark name")
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.
if not document.hasBookmarkAtAddress(address):
_segment(document, address)
document.removeBookmarkAtAddress(address)
return not document.hasBookmarkAtAddress(address)
raise InvalidRequestError("Unknown bridge method")
@@ -913,14 +1138,14 @@ def _diagnostic_type(error):
def _safe_diagnostic(error):
"""Project one exception without retaining provider or credential text."""
"""Preserve bridge validation reasons while excluding transport credentials."""
diagnostic_type = _diagnostic_type(error)
if diagnostic_type == "capability_unavailable":
message = str(error)
elif diagnostic_type == "authorization":
message = "Invalid bridge capability"
elif diagnostic_type == "invalid_request":
message = "Invalid Hopper bridge request"
message = str(error)
else:
message = "%s: Hopper bridge operation failed" % type(error).__name__
return {"code": -32000, "message": message, "type": diagnostic_type}
+208
View File
@@ -0,0 +1,208 @@
"""REA offline adapter using mitmproxy's unchanged native tnetstring decoder.
No FlowReader migrations, HAR conversion, proxy listener, or target requests.
The containing mitmdump process and its configuration directory are REA-owned.
"""
import base64
import hashlib
import json
import math
import re
import resource
from pathlib import Path
from mitmproxy import ctx, version
from mitmproxy.io import tnetstring
PROFILE = "12.2.3"
OUTPUT_BYTES = 96 * 1024 * 1024
CREDENTIALS = {b"authorization", b"proxy-authorization", b"cookie", b"set-cookie"}
class CaptureFailure(Exception):
def __init__(self, reason, message, pointer=""):
self.reason, self.pointer = reason, pointer
super().__init__(message)
def pointer(parent, key):
return parent + "/" + str(key).replace("~", "~0").replace("/", "~1")
class BoundedReader:
"""Refuse malicious declared lengths before the upstream parser allocates."""
def __init__(self, handle, size):
self.handle, self.size = handle, size
def read(self, count):
if count < 0 or count > self.size - self.handle.tell():
raise CaptureFailure("format", "Native record declares bytes beyond the retained capture.")
return self.handle.read(count)
def project_record(state, secrets):
binaries, numbers, redactions = [], [], []
byte_secrets = []
for secret in secrets:
try:
byte_secrets.append(secret.encode("utf-8", errors="strict"))
except UnicodeEncodeError:
# Non-scalar JSON strings have no UTF-8 byte representation.
# Keep them in secrets for the ordinary string projection.
pass
def redact(text, path, original):
if any(secret in text or secret in original for secret in secrets):
redactions.append({"pointer": path, "reason": "explicit-sensitive-value"})
return None
return text
def transport_value(value, path, transport_url):
if not (transport_url or re.fullmatch(r"/(?:backup/)*request/(?:path|authority)", path)):
return value, False
binary = isinstance(value, bytes)
pattern = rb"^([\x00-\x20]*(?:[a-z][a-z0-9+.-]*:)?//)[^/?#]*@" if binary else r"^([\x00-\x20]*(?:[a-z][a-z0-9+.-]*:)?//)[^/?#]*@"
replacement = rb"\1" if binary else r"\1"
separator = b"@" if binary else "@"
safe_url = re.sub(pattern, replacement, value, flags=re.I)
if path.endswith("/authority") and separator in safe_url:
safe_url = safe_url.rsplit(separator, 1)[1]
changed = safe_url != value
if changed:
redactions.append({"pointer": path, "reason": "transport-credential"})
return safe_url, changed
def validate_omitted(value, path, depth):
before = len(binaries), len(numbers), len(redactions)
visit(value, path, depth)
del binaries[before[0]:]
del numbers[before[1]:]
del redactions[before[2]:]
def visit(value, path, depth=0, credential=False, transport_url=False):
if depth > 64:
raise CaptureFailure("input-limit", "Native capture exceeds the 64-level evidence nesting budget.", path)
if credential:
validate_omitted(value, path, depth)
redactions.append({"pointer": path, "reason": "transport-credential"})
if isinstance(value, bytes):
binaries.append({"pointer": path, "representation": "producer-bytes", "state": "redacted", "content_base64": None, "bytes": None, "sha256": None})
return None
if isinstance(value, bytes):
safe_bytes, credential_url = transport_value(value, path, transport_url)
content_base64 = base64.b64encode(value).decode("ascii")
digest = hashlib.sha256(value).hexdigest()
hidden = any(secret in value or secret in safe_bytes for secret in byte_secrets) or any(secret in content_base64 or secret in digest for secret in secrets)
safe_text = None
try:
safe_text = safe_bytes.decode("utf-8", errors="strict")
except UnicodeDecodeError:
pass
if hidden:
redactions.append({"pointer": path, "reason": "explicit-sensitive-value"})
exclude = hidden or credential_url
binaries.append({"pointer": path, "representation": "producer-bytes", "state": "redacted" if exclude else "retained", "content_base64": None if exclude else content_base64, "bytes": None if exclude else len(value), "sha256": None if exclude else digest})
if hidden:
return None
return safe_text
if isinstance(value, str):
safe_text, _ = transport_value(value, path, transport_url)
return redact(safe_text, path, value)
if isinstance(value, bool) or value is None:
return value
if isinstance(value, (int, float)):
literal = repr(value)
if any(secret in literal for secret in secrets):
redactions.append({"pointer": path, "reason": "explicit-sensitive-value"})
return None
numbers.append({"pointer": path, "producer_type": "integer" if isinstance(value, int) else "float", "literal": literal})
return value if (abs(value) <= 9007199254740991 if isinstance(value, int) else math.isfinite(value)) else None
if isinstance(value, (list, tuple)):
fields = re.fullmatch(r"/(?:backup/)*(?:request|response)/(?:headers|trailers)", path)
if fields:
result = []
for index, field in enumerate(value):
if not isinstance(field, (list, tuple)) or len(field) != 2 or not isinstance(field[0], bytes):
raise CaptureFailure("format", "Native header/trailer is not an ordered byte pair.", pointer(path, index))
result.append([visit(field[0], pointer(pointer(path, index), 0), depth + 2), visit(field[1], pointer(pointer(path, index), 1), depth + 2, field[0].lower() in CREDENTIALS, field[0].lower() in {b"location", b"referer", b"origin"})])
return result
return [visit(item, pointer(path, index), depth + 1) for index, item in enumerate(value)]
if isinstance(value, dict):
result = {}
for key, item in value.items():
if not isinstance(key, str):
raise CaptureFailure("format", "Native record has a non-string dictionary key.", path)
if any(secret in key for secret in secrets):
# Validate the omitted subtree too, then discard all of its
# coordinates rather than inventing a replacement identity.
validate_omitted(item, pointer(path, key), depth + 1)
redactions.append({"pointer": path, "reason": "explicit-sensitive-value", "scope": "property-name"})
continue
result[key] = visit(item, pointer(path, key), depth + 1)
return result
raise CaptureFailure("decoder", "Native parser returned an unexpected value type: " + type(value).__name__ + ".", path)
return {"reported": visit(state, ""), "binary_fields": binaries, "numeric_literals": numbers, "redactions": redactions}
def decode(request):
if version.VERSION != PROFILE:
raise CaptureFailure("unsupported", "Expected mitmproxy " + PROFILE + "; observed " + version.VERSION + ".")
records = []
capture = Path(request["snapshot_path"])
size = capture.stat().st_size
with capture.open("rb") as handle:
reader = BoundedReader(handle, size)
while handle.tell() < size:
start = handle.tell()
try:
state = tnetstring.load(reader)
except CaptureFailure:
raise
except RecursionError:
raise CaptureFailure("input-limit", "Native parser exceeded its recursion budget at byte offset " + str(start) + ".") from None
except (ValueError, IndexError, UnicodeError, EOFError):
raise CaptureFailure("format", "Malformed native record at byte offset " + str(start) + ".") from None
if not isinstance(state, dict):
raise CaptureFailure("format", "Native flow record must be a dictionary at byte offset " + str(start) + ".")
records.append({"ordinal": len(records), "location": {"kind": "byte-range", "offset": start, "bytes": handle.tell() - start}, **project_record(state, request["sensitive_values"]), "limitations": ["Fields are original native states without FlowReader migration. Binary fields contain exact producer bytes; reported UTF-8 strings for those fields are derived display views. Unknown flow/version extensions are retained without interpretation."]})
return {"decoder": {"id": "mitmproxy-native-tnetstring", "name": "REA offline native mitmproxy adapter", "version": PROFILE}, "container": {"reported": None, "numeric_literals": [], "redactions": [], "records_pointer": None}, "total_records": len(records), "records": records}
def failure_reply(error):
"""Keep producer constraints, resource exhaustion and adapter defects distinct."""
if isinstance(error, CaptureFailure):
return {"ok": False, "reason": error.reason, "message": str(error), "pointer": error.pointer}
if isinstance(error, MemoryError):
return {"ok": False, "reason": "resource-limit", "message": "Native decoder exhausted its 768 MiB address-space budget.", "pointer": ""}
return {"ok": False, "reason": "decoder", "message": "Offline native decoder failed unexpectedly: " + type(error).__name__ + ".", "pointer": ""}
class OfflineCapture:
def load(self, loader):
loader.add_option("rea_request_path", str, "", "REA private offline capture request")
def running(self):
resource.setrlimit(resource.RLIMIT_AS, (768 * 1024 * 1024, 768 * 1024 * 1024))
resource.setrlimit(resource.RLIMIT_CPU, (30, 30))
resource.setrlimit(resource.RLIMIT_FSIZE, (OUTPUT_BYTES, OUTPUT_BYTES))
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
request = json.loads(Path(ctx.options.rea_request_path).read_text(encoding="utf-8"))
try:
response = {"ok": True, "value": decode(request)}
except Exception as error:
response = failure_reply(error)
try:
try:
output = json.dumps(response, ensure_ascii=True, allow_nan=False, separators=(",", ":")).encode("ascii")
except Exception as error:
output = json.dumps(failure_reply(error), separators=(",", ":")).encode("ascii")
if len(output) > OUTPUT_BYTES:
output = b'{"ok":false,"reason":"limit","message":"Native reply exceeds the complete-evidence output budget.","pointer":""}'
Path(request["reply_path"]).write_bytes(output)
finally:
ctx.master.shutdown()
addons = [OfflineCapture()]
+774
View File
@@ -0,0 +1,774 @@
"""Bounded native call observation executed inside LLDB's embedded Python.
REA imports this module into `lldb --batch` and runs `rea_trace <config.json>`.
It launches one owned process, stops at the requested functions and
Objective-C methods, records each entry from registers and memory reads only,
continues, and kills the process once it exits, the event or aggregate resource
limit is reached, or the duration elapses. Target output is drained through
bounded FIFOs. No expression is evaluated and no target code runs on the
tracer's behalf. The result is written as JSON to the configured path.
"""
import errno
import hashlib
import json
import os
import select
import threading
import time
import lldb
ARGUMENT_REGISTERS = {
"arm64": ["x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7"],
"arm64e": ["x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7"],
"x86_64": ["rdi", "rsi", "rdx", "rcx", "r8", "r9"],
"x86_64h": ["rdi", "rsi", "rdx", "rcx", "r8", "r9"],
}
MAX_SELECTOR_BYTES = 1024
MAX_REPORTED_LOCATIONS = 64
MAX_OTHER_STOPS = 256
MAX_TRACE_BYTES = 8 * 1024 * 1024
MAX_TRACE_FRAMES = 65_536
MAX_OBSERVATION_JOURNAL_BYTES = MAX_TRACE_BYTES
MAX_BREAKPOINT_LOCATION_BYTES = 8 * 1024 * 1024
MAX_TARGET_OUTPUT_BYTES = 1024 * 1024
OUTPUT_DRAIN_BYTES_PER_PASS = 1024 * 1024
OUTPUT_EOF_WAIT_SECONDS = 1.0
OUTPUT_READER_JOIN_SECONDS = 0.5
LISTENER_NAME = "rea-native-call-observation"
def _can_retain_trace(current_bytes, current_frames, event_bytes, event_frames):
"""Whether a complete event fits both aggregate trace budgets."""
return (
current_bytes + event_bytes <= MAX_TRACE_BYTES
and current_frames + event_frames <= MAX_TRACE_FRAMES
)
def _estimated_json_size(value):
"""Estimate JSON size without encoding potentially oversized values."""
if isinstance(value, str):
# json.dumps defaults to ensure_ascii=True; supplementary characters
# can become two six-byte Unicode escapes.
return len(value) * 12 + 2
if value is None or isinstance(value, (bool, int, float)):
return 32
if isinstance(value, dict):
return 2 + sum(
len(key) * 6 + 4 + _estimated_json_size(item)
for key, item in value.items()
)
if isinstance(value, (list, tuple)):
return 2 + sum(_estimated_json_size(item) + 1 for item in value)
return 64
def _fits_trace_budget(value, remaining_bytes):
return _estimated_json_size(value) <= remaining_bytes
class _ObservationJournal:
"""Flush a bounded JSONL prefix so observations survive tracer failure."""
def __init__(self, handle, max_bytes=MAX_OBSERVATION_JOURNAL_BYTES):
self.handle = handle
self.max_bytes = max_bytes
self.bytes = 0
def append(self, record):
encoded = (json.dumps(record) + "\n").encode("utf-8")
if self.bytes + len(encoded) > self.max_bytes:
return False
self.handle.write(encoded)
self.handle.flush()
self.bytes += len(encoded)
return True
def _set_launch_flags(launch):
launch.SetLaunchFlags(
launch.GetLaunchFlags()
| lldb.eLaunchFlagDebug
| lldb.eLaunchFlagStopAtEntry
)
def _hash_file(path):
digest = hashlib.sha256()
with open(path, "rb") as target_file:
while True:
chunk = target_file.read(1024 * 1024)
if not chunk:
break
digest.update(chunk)
return digest.hexdigest()
def _file_identity(path):
info = os.stat(path)
return str(info.st_dev), str(info.st_ino)
def _module_identity(target):
module = target.FindModule(target.GetExecutable())
if not module.IsValid():
return None, None, None
spec = module.GetFileSpec()
path = str(spec) if spec.IsValid() else None
identity = None
if path:
try:
identity = _file_identity(path)
except OSError:
pass
return path, identity, module.GetUUIDString() or None
def _wait_for_identity_ack(config):
"""Hold the target at entry while the host adapter captures its start identity."""
path = config.get("identity_ack_path")
if not path:
return
while True:
try:
with open(path, "r") as handle:
return handle.read(32).strip()
except FileNotFoundError:
time.sleep(0.025)
class _OutputPipe:
"""Drain a target FIFO while retaining only its bounded output prefix."""
def __init__(self, fifo_path, capture_path, max_bytes):
os.mkfifo(fifo_path, 0o600)
self.path = fifo_path
try:
self.fd = os.open(fifo_path, os.O_RDONLY | os.O_NONBLOCK)
except Exception:
os.unlink(fifo_path)
raise
try:
self.capture = open(capture_path, "wb")
except Exception:
os.close(self.fd)
os.unlink(fifo_path)
raise
self.max_bytes = max_bytes
self.bytes = 0
self.truncated = False
self.closed = False
self.stop_reader = threading.Event()
self.reader_thread = None
self.reader_error = None
self.launched = False
self.eof = threading.Event()
self.complete = False
def start(self):
if self.reader_thread is None:
self.reader_thread = threading.Thread(target=self._reader_loop, daemon=True)
self.reader_thread.start()
def _reader_loop(self):
try:
while not self.stop_reader.is_set():
readable, _, _ = select.select([self.fd], [], [], 0.05)
if not readable:
continue
if self.drain(OUTPUT_DRAIN_BYTES_PER_PASS) == 0:
# A FIFO read end is readable at EOF before the target opens it.
time.sleep(0.01)
except Exception as error:
self.reader_error = error
self.stop_reader.set()
def mark_launched(self):
self.launched = True
def drain(self, max_bytes=OUTPUT_DRAIN_BYTES_PER_PASS):
consumed = 0
while consumed < max_bytes:
try:
chunk = os.read(self.fd, min(65_536, max_bytes - consumed))
except OSError as error:
if error.errno in (errno.EAGAIN, errno.EWOULDBLOCK):
return consumed
raise
if not chunk:
if self.launched:
self.eof.set()
return consumed
consumed += len(chunk)
self.bytes += len(chunk)
remaining = max(0, self.max_bytes - self.capture.tell())
if remaining:
self.capture.write(chunk[:remaining])
if len(chunk) > remaining:
self.truncated = True
def close(self, expected_exit=False):
if self.closed:
return
if expected_exit and self.launched:
self.eof.wait(OUTPUT_EOF_WAIT_SECONDS)
self.stop_reader.set()
if self.reader_thread is not None:
self.reader_thread.join(OUTPUT_READER_JOIN_SECONDS)
stopped = self.reader_thread is None or not self.reader_thread.is_alive()
if stopped:
try:
self.drain(OUTPUT_DRAIN_BYTES_PER_PASS)
finally:
try:
self.capture.close()
finally:
os.close(self.fd)
self.closed = True
else:
# A daemon reader may still be unwinding a blocked filesystem write.
# Leave its handles alone rather than racing that thread at shutdown.
self.closed = True
self.complete = bool(
stopped
and (not self.launched or (expected_exit and self.eof.is_set()))
and self.reader_error is None
)
if not self.complete:
self.truncated = True
def _hex(value):
return "0x%x" % value
def _location(address, target):
"""Load and file address, module and symbol of one code address."""
module = address.GetModule()
symbol = address.GetSymbol()
file_address = address.GetFileAddress()
location = {
"load_address": _hex(address.GetLoadAddress(target)),
"file_address": None
if file_address == lldb.LLDB_INVALID_ADDRESS
else _hex(file_address),
"module": module.GetFileSpec().GetFilename() if module.IsValid() else None,
"module_path": str(module.GetFileSpec()) if module.IsValid() else None,
"symbol": symbol.GetName() if symbol.IsValid() else None,
}
return location
def _no_files():
return lldb.SBFileSpecList()
def _create_breakpoints(target, spec):
"""LLDB breakpoints for one request; a class-qualified method may need two."""
if spec["kind"] == "function":
modules = lldb.SBFileSpecList()
if spec.get("module"):
modules.Append(lldb.SBFileSpec(spec["module"], False))
return [
target.BreakpointCreateByName(
spec["name"], lldb.eFunctionNameTypeAuto, modules, _no_files()
)
]
class_name = spec.get("class_name")
method_type = spec.get("method_type", "any")
if class_name is None:
return [
target.BreakpointCreateByName(
spec["selector"], lldb.eFunctionNameTypeSelector, _no_files(), _no_files()
)
]
prefixes = {"instance": ["-"], "class": ["+"], "any": ["-", "+"]}[method_type]
return [
target.BreakpointCreateByName(
"%s[%s %s]" % (prefix, class_name, spec["selector"]),
lldb.eFunctionNameTypeFull,
_no_files(),
_no_files(),
)
for prefix in prefixes
]
def _dynamic_class(target, pointer):
"""Class of an Objective-C receiver from LLDB's runtime reader, without running code."""
if pointer == 0:
return None
data = lldb.SBData.CreateDataFromUInt64Array(
target.GetByteOrder(), target.GetAddressByteSize(), [pointer]
)
value = target.CreateValueFromData(
"receiver", data, target.GetBasicType(lldb.eBasicTypeObjCID)
)
dynamic = value.GetDynamicValue(lldb.eDynamicDontRunTarget)
name = dynamic.GetTypeName() if dynamic.IsValid() else None
if not name or name in ("id", "objc_object *"):
return None
return name[:-2] if name.endswith(" *") else name
def _wanted(spec, symbol):
"""Selector-only requests may admit only instance or only class methods."""
if spec["kind"] != "objc-method" or spec.get("class_name") is not None:
return True
method_type = spec.get("method_type", "any")
if method_type == "any":
return True
return symbol.startswith("-[" if method_type == "instance" else "+[")
def _record(context, thread, index, sequence, location, remaining_bytes):
target, process, config = context["target"], context["process"], context["config"]
frame = thread.GetFrameAtIndex(0)
symbol = location["symbol"] or ""
if not _wanted(config["breakpoints"][index], symbol):
return None
names = ARGUMENT_REGISTERS.get(config["architecture"], [])
registers = [
{"name": name, "value": _hex(frame.FindRegister(name).GetValueAsUnsigned())}
for name in names[: config["argument_registers"]]
]
receiver_class = None
selector = None
if symbol.startswith("-[") or symbol.startswith("+["):
error = lldb.SBError()
text = process.ReadCStringFromMemory(
frame.FindRegister(names[1]).GetValueAsUnsigned(),
MAX_SELECTOR_BYTES,
error,
)
selector = text if error.Success() and text else None
if symbol.startswith("-["):
receiver_class = _dynamic_class(
target, frame.FindRegister(names[0]).GetValueAsUnsigned()
)
estimated_base = (
_estimated_json_size(location)
+ _estimated_json_size(receiver_class)
+ _estimated_json_size(selector)
+ 2048
)
if estimated_base > remaining_bytes:
raise ValueError("trace event exceeds aggregate byte budget")
record = {
"sequence": sequence,
"elapsed_ms": round((time.monotonic() - context["started"]) * 1000, 3),
"thread_id": thread.GetThreadID(),
"breakpoint_index": index,
"receiver_class": receiver_class,
"selector": selector,
"registers": registers,
"backtrace": [],
}
record.update(location)
return record
def _handle_stop(context, events, other_stops):
"""Record breakpoint hits of one stop; return an outcome when tracing must end."""
process = context["process"]
for thread in process:
reason = thread.GetStopReason()
if reason == lldb.eStopReasonBreakpoint:
index = context["breakpoint_index"].get(thread.GetStopReasonDataAtIndex(0))
if index is None:
continue
frame = thread.GetFrameAtIndex(0)
location = _location(frame.GetPCAddress(), context["target"])
symbol = location["symbol"] or ""
if not _wanted(context["config"]["breakpoints"][index], symbol):
continue
frame_count = max(
0,
min(thread.GetNumFrames() - 1, context["config"]["backtrace_frames"]),
)
event_frame_count = 1 + frame_count
if not _can_retain_trace(
context["retained_trace_bytes"],
context["retained_frames"],
0,
event_frame_count,
):
return "resource-limit"
try:
remaining_bytes = MAX_TRACE_BYTES - context["retained_trace_bytes"]
record = _record(
context, thread, index, len(events), location, remaining_bytes
)
except ValueError:
return "resource-limit"
if record is not None:
if not _fits_trace_budget(record, remaining_bytes):
return "resource-limit"
record_bytes = len(json.dumps(record).encode("utf-8"))
if context["retained_trace_bytes"] + record_bytes > MAX_TRACE_BYTES:
return "resource-limit"
for depth in range(1, frame_count + 1):
try:
location = _location(
thread.GetFrameAtIndex(depth).GetPCAddress(), context["target"]
)
except ValueError:
return "resource-limit"
if (
_estimated_json_size(location) + record_bytes + 1
> MAX_TRACE_BYTES - context["retained_trace_bytes"]
):
return "resource-limit"
location_bytes = len(json.dumps(location).encode("utf-8"))
addition = location_bytes + (1 if record["backtrace"] else 0)
if (
context["retained_trace_bytes"]
+ record_bytes
+ addition
> MAX_TRACE_BYTES
):
return "resource-limit"
record["backtrace"].append(location)
record_bytes += addition
journal = context.get("observation_journal")
if journal is not None and not journal.append(
{"kind": "event", "event": record}
):
return "resource-limit"
events.append(record)
context["retained_frames"] += event_frame_count
context["retained_trace_bytes"] += record_bytes
if len(events) >= context["config"]["max_events"]:
return "event-limit"
elif reason in (lldb.eStopReasonSignal, lldb.eStopReasonException):
stop_description = thread.GetStopDescription(256)
journal = context.get("observation_journal")
if journal is not None and not journal.append(
{"kind": "other-stop", "reason": stop_description}
):
return "resource-limit"
other_stops.append(stop_description)
if len(other_stops) >= MAX_OTHER_STOPS:
return "stop-limit"
return None
def _breakpoint_report(target, created, config):
report = []
retained_location_bytes = 0
locations_truncated = False
for index, breakpoints in enumerate(created):
locations = []
count = 0
for breakpoint in breakpoints:
for position in range(breakpoint.GetNumLocations()):
address = breakpoint.GetLocationAtIndex(position).GetAddress()
location = _location(address, target)
if not _wanted(config["breakpoints"][index], location["symbol"] or ""):
continue
count += 1
if len(locations) < MAX_REPORTED_LOCATIONS:
remaining_bytes = (
MAX_BREAKPOINT_LOCATION_BYTES - retained_location_bytes
)
if not _fits_trace_budget(location, remaining_bytes):
locations_truncated = True
continue
location_bytes = len(json.dumps(location).encode("utf-8"))
addition = location_bytes + (1 if locations else 0)
if addition > remaining_bytes:
locations_truncated = True
continue
locations.append(location)
retained_location_bytes += addition
report.append({"index": index, "location_count": count, "locations": locations})
return report, locations_truncated
def _launch_info(target, config, listener):
launch = lldb.SBLaunchInfo(config["arguments"])
launch.SetListener(listener)
_set_launch_flags(launch)
environment = target.GetEnvironment()
for key, value in config["environment"].items():
environment.Set(key, value, True)
launch.SetEnvironment(environment, False)
if config.get("working_directory"):
launch.SetWorkingDirectory(config["working_directory"])
launch.AddOpenFileAction(0, "/dev/null", True, False)
launch.AddOpenFileAction(1, config["stdout_path"], False, True)
launch.AddOpenFileAction(2, config["stderr_path"], False, True)
return launch
def _wait_for_exit(listener, process, seconds):
event = lldb.SBEvent()
deadline = time.monotonic() + seconds
while process.GetState() != lldb.eStateExited and time.monotonic() < deadline:
listener.WaitForEvent(1, event)
def trace(debugger, config):
debugger.SetAsync(True)
# Stop at the symbol itself, where argument registers still hold the ABI
# arguments, even when line tables would let LLDB skip the prologue.
lldb.SBDebugger.SetInternalVariable(
"target.skip-prologue", "false", debugger.GetInstanceName()
)
try:
before_digest = _hash_file(config["executable"])
before_identity = _file_identity(config["executable"])
except OSError as error:
return {"status": "target-integrity-error", "error": str(error)}
if before_digest != config["expected_sha256"]:
return {
"status": "target-integrity-error",
"error": "selected executable digest changed before LLDB target creation",
}
error = lldb.SBError()
target = debugger.CreateTarget(
config["executable"], config["architecture"], None, False, error
)
if not target.IsValid():
return {"status": "target-error", "error": error.GetCString()}
module_path_before, module_file_identity_before, module_uuid_before = (
_module_identity(target)
)
if (
module_file_identity_before is not None
and module_file_identity_before != before_identity
):
return {
"status": "target-integrity-error",
"error": "LLDB executable module resolved to a different pre-launch file identity",
}
created = [_create_breakpoints(target, spec) for spec in config["breakpoints"]]
breakpoint_index = {}
for index, breakpoints in enumerate(created):
for breakpoint in breakpoints:
breakpoint_index[breakpoint.GetID()] = index
listener = lldb.SBListener(LISTENER_NAME)
output_budget = config.get("max_output_bytes", MAX_TARGET_OUTPUT_BYTES)
stdout = _OutputPipe(
config["stdout_path"], config["stdout_capture_path"], output_budget
)
try:
stderr = _OutputPipe(
config["stderr_path"], config["stderr_capture_path"], output_budget
)
except Exception:
stdout.close()
raise
stdout.start()
stderr.start()
observation_journal = None
started = time.monotonic()
process = None
try:
observation_path = config.get("observation_path")
if observation_path:
observation_journal = _ObservationJournal(
open(observation_path, "wb")
)
process = target.Launch(_launch_info(target, config, listener), error)
if not error.Success() or not process.IsValid():
stdout.close()
stderr.close()
return {"status": "launch-error", "error": error.GetCString()}
stdout.mark_launched()
stderr.mark_launched()
# Publish the PID before any post-launch file or module inspection. The
# process is stopped at entry and remains there until identity_ack.
with open(config["pid_path"], "w") as handle:
handle.write(str(process.GetProcessID()))
identity_ack = _wait_for_identity_ack(config)
if identity_ack != "readable":
process.Kill()
_wait_for_exit(listener, process, 5)
expected_exit = process.GetState() == lldb.eStateExited
stdout.close(expected_exit=expected_exit)
stderr.close(expected_exit=expected_exit)
return {
"status": "target-identity-unavailable",
"error": "host could not establish the launched process identity; target was stopped",
}
try:
after_digest = _hash_file(config["executable"])
after_identity = _file_identity(config["executable"])
except OSError as identity_error:
process.Kill()
stdout.close(expected_exit=process.GetState() == lldb.eStateExited)
stderr.close(expected_exit=process.GetState() == lldb.eStateExited)
return {"status": "target-integrity-error", "error": str(identity_error)}
if after_digest != config["expected_sha256"] or after_identity != before_identity:
process.Kill()
_wait_for_exit(listener, process, 5)
stdout.close(expected_exit=process.GetState() == lldb.eStateExited)
stderr.close(expected_exit=process.GetState() == lldb.eStateExited)
return {
"status": "target-integrity-error",
"error": "selected executable digest or file identity changed during LLDB launch",
}
module_path_after, module_file_identity_after, module_uuid_after = (
_module_identity(target)
)
if (
module_file_identity_after is not None
and module_file_identity_after != before_identity
):
process.Kill()
_wait_for_exit(listener, process, 5)
stdout.close(expected_exit=process.GetState() == lldb.eStateExited)
stderr.close(expected_exit=process.GetState() == lldb.eStateExited)
return {
"status": "target-integrity-error",
"error": "LLDB executable module resolved to a different file identity",
}
if (
module_uuid_before is not None
and module_uuid_after is not None
and module_uuid_before != module_uuid_after
):
process.Kill()
_wait_for_exit(listener, process, 5)
stdout.close(expected_exit=process.GetState() == lldb.eStateExited)
stderr.close(expected_exit=process.GetState() == lldb.eStateExited)
return {
"status": "target-integrity-error",
"error": "LLDB executable module identity changed during launch",
}
context = {
"target": target,
"process": process,
"config": config,
"started": started,
"breakpoint_index": breakpoint_index,
"retained_frames": 0,
"retained_trace_bytes": 0,
"observation_journal": observation_journal,
}
events = []
other_stops = []
outcome = None
deadline = started + config["duration_ms"] / 1000
event = lldb.SBEvent()
while outcome is None:
if stdout.reader_error is not None:
raise RuntimeError("stdout capture failed: %s" % stdout.reader_error)
if stderr.reader_error is not None:
raise RuntimeError("stderr capture failed: %s" % stderr.reader_error)
if time.monotonic() >= deadline:
outcome = "duration-elapsed"
break
if not listener.WaitForEvent(1, event):
continue
if not lldb.SBProcess.EventIsProcessEvent(event):
continue
state = lldb.SBProcess.GetStateFromEvent(event)
if state == lldb.eStateExited:
outcome = "exited"
elif state == lldb.eStateStopped and not lldb.SBProcess.GetRestartedFromEvent(event):
outcome = _handle_stop(context, events, other_stops)
if outcome is None:
process.Continue()
pid = process.GetProcessID()
exit_status = None
exit_description = None
if outcome == "exited":
exit_status = process.GetExitStatus()
exit_description = process.GetExitDescription()
else:
process.Kill()
_wait_for_exit(listener, process, 5)
process_exited = process.GetState() == lldb.eStateExited
stdout.close(expected_exit=process_exited)
stderr.close(expected_exit=process_exited)
launch_identity_stable = (
module_path_before is not None
and module_path_after == module_path_before
and module_file_identity_before == before_identity
and module_file_identity_after == module_file_identity_before
and module_uuid_before is not None
and module_uuid_after == module_uuid_before
)
breakpoint_report, breakpoint_locations_truncated = _breakpoint_report(
target, created, config
)
return {
"status": "traced",
"version": lldb.SBDebugger.GetVersionString().splitlines()[0],
"pid": pid,
"outcome": outcome,
"exit_status": exit_status,
"exit_description": exit_description,
"killed": outcome != "exited",
"terminated": process.GetState() == lldb.eStateExited,
"elapsed_ms": round((time.monotonic() - started) * 1000, 3),
"breakpoints": breakpoint_report,
"breakpoint_locations_truncated": breakpoint_locations_truncated,
"events": events,
"resource_limit_reached": outcome == "resource-limit",
"target_output": {
"stdout_bytes": stdout.bytes,
"stderr_bytes": stderr.bytes,
"stdout_truncated": stdout.truncated,
"stderr_truncated": stderr.truncated,
"stdout_complete": stdout.complete,
"stderr_complete": stderr.complete,
},
"target_identity": {
"selected_file_sha256": after_digest,
"loaded_image_sha256": None,
"file_device": after_identity[0],
"file_inode": after_identity[1],
"module_path": module_path_after,
"module_uuid": module_uuid_after,
# This records matching path/module identity observations. LLDB
# does not expose the exact mapped bytes, so it never proves a digest.
"stable": launch_identity_stable,
},
"other_stops": other_stops,
}
finally:
if observation_journal is not None:
try:
observation_journal.handle.close()
except Exception:
pass
if process is not None:
try:
if process.IsValid() and process.GetState() != lldb.eStateExited:
process.Kill()
_wait_for_exit(listener, process, 5)
except Exception:
pass
for output_pipe in (stdout, stderr):
try:
output_pipe.close(
expected_exit=(
process is not None
and process.IsValid()
and process.GetState() == lldb.eStateExited
)
)
except Exception:
pass
def run(debugger, command, result, internal_dict):
"""LLDB command entry point: `rea_trace <config.json>`."""
with open(command.strip()) as handle:
config = json.load(handle)
try:
output = trace(debugger, config)
except Exception as error: # Reported as a tracer failure, never re-raised into LLDB.
output = {"status": "tracer-error", "error": "%s: %s" % (type(error).__name__, error)}
with open(config["result_path"], "w") as handle:
json.dump(output, handle)
def __lldb_init_module(debugger, internal_dict):
debugger.HandleCommand("command script add -f rea_lldb_tracer.run rea_trace")
+273
View File
@@ -0,0 +1,273 @@
"""Source-owned tests for the LLDB bridge's bounded retention helpers."""
from io import BytesIO
import importlib.util
import json
import os
import sys
import tempfile
import types
import unittest
fake_lldb = sys.modules.setdefault("lldb", types.ModuleType("lldb"))
fake_lldb.LLDB_INVALID_ADDRESS = -1
fake_lldb.eLaunchFlagDebug = 2
fake_lldb.eLaunchFlagStopAtEntry = 4
spec = importlib.util.spec_from_file_location(
"rea_lldb_tracer", os.path.join(os.path.dirname(__file__), "rea_lldb_tracer.py")
)
tracer = importlib.util.module_from_spec(spec)
spec.loader.exec_module(tracer)
class NativeTraceBudgetTests(unittest.TestCase):
def test_journal_checks_exact_byte_budget_before_writing(self):
record = {"kind": "other-stop", "reason": "signal"}
encoded_size = len((tracer.json.dumps(record) + "\n").encode("utf-8"))
stream = BytesIO()
journal = tracer._ObservationJournal(stream, max_bytes=encoded_size - 1)
self.assertFalse(journal.append(record))
self.assertEqual(stream.getvalue(), b"")
self.assertEqual(journal.bytes, 0)
def test_journal_appends_each_row_once_and_flushes_it(self):
class CountingStream(BytesIO):
writes = 0
flushes = 0
def write(self, value):
self.writes += 1
return super().write(value)
def flush(self):
self.flushes += 1
return super().flush()
stream = CountingStream()
journal = tracer._ObservationJournal(stream)
journal.append({"kind": "event", "event": {"sequence": 0}})
journal.append({"kind": "other-stop", "reason": "signal"})
self.assertEqual((stream.writes, stream.flushes), (2, 2))
self.assertEqual(journal.bytes, len(stream.getvalue()))
def test_aggregate_trace_budget_admits_exact_boundary_and_rejects_next_record(self):
self.assertTrue(
tracer._can_retain_trace(
tracer.MAX_TRACE_BYTES - 10,
tracer.MAX_TRACE_FRAMES - 2,
10,
2,
)
)
self.assertFalse(
tracer._can_retain_trace(
tracer.MAX_TRACE_BYTES - 10,
tracer.MAX_TRACE_FRAMES - 2,
11,
2,
)
)
self.assertFalse(
tracer._can_retain_trace(
tracer.MAX_TRACE_BYTES - 10,
tracer.MAX_TRACE_FRAMES - 2,
10,
3,
)
)
def test_oversized_producer_string_is_rejected_before_serialization(self):
self.assertTrue(tracer._fits_trace_budget("method", 1024))
self.assertFalse(tracer._fits_trace_budget("m" * 2048, 1024))
self.assertGreater(
tracer._estimated_json_size("🙂"), len('"🙂"'.encode("utf-8"))
)
def test_launch_flags_preserve_default_debug_and_stop_at_entry(self):
class Launch:
flags = 10
def GetLaunchFlags(self):
return self.flags
def SetLaunchFlags(self, flags):
self.flags = flags
launch = Launch()
tracer._set_launch_flags(launch)
self.assertEqual(launch.flags, 14)
def test_output_pipe_counts_all_bytes_and_keeps_only_the_prefix(self):
with tempfile.TemporaryDirectory() as directory:
fifo = os.path.join(directory, "stdout.fifo")
capture = os.path.join(directory, "stdout.capture")
sink = tracer._OutputPipe(fifo, capture, 5)
sink.start()
sink.mark_launched()
writer = os.open(fifo, os.O_WRONLY)
try:
for index in range(100):
chunk = (b"hello world" if index == 0 else b"x" * 4096)
os.write(writer, chunk)
finally:
os.close(writer)
sink.drain()
sink.close(expected_exit=True)
with open(capture, "rb") as retained:
self.assertEqual(retained.read(), b"hello")
self.assertEqual(sink.bytes, 11 + 99 * 4096)
self.assertTrue(sink.truncated)
self.assertTrue(sink.complete)
def test_output_pipe_reports_incomplete_capture_after_unexpected_close(self):
with tempfile.TemporaryDirectory() as directory:
fifo = os.path.join(directory, "stdout.fifo")
capture = os.path.join(directory, "stdout.capture")
sink = tracer._OutputPipe(fifo, capture, 5)
sink.start()
sink.mark_launched()
sink.close(expected_exit=False)
self.assertFalse(sink.complete)
self.assertTrue(sink.truncated)
def test_selector_location_report_uses_the_requested_method_type(self):
class Symbol:
def IsValid(self):
return True
def GetName(self):
return "+[Greeter shared]"
class FileSpec:
def GetFilename(self):
return "Fixture"
def __str__(self):
return "/tmp/Fixture"
class Module:
def IsValid(self):
return True
def GetFileSpec(self):
return FileSpec()
class Address:
def GetModule(self):
return Module()
def GetSymbol(self):
return Symbol()
def GetFileAddress(self):
return 1
def GetLoadAddress(self, target):
return 2
class Location:
def GetAddress(self):
return Address()
class Breakpoint:
def GetNumLocations(self):
return 1
def GetLocationAtIndex(self, index):
return Location()
report, locations_truncated = tracer._breakpoint_report(
object(),
[[Breakpoint()]],
{
"breakpoints": [
{
"kind": "objc-method",
"selector": "shared",
"method_type": "instance",
}
]
},
)
self.assertEqual(report[0]["location_count"], 0)
self.assertEqual(report[0]["locations"], [])
self.assertFalse(locations_truncated)
def test_breakpoint_report_bounds_aggregate_location_json(self):
tracer.lldb.LLDB_INVALID_ADDRESS = -1
symbol_name = (
"-[VeryLongButPlausibleObjectiveCController "
"performTransactionWithRequestIdentifier:accountContext:"
"securityPolicy:completionHandler:]"
)
module_path = (
"/Applications/Enterprise Product.app/Contents/Frameworks/"
"EnterpriseSubsystem.framework/Versions/A/EnterpriseSubsystem"
)
class Symbol:
def IsValid(self):
return True
def GetName(self):
return symbol_name
class FileSpec:
def GetFilename(self):
return "EnterpriseSubsystem"
def __str__(self):
return module_path
class Module:
def IsValid(self):
return True
def GetFileSpec(self):
return FileSpec()
class Address:
def GetModule(self):
return Module()
def GetSymbol(self):
return Symbol()
def GetFileAddress(self):
return 0x123456789
def GetLoadAddress(self, target):
return 0x7123456789
class Location:
def GetAddress(self):
return Address()
class Breakpoint:
def GetNumLocations(self):
return 64
def GetLocationAtIndex(self, index):
return Location()
breakpoint_count = 1000
created = [[Breakpoint()] for _ in range(breakpoint_count)]
config = {"breakpoints": [{"kind": "objc-method"}] * breakpoint_count}
report, locations_truncated = tracer._breakpoint_report(
object(), created, config
)
retained_bytes = sum(
len(json.dumps(location).encode("utf-8"))
+ (1 if index > 0 else 0)
for row in report
for index, location in enumerate(row["locations"])
)
self.assertTrue(locations_truncated)
self.assertEqual(sum(row["location_count"] for row in report), 64_000)
self.assertLess(sum(len(row["locations"]) for row in report), 64_000)
self.assertLessEqual(retained_bytes, tracer.MAX_BREAKPOINT_LOCATION_BYTES)
if __name__ == "__main__":
unittest.main()
+293
View File
@@ -0,0 +1,293 @@
import Darwin
import Foundation
struct RunTokenObservation: Encodable {
let pid: Int32
let state: String
let run_id: String?
let reason: String?
}
struct ProcessIdentityObservation: Encodable {
let pid: Int32
let state: String
let identity: String?
let reason: String?
}
private struct ProcessMetadata {
let identity: String
let pointerSize: Int
}
struct ProcessSnapshot {
fileprivate let metadata: [Int32: ProcessMetadata]
let failure: String?
}
typealias ProcessTableSysctlCall = (
_ mib: inout [Int32],
_ output: UnsafeMutableRawPointer?,
_ size: inout Int
) -> Int32
private func readProcessSnapshot() -> ProcessSnapshot {
readProcessSnapshot { mib, output, size in
sysctl(&mib, u_int(mib.count), output, &size, nil, 0)
}
}
func readProcessSnapshot(
sysctlCall: ProcessTableSysctlCall
) -> ProcessSnapshot {
for _ in 0..<3 {
var mib = [CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0]
var size = 0
guard sysctlCall(&mib, nil, &size) == 0 else {
let failure = errno
return ProcessSnapshot(metadata: [:], failure: "process_table_failed_\(failure)")
}
guard size >= MemoryLayout<kinfo_proc>.size,
size % MemoryLayout<kinfo_proc>.stride == 0 else {
return ProcessSnapshot(metadata: [:], failure: "process_table_failed_\(errno)")
}
var processes = [kinfo_proc](
repeating: kinfo_proc(),
count: size / MemoryLayout<kinfo_proc>.stride
)
let result = processes.withUnsafeMutableBufferPointer { buffer in
let output = buffer.baseAddress.map { UnsafeMutableRawPointer($0) }
return sysctlCall(&mib, output, &size)
}
guard result == 0 else {
let failure = errno
if failure == ENOMEM { continue }
return ProcessSnapshot(metadata: [:], failure: "process_table_failed_\(failure)")
}
guard size % MemoryLayout<kinfo_proc>.stride == 0 else {
return ProcessSnapshot(metadata: [:], failure: "process_table_failed_\(errno)")
}
let processCount = size / MemoryLayout<kinfo_proc>.stride
let metadata = Dictionary(
processes.prefix(processCount).map { process in
let start = process.kp_proc.p_starttime
return (
process.kp_proc.p_pid,
ProcessMetadata(
identity: "\(start.tv_sec):\(start.tv_usec)",
pointerSize: process.kp_proc.p_flag & P_LP64 != 0 ? 8 : 4
)
)
},
uniquingKeysWith: { first, _ in first }
)
return ProcessSnapshot(metadata: metadata, failure: nil)
}
return ProcessSnapshot(metadata: [:], failure: "process_table_failed_\(ENOMEM)")
}
func readProcessIdentities(pids: [Int32]) -> [ProcessIdentityObservation] {
let snapshot = readProcessSnapshot()
return pids.map { pid in
guard let metadata = snapshot.metadata[pid] else {
return unavailableIdentity(
pid: pid,
reason: snapshot.failure ?? "process_not_in_snapshot"
)
}
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, reason: reason)
}
enum RunTokenReadError: Error {
case invalidBuffer
case duplicateToken
case invalidTokenEncoding
case environmentUnavailable
case appleVectorUnavailable
case ambiguousEnvironmentBoundary
}
@_silgen_name("csops")
private func csops(
_ pid: Int32,
_ ops: UInt32,
_ addr: UnsafeMutableRawPointer?,
_ size: Int
) -> Int32
private let csOpsStatus: UInt32 = 0
private let csPlatformBinary: UInt32 = 0x0400_0000
/**
* Whether the target image is an Apple platform binary. Current macOS omits
* the environment of platform binaries from KERN_PROCARGS2 for every caller,
* so this distinguishes a policy-withheld environment from a process whose
* environment is genuinely absent or transiently unreadable.
*/
func isPlatformBinary(pid: Int32) -> Bool {
var flags: UInt32 = 0
let result = withUnsafeMutablePointer(to: &flags) { pointer in
csops(pid, csOpsStatus, UnsafeMutableRawPointer(pointer), MemoryLayout<UInt32>.size)
}
return result == 0 && (flags & csPlatformBinary) != 0
}
private func readRunToken(pid: Int32, pointerSize: Int) -> RunTokenObservation {
var mib = [CTL_KERN, KERN_PROCARGS2, Int32(pid)]
var size = 0
guard sysctl(&mib, u_int(mib.count), nil, &size, nil, 0) == 0, size >= MemoryLayout<Int32>.size else {
return unavailable(pid: pid, reason: "sysctl_failed_\(errno)")
}
var bytes = [UInt8](repeating: 0, count: size)
let result = bytes.withUnsafeMutableBytes { buffer in
sysctl(&mib, u_int(mib.count), buffer.baseAddress, &size, nil, 0)
}
guard result == 0 else {
return unavailable(pid: pid, reason: "sysctl_failed_\(errno)")
}
bytes = Array(bytes.prefix(size))
do {
return RunTokenObservation(pid: pid, state: "readable", run_id: try token(in: bytes, pointerSize: pointerSize), reason: nil)
} catch RunTokenReadError.duplicateToken {
return unavailable(pid: pid, reason: "duplicate_run_token")
} catch RunTokenReadError.invalidTokenEncoding {
return unavailable(pid: pid, reason: "invalid_run_token_encoding")
} catch RunTokenReadError.environmentUnavailable {
return unavailable(
pid: pid,
reason: isPlatformBinary(pid: pid)
? "platform_binary_environment_withheld"
: "environment_unavailable"
)
} catch RunTokenReadError.appleVectorUnavailable {
return unavailable(pid: pid, reason: "apple_vector_unavailable")
} catch RunTokenReadError.ambiguousEnvironmentBoundary {
return unavailable(pid: pid, reason: "ambiguous_environment_boundary")
} catch {
return unavailable(pid: pid, reason: "malformed_procargs")
}
}
func readRunTokens(pids: [Int32]) -> [RunTokenObservation] {
let before = readProcessSnapshot()
let observations = pids.map { pid -> RunTokenObservation in
guard let metadata = before.metadata[pid] else {
return unavailable(pid: pid, reason: before.failure ?? "process_not_in_snapshot")
}
return readRunToken(pid: pid, pointerSize: metadata.pointerSize)
}
let after = readProcessSnapshot()
return zip(pids, observations).map { pid, observation in
guard let initial = before.metadata[pid], let current = after.metadata[pid] else {
return unavailable(pid: pid, reason: after.failure ?? "process_not_in_snapshot")
}
guard initial.identity == current.identity,
initial.pointerSize == current.pointerSize else {
return unavailable(pid: pid, reason: "process_identity_changed_during_token_read")
}
return observation
}
}
func token(in bytes: [UInt8], pointerSize: Int = MemoryLayout<UnsafeRawPointer>.size) throws -> String? {
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.invalidBuffer }
var offset = MemoryLayout<Int32>.size
guard let executablePath = readString(in: bytes, offset: &offset) else {
throw RunTokenReadError.invalidBuffer
}
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.invalidBuffer
}
offset += padding
for _ in 0..<argc {
guard readString(in: bytes, offset: &offset, allowEmpty: true) != nil else {
throw RunTokenReadError.invalidBuffer
}
}
guard bytes.last == 0 else { throw RunTokenReadError.invalidBuffer }
if offset == bytes.count {
throw RunTokenReadError.environmentUnavailable
}
var trailingEntries: [ArraySlice<UInt8>] = []
var zeroedAppleVectorStart: Int?
while offset < bytes.count {
if bytes[offset] == 0 {
let start = offset
while offset < bytes.count, bytes[offset] == 0 {
offset += 1
}
if offset - start > 1 {
zeroedAppleVectorStart = zeroedAppleVectorStart ?? trailingEntries.count
}
continue
}
guard let entry = readString(in: bytes, offset: &offset) else {
throw RunTokenReadError.invalidBuffer
}
trailingEntries.append(entry)
}
// XNU appends its Apple vector after the environment. libc clears the
// initial `pfz=` record after startup, leaving a multi-NUL run in its place.
// A caller can also set `pfz`, so prefer the final intact sentinel when it
// remains available and otherwise stop at the cleared record.
let intactAppleVectorStart = trailingEntries.lastIndex(where: {
$0.starts(with: Array("pfz=".utf8))
})
let preferZeroedBoundary = zeroedAppleVectorStart.flatMap { zeroedStart in
intactAppleVectorStart.map { intactStart in intactStart < zeroedStart } ?? true
} ?? false
let appleVectorStart = preferZeroedBoundary
? zeroedAppleVectorStart
: intactAppleVectorStart
guard let appleVectorStart else {
throw RunTokenReadError.appleVectorUnavailable
}
if trailingEntries[appleVectorStart...].contains(where: {
$0.starts(with: Array("REA_PROCESS_RUN_ID=".utf8))
}) {
throw RunTokenReadError.ambiguousEnvironmentBoundary
}
let environmentEntries = trailingEntries[..<appleVectorStart]
guard !environmentEntries.isEmpty else {
throw RunTokenReadError.environmentUnavailable
}
var found: String?
for entry in environmentEntries {
if entry.starts(with: Array("REA_PROCESS_RUN_ID=".utf8)) {
guard found == nil else { throw RunTokenReadError.duplicateToken }
let value = entry.dropFirst("REA_PROCESS_RUN_ID=".utf8.count)
guard let decoded = String(bytes: value, encoding: .utf8) else {
throw RunTokenReadError.invalidTokenEncoding
}
found = decoded
}
}
return found
}
private func readString(
in bytes: [UInt8],
offset: inout Int,
allowEmpty: Bool = false
) -> ArraySlice<UInt8>? {
guard offset < bytes.count, let end = bytes[offset...].firstIndex(of: 0),
allowEmpty || end > offset else { return nil }
let string = bytes[offset..<end]
offset = end + 1
return string
}
private func unavailable(pid: Int32, reason: String) -> RunTokenObservation {
RunTokenObservation(pid: pid, state: "unavailable", run_id: nil, reason: reason)
}
@@ -0,0 +1,35 @@
import Foundation
struct RunTokenResponse: Encodable {
let results: [RunTokenObservation]
}
struct ProcessIdentityResponse: Encodable {
let results: [ProcessIdentityObservation]
}
@main
struct ProcessRunTokenReaderMain {
static func main() {
let arguments = Array(CommandLine.arguments.dropFirst())
let identityMode = arguments.first == "--identities"
let pidArguments = identityMode ? Array(arguments.dropFirst()) : arguments
let pids = pidArguments.compactMap(Int32.init)
guard pids.count == pidArguments.count else {
fputs("invalid process ID input\n", stderr)
exit(64)
}
do {
let output: Data
if identityMode {
output = try JSONEncoder().encode(ProcessIdentityResponse(results: readProcessIdentities(pids: pids)))
} else {
output = try JSONEncoder().encode(RunTokenResponse(results: readRunTokens(pids: pids)))
}
FileHandle.standardOutput.write(output)
} catch {
fputs("could not encode process token observations\n", stderr)
exit(70)
}
}
}
+101
View File
@@ -0,0 +1,101 @@
"""Fixed core-only GDB/pwndbg operation; no caller commands or historical PID lookup."""
import json
import os
from pathlib import Path
import resource
import gdb
OUTPUT_BYTES = 64 * 1024 * 1024
class ContextFailure(Exception):
def __init__(self, reason, message):
super().__init__(message)
self.reason = reason
def inspect(request):
if gdb.selected_inferior().connection is not None:
raise ContextFailure("decoder", "Owned GDB unexpectedly has an inferior connection before core loading.")
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 sysroot ./no-libraries", "set solib-search-path ./no-libraries"]:
gdb.execute(command, to_string=True)
entry = request["gdbinit"]
try:
exec(compile(Path(entry).read_bytes(), entry, "exec"), {"__name__": "__main__", "__file__": entry})
import pwndbg.aglib.vmmap
import pwndbg.lib.version
except (ImportError, OSError) as error:
raise ContextFailure("unavailable", "Selected pwndbg could not initialize: " + str(error)) from error
version = pwndbg.lib.version.__version__
if version != "2026.09.15" and not version.startswith("2026.09.15 build: "):
raise ContextFailure("unsupported", "Expected unchanged pwndbg2026.09.15; selected plugin reports " + version + ".")
gdb.execute('set context-sections ""', to_string=True)
# core-file takes a raw filename and retains quote characters. Use a fixed
# 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.execute("core-file ../object.snapshot", 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.")
maps = []
for page in pwndbg.aglib.vmmap.get():
flags = page.flags
maps.append({"start": hex(page.start), "end": hex(page.end), "offset": hex(page.offset), "reported_flags": flags,
"permissions": {"read": bool(flags & 4), "write": bool(flags & 2), "execute": bool(flags & 1)} if 1 <= flags <= 7 else None,
"pathname_display": page.objfile, "current_file_identity": "unknown"})
return {"status": "available", "gdb_version": gdb.VERSION, "pwndbg_version": version, "connection": "core", "executable": None,
"historical_pid": inferior.pid, "reported_limits": {"address_space_bytes": resource.getrlimit(resource.RLIMIT_AS)[0], "cpu_seconds": resource.getrlimit(resource.RLIMIT_CPU)[0], "file_size_bytes": resource.getrlimit(resource.RLIMIT_FSIZE)[0]}, "maps": maps, "confidence": "derived", "completeness": "unknown",
"diagnostics": {"stdout": "", "stderr": "", "truncated": False}}
def failure_exit(request_path, status, marker):
try:
descriptor = os.open(Path(request_path).parent / "resource.failure", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
os.write(descriptor, marker)
os.close(descriptor)
except (OSError, MemoryError):
pass
os._exit(status)
def main():
request_path = os.environ["REA_CORE_CONTEXT_REQUEST"]
request = json.loads(Path(request_path).read_text(encoding="utf-8"))
reserve = bytearray(1024 * 1024)
try:
reply = {"ok": True, "value": inspect(request)}
except ContextFailure as error:
reply = {"ok": False, "reason": error.reason, "message": str(error)}
except MemoryError:
reserve = None
failure_exit(request_path, 75, b"M")
except Exception as error:
if isinstance(error, OSError) and error.errno == 12:
reserve = None
failure_exit(request_path, 75, b"M")
reply = {"ok": False, "reason": "decoder", "message": type(error).__name__ + ": " + str(error)}
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 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)
except MemoryError:
reserve = None
failure_exit(request_path, 75, b"M")
except OSError as error:
if error.errno == 27:
failure_exit(request_path, 76, b"F")
if error.errno == 12:
reserve = None
failure_exit(request_path, 75, b"M")
raise
main()
+39
View File
@@ -0,0 +1,39 @@
"""Catch typed resource failures while loading or running the owned debugger launcher.
Only this package's fixed sibling is executed; caller input stays in request.json.
Interpreter startup failures before this boundary remain unclassified.
"""
import os
import sys
def resource_exit(status, marker):
try:
if len(sys.argv) > 2:
descriptor = os.open(sys.argv[1], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
os.write(descriptor, marker)
os.close(descriptor)
except (OSError, MemoryError):
# Without a complete marker the parent keeps this failure unclassified.
pass
os._exit(status)
try:
implementation = os.path.join(os.path.dirname(__file__), "launch_impl.py")
with open(implementation, "rb") as handle:
source = handle.read()
exec(compile(source, implementation, "exec"), {
"__file__": implementation,
"__name__": "__main__",
})
except MemoryError:
# Reserved status: an observed allocation failure, including imports/compile.
resource_exit(75, b"M")
except OSError as error:
# Linux ENOMEM is an observed allocation failure, independent of error text.
if error.errno == 12:
resource_exit(75, b"M")
# Linux EFBIG is observed directly, including a partial limits/reply write.
if error.errno == 27:
resource_exit(76, b"F")
raise
+34
View File
@@ -0,0 +1,34 @@
"""Lower inherited limits before replacing this owned process with GDB."""
import json
import os
from pathlib import Path
import sys
runtime_path = Path(__file__).parent.parent / "pwntools" / "decoder_runtime.py"
runtime = {"__file__": str(runtime_path), "__name__": "rea_decoder_runtime"}
exec(compile(runtime_path.read_bytes(), str(runtime_path), "exec"), runtime)
class LaunchFailure(Exception):
def __init__(self, reason, message):
super().__init__(message)
self.reason = reason
def launch(snapshot, cache):
request_path = sys.argv[-1]
request = json.loads(Path(request_path).read_text(encoding="utf-8"))
bridge = str(Path(__file__).with_name("core_context.py"))
environment = dict(os.environ, REA_CORE_CONTEXT_REQUEST=request_path)
# Only fixed package code appears in the GDB Python command. Selected paths stay JSON data.
command = "python exec(compile(open(" + repr(bridge) + ", 'rb').read(), " + repr(bridge) + ", 'exec'), {'__name__':'__main__','__file__':" + repr(bridge) + "})"
try:
os.execve(request["gdb"], [request["gdb"], "-nx", "-nh", "--batch", "--quiet",
"-iex", "set auto-load off", "-iex", "set debuginfod enabled off",
"-iex", "set auto-solib-add off", "-ex", command], environment)
except OSError as error:
raise LaunchFailure("unavailable", "Selected GDB could not start (" + str(error.errno) + "): " + request["gdb"] + "; " + str(error)) from error
if __name__ == "__main__":
runtime["main"](sys.argv[-1], launch, LaunchFailure, "pwndbg-core-launcher")
+59
View File
@@ -0,0 +1,59 @@
"""Shared bounded reply/limit protocol, independent of the supplied artifact format."""
import json
import os
from pathlib import Path
import resource
import sys
OUTPUT_BYTES = 64 * 1024 * 1024
def lower_resource_limits():
limits = {}
for name, kind, maximum in (
("address_space_bytes", resource.RLIMIT_AS, 3 * 1024**3),
("cpu_seconds", resource.RLIMIT_CPU, 30),
("file_size_bytes", resource.RLIMIT_FSIZE, OUTPUT_BYTES),
):
soft, hard = resource.getrlimit(kind)
finite = [maximum] + [value for value in (soft, hard) if value != resource.RLIM_INFINITY]
effective = min(finite)
# Preserve the inherited hard boundary and never raise a caller's soft limit.
resource.setrlimit(kind, (effective, hard))
limits[name] = effective
return limits
def main(request_path, inspect, failure_type, profile):
request = json.loads(Path(request_path).read_text(encoding="utf-8"))
os.environ["PWNLIB_NOTERM"] = "1"
os.environ["PWNLIB_CACHE_DIR"] = str(Path(request_path).parent / "cache")
limits = None
# Release bounded emergency headroom before constructing a MemoryError reply.
# The outer exit-status contract also covers allocation failure during serialization.
memory_reserve = bytearray(1024 * 1024)
try:
limits = lower_resource_limits()
descriptor = os.open(Path(request_path).parent / "limits.json", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
json.dump(limits, handle)
value = inspect(Path(request["snapshot_path"]), Path(request_path).parent / "cache")
value["limitations"].append("Effective owned Python resource soft limits: " + json.dumps(limits, sort_keys=True) + ". Inherited tighter limits are retained.")
reply = {"ok": True, "profile": profile, "value": value}
except failure_type as error:
reply = {"ok": False, "reason": error.reason, "message": str(error)}
except MemoryError:
memory_reserve = None
reply = {"ok": False, "reason": "resource-limit", "message": "pwntools memory allocation failed under the effective resource limits; the exact allocation cause is unknown.", "reported_limits": limits}
except Exception as error:
if isinstance(error, OSError) and error.errno == 12:
memory_reserve = None
reply = {"ok": False, "reason": "resource-limit", "message": "pwntools reported OSError(ENOMEM); the exact allocation cause is unknown.", "reported_limits": limits}
else:
reply = {"ok": False, "reason": "decoder", "message": type(error).__name__ + ": " + str(error)}
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 artifact result exceeds the 64 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)
+39
View File
@@ -0,0 +1,39 @@
"""Catch typed resource failures while loading or running the owned ELF adapter.
Only this package's fixed sibling is executed; caller input stays in request.json.
Interpreter startup failures before this boundary remain unclassified.
"""
import os
import sys
def resource_exit(status, marker):
try:
if len(sys.argv) > 2:
descriptor = os.open(sys.argv[1], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
os.write(descriptor, marker)
os.close(descriptor)
except (OSError, MemoryError):
# Without a complete marker the parent keeps this failure unclassified.
pass
os._exit(status)
try:
implementation = os.path.join(os.path.dirname(__file__), "layout_impl.py")
with open(implementation, "rb") as handle:
source = handle.read()
exec(compile(source, implementation, "exec"), {
"__file__": implementation,
"__name__": "__main__",
})
except MemoryError:
# Reserved status: an observed allocation failure, including imports/compile.
resource_exit(75, b"M")
except OSError as error:
# Linux ENOMEM is an observed allocation failure, independent of error text.
if error.errno == 12:
resource_exit(75, b"M")
# Linux EFBIG is observed directly, including a partial limits/reply write.
if error.errno == 27:
resource_exit(76, b"F")
raise
+309
View File
@@ -0,0 +1,309 @@
"""Offline adapter to unchanged pwntools 4.15.0; never use libs/maps/libc.
Owned request/snapshot/reply paths are supplied by REA. No host target-process launch,
user configuration initialization, debugger startup, or dependency installation.
"""
import base64
import importlib.metadata
import io
import json
import os
from pathlib import Path
import resource
import sys
PROFILE = "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2"
PACKAGES = {"pwntools": "4.15.0", "pyelftools": "0.33", "unicorn": "2.1.2"}
OUTPUT_BYTES = 64 * 1024 * 1024
class LayoutFailure(Exception):
def __init__(self, reason, message):
super().__init__(message)
self.reason = reason
def address(value):
return hex(int(value))
def file_range(offset, size, length):
return 0 <= offset <= length and 0 <= size <= length - offset
def location(offset, size, length):
if not file_range(offset, size, length):
raise LayoutFailure("format", "ELF contains a file-backed range outside the selected snapshot.")
return {"offset": address(offset), "bytes": address(size)}
def name_reference(display, section, offset, content):
header = getattr(section, "header", None)
if header is None or header.sh_type != "SHT_STRTAB":
return {"display": display, "bytes_base64": None, "location": None,
"unknown_reason": "Referenced string table could not be resolved."}
return name_in_table(display, header.sh_offset, header.sh_size, offset, content)
def name_in_table(display, table_start, table_size, offset, content):
location(table_start, table_size, len(content))
if not 0 <= offset < table_size:
raise LayoutFailure("format", f"ELF name offset {offset} lies outside its declared string table of {table_size} bytes at file offset {table_start}.")
start = table_start + offset
limit = table_start + table_size
end = content.find(b"\0", start, limit)
if end < 0:
raise LayoutFailure("format", "ELF name lacks a terminator within its reported string table.")
raw = content[start:end]
return {"display": raw.decode("utf-8", "replace") if display is None else display, "bytes_base64": base64.b64encode(raw).decode("ascii"),
"location": location(start, len(raw) + 1, len(content)), "unknown_reason": None}
def dynamic_name_reference(tags, offset, image, content):
starts = [tag.d_val for tag in tags if tag.d_tag == "DT_STRTAB"]
sizes = [tag.d_val for tag in tags if tag.d_tag == "DT_STRSZ"]
if not starts or not sizes:
raise LayoutFailure("format", "ELF dependency requires DT_STRTAB and DT_STRSZ.")
if len(starts) != 1 or len(sizes) != 1:
raise LayoutFailure("unsupported-target", "ELF dependency string-table tags are ambiguous: " + repr({"DT_STRTAB": starts, "DT_STRSZ": sizes}))
if not 0 <= offset < sizes[0]:
raise LayoutFailure("format", f"ELF dependency name offset {offset} lies outside declared DT_STRSZ {sizes[0]}.")
offsets = set()
if len(starts) == 1 and len(sizes) == 1:
for segment in image.iter_segments_by_type("PT_LOAD"):
h = segment.header
if starts[0] >= h.p_vaddr and starts[0] + sizes[0] <= h.p_vaddr + h.p_filesz:
offsets.add(h.p_offset + starts[0] - h.p_vaddr)
if len(offsets) != 1:
raise LayoutFailure("unsupported-target", "Dynamic string table lacks a unique file-backed mapping: " + repr({"DT_STRTAB": starts, "DT_STRSZ": sizes, "candidate_file_offsets": sorted(offsets)}))
return name_in_table(None, offsets.pop(), sizes[0], offset, content)
def symbol_value_meaning(raw, image_type, sections):
index = raw.st_shndx
if raw.st_info.type == "STT_FILE": return "no-address"
if index == "SHN_UNDEF": return "undefined"
if index == "SHN_COMMON": return "alignment"
if index == "SHN_ABS": return "absolute-value"
if not isinstance(index, int) or not 0 < index < len(sections) or index >= 0xff00:
return "unknown-section-index"
if sections[index].header.sh_type == "SHT_NULL":
return "unknown-section-index"
if image_type == "ET_REL": return "section-offset"
if raw.st_info.type == "STT_TLS": return "tls-offset"
return "linked-virtual-address"
def relocation_target(image_type, section_index, offset, sections):
if image_type != "ET_REL":
return {"kind": "linked-virtual-address", "address": address(offset)}
if section_index == 0 or sections[section_index].header.sh_type == "SHT_NULL":
return {"kind": "unknown-section", "reported_section_index": section_index,
"offset": address(offset), "unknown_reason": "undefined-section-reference" if section_index == 0 else "inactive-section-header"}
return {"kind": "section-offset", "section_index": section_index, "offset": address(offset)}
def inspect_elf(path, cache):
for package, expected in PACKAGES.items():
try:
version = importlib.metadata.version(package)
except importlib.metadata.PackageNotFoundError as error:
raise LayoutFailure("unavailable", "Selected Python lacks " + package + " " + expected + ".") from error
if version != expected:
raise LayoutFailure("unsupported", "Expected " + package + " " + expected + "; selected Python reports " + version + ".")
content = path.read_bytes()
if len(content) < 4 or content[:4] != b"\x7fELF":
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] != 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"):
from elftools.common.exceptions import ELFError
from elftools.elf.elffile import ELFFile
from pwnlib.elf import ELF
try:
headers = ELFFile(io.BytesIO(content))
section_count = headers.num_sections()
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
if 0xff00 <= raw_name_index < 0xffff:
raise LayoutFailure("format", f"ELF section-name index {raw_name_index} is reserved and cannot directly reference a section.")
section_name_index = headers.get_shstrndx()
if raw_name_index == 0xffff and section_name_index < 0xff00:
raise LayoutFailure("format", f"ELF SHN_XINDEX resolves to ordinary index {section_name_index}; the escape requires an index at least SHN_LORESERVE (0xff00).")
if section_name_index != 0:
if section_name_index >= section_count:
raise LayoutFailure("format", f"ELF section-name table index {section_name_index} lies outside the declared section count {section_count}.")
names_header = headers._get_section_header(section_name_index)
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 as error:
raise LayoutFailure("format", str(error)) from error
class LayoutELF(ELF):
def _get_section_name(self, header):
# SHN_UNDEF declares no names; upstream would otherwise read
# strings at the inactive section-zero offset.
return "" if section_name_index == 0 else super()._get_section_name(header)
# pyelftools 0.33 calls these documented filters internally;
# pwntools 4.15.0's cached overrides do not accept the argument.
# Forward the filter on this adapter instance, without modifying
# either upstream package or process-global methods.
def iter_segments(self, type=None):
return (segment for segment in super().iter_segments()
if type is None or segment.header.p_type == type)
def iter_sections(self, type=None):
return (section for section in super().iter_sections()
if type is None or section.header.sh_type == type)
try:
image = LayoutELF(str(path), checksec=False)
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)
all_sections = list(image.iter_sections())
shstrings = image.get_section(section_name_index) if all_sections and section_name_index != 0 else None
sections, segments, symbols, relocations, packed_relatives = [], [], [], [], []
needed, interpreters = [], []
for index, section in enumerate(all_sections):
h = section.header
backing = h.sh_type not in ("SHT_NOBITS", "SHT_NULL")
if backing: location(h.sh_offset, h.sh_size, length)
sections.append({
"index": index, "name": name_reference(section.name, shstrings, h.sh_name, content) if shstrings is not None else {"display": "", "bytes_base64": None, "location": None, "unknown_reason": "ELF declares no section-name table (SHN_UNDEF index 0)."}, "name_offset": address(h.sh_name), "type": h.sh_type,
"header_location": location(image.header.e_shoff + index * image.header.e_shentsize, image.header.e_shentsize, length),
"address": address(h.sh_addr), "offset": address(h.sh_offset),
"size": address(h.sh_size), "alignment": address(h.sh_addralign),
"flags": address(h.sh_flags), "link": h.sh_link, "info": h.sh_info,
"entry_size": address(h.sh_entsize), "file_backing": "file" if backing else "none",
})
if h.sh_type in ("SHT_SYMTAB", "SHT_DYNSYM"):
if h.sh_entsize < image.structs.Elf_Sym.sizeof():
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]
for entry, symbol in enumerate(section.iter_symbols()):
raw = symbol.entry
symbols.append({
"table_index": index, "entry_index": entry,
"name": name_reference(symbol.name, strings, raw.st_name, content), "name_offset": address(raw.st_name),
"location": location(h.sh_offset + entry * h.sh_entsize, h.sh_entsize, length),
"value": address(raw.st_value), "value_meaning": symbol_value_meaning(raw, image.header.e_type, all_sections),
"size": address(raw.st_size), "binding": raw.st_info.bind, "type": raw.st_info.type,
"visibility": raw.st_other.visibility, "section_index": raw.st_shndx,
})
if h.sh_type in ("SHT_REL", "SHT_RELA"):
if image.header.e_type == "ET_REL" and h.sh_info >= len(all_sections):
raise LayoutFailure("format", f"ELF relocation section {index} references missing target section {h.sh_info}.")
symbol_table = None
if h.sh_link != 0:
if h.sh_link >= len(all_sections) or all_sections[h.sh_link].header.sh_type not in ("SHT_SYMTAB", "SHT_DYNSYM"):
raise LayoutFailure("format", f"ELF relocation section {index} links to invalid symbol table {h.sh_link}.")
symbol_table = all_sections[h.sh_link]
for entry, relocation in enumerate(section.iter_relocations()):
raw = relocation.entry
if raw.r_info_sym != 0 and (symbol_table is None or raw.r_info_sym >= symbol_table.num_symbols()):
raise LayoutFailure("format", f"ELF relocation section {index} entry {entry} references missing symbol {raw.r_info_sym} in table {h.sh_link}.")
relocations.append({
"section_index": index, "entry_index": entry, "reported_offset": address(raw.r_offset),
"target": relocation_target(image.header.e_type, h.sh_info, raw.r_offset, all_sections),
"location": location(h.sh_offset + entry * h.sh_entsize, h.sh_entsize, length),
"type": raw.r_info_type, "symbol_table_index": h.sh_link, "symbol_index": raw.r_info_sym,
"symbol_reference_meaning": "zero-symbol-value" if raw.r_info_sym == 0 else "symbol-table-entry",
"addend": None if "r_addend" not in raw else str(raw.r_addend),
})
if h.sh_type == "SHT_RELR":
packed_relatives.append({
"section_index": index,
"location": location(h.sh_offset, h.sh_size, length),
"encoded_bytes_base64": base64.b64encode(content[h.sh_offset:h.sh_offset + h.sh_size]).decode("ascii"),
"entries": [{"decoded_index": decoded, "reported_offset": address(relocation.entry.r_offset)} for decoded, relocation in enumerate(section.iter_relocations())],
"offset_meaning": "unknown" if image.header.e_type == "ET_REL" else "linked-virtual-address",
"evidence_kind": "derived", "entry_source_locations": None, "addends": None,
})
for index, segment in enumerate(image.iter_segments()):
h = segment.header
backing = h.p_type != "PT_NULL" and h.p_filesz != 0
if backing: location(h.p_offset, h.p_filesz, length)
if h.p_type == "PT_LOAD" and h.p_memsz < h.p_filesz:
raise LayoutFailure("format", "ELF load segment memory size is smaller than its file size.")
segments.append({
"index": index, "type": h.p_type, "offset": address(h.p_offset),
"header_location": location(image.header.e_phoff + index * image.header.e_phentsize, image.header.e_phentsize, length),
"file_size": address(h.p_filesz), "memory_size": address(h.p_memsz),
"virtual_address": address(h.p_vaddr), "physical_address": address(h.p_paddr),
"alignment": address(h.p_align), "flags": address(h.p_flags),
"file_backing": "file" if backing else "none",
"permissions": None if h.p_type == "PT_NULL" else {"read": bool(h.p_flags & 4), "write": bool(h.p_flags & 2), "execute": bool(h.p_flags & 1)},
})
if h.p_type == "PT_DYNAMIC":
tag_size = image.structs.Elf_Dyn.sizeof()
tags = []
for tag_index in range(h.p_filesz // tag_size):
tag = segment._get_tag(tag_index)
tags.append(tag)
if tag.d_tag == "DT_NULL":
break
else:
raise LayoutFailure("format", f"ELF dynamic segment {index} has no complete DT_NULL terminator within its reported file range.")
for tag in tags:
if tag.d_tag == "DT_NEEDED":
needed.append(dynamic_name_reference(tags, tag.d_val, image, content))
if h.p_type == "PT_INTERP":
raw = content[h.p_offset:h.p_offset + h.p_filesz]
end = raw.find(b"\0")
if end < 0:
raise LayoutFailure("format", "ELF interpreter lacks a reported terminator.")
interpreters.append({"display": raw[:end].decode("utf-8", "replace"), "bytes_base64": base64.b64encode(raw[:end]).decode("ascii"), "location": location(h.p_offset, end + 1, length), "unknown_reason": None})
return {
"format": "elf", "architecture": {"machine": image.header.e_machine, "bits": image.bits, "byte_order": image.endian},
"image_type": image.header.e_type,
"entry_point": {"reported_value": address(image.header.e_entry), "meaning": "not-applicable" if image.header.e_type == "ET_REL" else "absent" if image.header.e_entry == 0 else "linked-virtual-address", "execution_status": "unknown"},
"runtime_load_base": None, "sections": sections, "segments": segments, "symbols": symbols, "relocations": relocations,
"packed_relative_relocations": packed_relatives, "relocation_inventory_completeness": "unknown",
"linkage": {"needed_libraries": needed, "interpreters": interpreters,
"got": [{"display_name": name, "address": address(value)} for name, value in image.got.items()],
"plt": [{"display_name": name, "address": address(value)} for name, value in image.plt.items()],
"convenience_maps_completeness": "unknown", "runtime_library_paths": None},
"mitigations": {"evidence_kind": "inferred", "position_independent": image.pie, "nx_indicator": image.nx, "executable_stack_indicator": image.execstack, "stack_canary_indicator": image.canary, "relro": image.relro},
"limitations": [
"PT_NULL payload fields are unused and retain their reported numbers without file-byte or permission claims. Zero-file-size segments provide no file bytes.",
"SHN_XINDEX symbol values remain unresolved in this upstream representation; an external index table does not establish a resolved index in this report.",
"Reported values preserve linked addresses, section offsets, TLS offsets, alignment and absolute values separately. Runtime load base and library paths are unknown.",
"Names are display strings plus raw bytes and file ranges where resolvable; section/table/entry indices preserve identity and duplicates. Name ranges include their terminating NUL; raw name bytes exclude it.",
"Ordinary relocation rows cover REL/RELA. RELR tables retain complete encoded bytes and upstream-decoded offsets as derived evidence; per-offset packed-word locations and implicit addends remain unknown. Relocation inventory completeness is unknown, including other encodings and missing section tables.",
"Relocation symbol index zero uses a zero symbol value without a table lookup. Positive indices reference validated original symbol-table entries; raw table and symbol indices remain reported.",
"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.",
"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 []),
*(["Sectionless images lack the .dynamic section and symbol tables used by upstream heuristics. RELRO/canary indicators may be incomplete; their values remain reported static candidates, with protection coverage unknown."] if not all_sections else []),
],
}
except LayoutFailure:
raise
except (ELFError, ValueError, AssertionError, IndexError) as error:
raise LayoutFailure("format", "Selected ELF failed unchanged upstream structural parsing: " + str(error)) from error
if __name__ == "__main__":
runtime_path = Path(__file__).with_name("decoder_runtime.py")
runtime = {"__file__": str(runtime_path), "__name__": "rea_decoder_runtime"}
exec(compile(runtime_path.read_bytes(), str(runtime_path), "exec"), runtime)
runtime["main"](sys.argv[-1], inspect_elf, LayoutFailure, PROFILE)
+39
View File
@@ -0,0 +1,39 @@
"""Catch typed resource failures while loading or running the owned recorded-core adapter.
Only this package's fixed sibling is executed; caller input stays in request.json.
Interpreter startup failures before this boundary remain unclassified.
"""
import os
import sys
def resource_exit(status, marker):
try:
if len(sys.argv) > 2:
descriptor = os.open(sys.argv[1], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
os.write(descriptor, marker)
os.close(descriptor)
except (OSError, MemoryError):
# Without a complete marker the parent keeps this failure unclassified.
pass
os._exit(status)
try:
implementation = os.path.join(os.path.dirname(__file__), "recorded_crash_impl.py")
with open(implementation, "rb") as handle:
source = handle.read()
exec(compile(source, implementation, "exec"), {
"__file__": implementation,
"__name__": "__main__",
})
except MemoryError:
# Reserved status: an observed allocation failure, including imports/compile.
resource_exit(75, b"M")
except OSError as error:
# Linux ENOMEM is an observed allocation failure, independent of error text.
if error.errno == 12:
resource_exit(75, b"M")
# Linux EFBIG is observed directly, including a partial limits/reply write.
if error.errno == 27:
resource_exit(76, b"F")
raise
+147
View File
@@ -0,0 +1,147 @@
"""Bounded Linux core evidence via unchanged ELFFile/structs and pwntools ctypes.
No Corefile-selected thread, executable guessing, process lookup or target launch.
"""
import base64
import ctypes
import importlib.metadata
import io
from pathlib import Path
import sys
PROFILE = "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2"
class CoreFailure(Exception):
def __init__(self, reason, message):
super().__init__(message)
self.reason = reason
def encoded(value):
return base64.b64encode(value).decode("ascii")
def location(offset, size, limit):
if not 0 <= offset <= limit or not 0 <= size <= limit - offset:
raise CoreFailure("format", "Recorded source range exceeds its declared container.")
return {"offset": hex(offset), "bytes": hex(size)}
def inspect_core(path, cache):
for package, required in {"pwntools": "4.15.0", "pyelftools": "0.33", "unicorn": "2.1.2"}.items():
try:
actual = importlib.metadata.version(package)
except importlib.metadata.PackageNotFoundError as error:
raise CoreFailure("unavailable", "Selected Python lacks " + package + " " + required + ".") from error
if actual != required:
raise CoreFailure("unsupported", "Expected " + package + " " + required + "; selected Python reports " + actual + ".")
content = path.read_bytes()
if len(content) < 16 or content[:4] != b"\x7fELF":
raise CoreFailure("format", "Selected recording lacks a complete ELF identification.")
if content[4:6] != b"\x02\x01":
raise CoreFailure("unsupported-target", "Initial recorded-core profile requires ELF64 little-endian.")
if len(content) < 64:
raise CoreFailure("format", "Selected ELF64 core header is truncated.")
if content[6] != 1:
raise CoreFailure("format", "ELF identification version is not EV_CURRENT.")
if int.from_bytes(content[20:24], "little") != 1:
raise CoreFailure("format", "ELF header version is not EV_CURRENT.")
# Linux commonly emits ELFOSABI_NONE (0), also accepted as GNU/Linux (3).
# Other declared OSABIs must never select Linux note ctypes.
if content[7] not in (0, 3) or content[8] != 0:
raise CoreFailure("unsupported-target", "Initial recorded-core profile requires System V or GNU/Linux OSABI with ABI version zero; selected OSABI=" + str(content[7]) + ", ABI version=" + str(content[8]) + ".")
if int.from_bytes(content[16:18], "little") != 4 or int.from_bytes(content[18:20], "little") != 62:
raise CoreFailure("unsupported-target", "Initial recorded-core profile requires x86-64 ET_CORE.")
from elftools.common.exceptions import ELFError
from elftools.common.utils import struct_parse
from elftools.elf.elffile import ELFFile
from pwnlib.elf.datatypes import elf_prstatus_amd64, elf_siginfo_64, user_regs_struct_amd64
try:
image = ELFFile(io.BytesIO(content))
header = image.header
count = image.num_segments()
if count and header.e_phentsize < image.structs.Elf_Phdr.sizeof():
raise CoreFailure("format", "Program-header entry is shorter than the required ELF64 structure.")
location(header.e_phoff, count * header.e_phentsize, len(content))
segments, notes, padding, threads, signals = [], [], [], [], []
for segment_index, segment in enumerate(image.iter_segments()):
h = segment.header
backing = h.p_type != "PT_NULL" and h.p_filesz > 0
if backing:
location(h.p_offset, h.p_filesz, len(content))
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,
"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 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()
while position < end:
if end - position < header_size:
raw = content[position:end]
if any(raw):
raise CoreFailure("format", "Nonzero trailing note bytes lack a complete note header.")
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)
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
location(position, note_end - position, end)
owner = content[owner_start:owner_start + note.n_namesz]
if owner and b"\0" not in owner:
raise CoreFailure("format", "Declared note owner has no NUL terminator.")
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)
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)})
position = note_end
# Only the initial Linux CORE ABI is interpreted. Other owners retain exact bytes.
if owner != b"CORE\0":
continue
if note.n_type == "NT_PRSTATUS":
if len(descriptor) < ctypes.sizeof(elf_prstatus_amd64):
raise CoreFailure("format", "Linux amd64 NT_PRSTATUS descriptor is truncated.")
status = elf_prstatus_amd64.from_buffer_copy(descriptor)
registers = []
for name, kind in user_regs_struct_amd64._fields_:
relative = elf_prstatus_amd64.pr_reg.offset + getattr(user_regs_struct_amd64, 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.n_type == "NT_SIGINFO":
if len(descriptor) < ctypes.sizeof(elf_siginfo_64):
raise CoreFailure("format", "Linux amd64 NT_SIGINFO descriptor is truncated.")
signal = elf_siginfo_64.from_buffer_copy(descriptor)
number = ctypes.c_int32(signal.si_signo).value
code = ctypes.c_int32(signal.si_code).value
known = number == 11 and code in (1, 2)
signals.append({"note_index": note_index, "number": number, "code": code, "errno": ctypes.c_int32(signal.si_errno).value,
"fault_address": hex(signal.sigfault_addr) if known else None,
"fault_address_meaning": "recorded-sigsegv-address" if known else "unknown", "thread_association": "unknown"})
return {"format": "elf-core", "architecture": "x86_64-little-endian", "target_execution": "not-performed", "live_process_identity": "unknown",
"segments": segments, "notes": notes, "note_padding": padding, "threads": threads, "signals": signals,
"note_interpretation_completeness": "unknown",
"limitations": ["The initial Linux note profile accepts System V/unspecified or GNU/Linux OSABI with ABI version zero; an unspecified OSABI alone does not prove operating-system origin.",
"Historical PID and signal values are recorded metadata, not current process identity or attach authority.",
"Only exact Linux CORE NT_PRSTATUS/NT_SIGINFO owners are interpreted; every descriptor and owner retains original bytes. Other note semantics remain unknown.",
"NT_SIGINFO thread association is unknown. Only SIGSEGV SEGV_MAPERR/SEGV_ACCERR initially establish a fault-address union meaning.",
"PT_LOAD file bytes are recorded bytes. Undumped memory and current executable/library identity remain unknown; no current host file is resolved from a recorded pathname.",
"Missing notes/registers/signals do not establish their absence at the original crash. Full unwinding, executable pairing and live control are not provided."]}
except CoreFailure:
raise
except (ELFError, ValueError, AssertionError, IndexError) as error:
raise CoreFailure("format", "Recorded core failed unchanged upstream structural parsing: " + str(error)) from error
if __name__ == "__main__":
runtime_path = Path(__file__).with_name("decoder_runtime.py")
runtime = {"__file__": str(runtime_path), "__name__": "rea_decoder_runtime"}
exec(compile(runtime_path.read_bytes(), str(runtime_path), "exec"), runtime)
runtime["main"](sys.argv[-1], inspect_core, CoreFailure, PROFILE)
+1
View File
@@ -17,6 +17,7 @@ export default defineConfig({
text: "Getting started",
items: [
{ text: "Installation and setup", link: "/installation" },
{ text: "CLI and Evidence", link: "/cli" },
{ text: "Agent prompts", link: "/mcp-prompts" },
{ text: "Roadmap", link: "/roadmap" },
],
+8 -1
View File
@@ -111,7 +111,14 @@ 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. It does not decode DEX or claim observed
runtime calls.
runtime calls. Bridge hypotheses form a DEX/JVM-class by native-library
Cartesian product. The projection computes the product and exact serialized
candidate-array size before creating candidate objects, then retains a
deterministic prefix within a 2 MiB UTF-8 JSON budget. If candidates are omitted,
`bridge_candidate_coverage` reports total, emitted, and omitted counts; overall
coverage is `partial` while component arrays still include every component
from the supplied inventory pages. A path pair does not establish that a
managed declaration names, loads, or calls the native library.
## Resource and lifecycle limits
+105 -7
View File
@@ -20,16 +20,27 @@ rea project-apple-application-graph '{"inventory_evidence":[<inventory_artifact
}
```
The result includes every component in each inventory category and every
JavaScript-to-native bridge candidate pair. It has no caller-selected component
budget, prefix truncation, or omitted-count fields. Duplicate inventory pages
are merged by artifact identity and occurrence path; their Evidence IDs remain
attached as source Evidence.
The result includes every component in each inventory category. Bridge
hypotheses are the path-based JavaScript/native pairs within each application
root. To keep their Cartesian expansion bounded, the projection limits the
serialized candidate array to 2 MiB. It computes the pair count and
exact UTF-8 JSON size before creating candidate objects, then retains a
deterministic prefix when the byte budget is exceeded. The result's
`bridge_candidate_coverage` reports total, emitted, and omitted pair counts;
overall coverage becomes `partial` and a limitation explains the omission.
Duplicate inventory pages are merged by artifact identity and occurrence path;
their Evidence IDs remain attached as source Evidence.
For IPA inventories, archive components outside every application root remain
in the component lists. Their JavaScript and native candidates are grouped with
other unrooted components, preserving the inventory projection's path pairing
semantics without attributing them to an application bundle.
Coverage is `complete-within-inventory` when the supplied pages reconstruct the
complete authenticated inventory, and `partial` otherwise. A partial result
states that absence is unknown. Component and bridge-candidate arrays are still
the complete projection of the inventory that was supplied.
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.
## Application roots
@@ -110,3 +121,90 @@ 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.
Use `inspect_signature` on an executable candidate for code-signing claims.
## Dylib load resolution
`trace_dylib_resolution` answers which file each Mach-O dylib load reaches. It
runs on the active Mach-O, or on every executable in the active `.app`. Each
executable in a bundle (the main app, XPC services, app extensions, login items,
privileged helpers, and helper tools) is a separate process root with its own
`@executable_path`.
```sh
rea trace-dylib-resolution MyApp.app --json
rea trace-dylib-resolution MyApp.app --root Contents/MacOS/MyApp --architecture arm64 --json
```
```json
{
"name": "trace_dylib_resolution",
"arguments": { "roots": ["Contents/MacOS/MyApp"], "architecture": "arm64" }
}
```
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. Nothing is executed and no Apple tool is required, so the tool
also works off macOS.
Resolution follows dyld:
- `@executable_path` is the directory of the process root's executable. It is
undetermined when the root is a library.
- `@loader_path` is the directory of the image that holds the load command, or
of the image that owns the `LC_RPATH` entry being expanded.
- `@rpath` tries each `LC_RPATH` of the loading image first, then those of the
image that loaded it, up to the process root.
- Dependencies load dependents-first in load-command order, and each image
loads once per process. A later request whose install name matches an
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`.
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`,
`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 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 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 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` 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
image dyld will load; use `inspect_signature` for signing metadata.
- `dyld-environment-present`: preserves observed `LC_DYLD_ENVIRONMENT` entries.
Only an executable root supplies process settings; entries on library and
other non-executable roots do not alter resolution or coverage. Applicable
executable image-selection overrides are not modeled.
Empty dyld settings have different consumers. Modern dyld appends a slash and
image name to ordinary and fallback directory entries, so an empty entry can
search `/child.dylib` or `/Foo.framework/Foo`. REA retains uncertainty for those
unchecked paths; an empty fallback does not preempt an ordinary-search hit.
Empty versioned-directory lists do not scan a directory, and empty suffixes
only repeat the original candidate, so they do not add search uncertainty.
Reported command values are retained in all cases.
`verify:macos-bundle` checks the parser against `otool -l` for every traced
image. It also compares the predicted load order of two process roots with the
images dyld actually loads (`DYLD_PRINT_LIBRARIES`).
+11 -1
View File
@@ -26,11 +26,13 @@ graph TD
end
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"]
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"]
IDA_MCP["External ida-pro-mcp<br/>GUI proxy or database supervisor<br/>upstream installation and engine lifecycle"]
AUX["src/artifacts/ + src/native/<br/>disjoint artifact 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"]
@@ -92,6 +94,9 @@ graph TD
APP -->|record callbacks| RECORDS
SERVER -->|narrow record ports| RECORDS
RECORDS --> DOMAIN
COMPOSITION --> PWNTOOLS
PWNTOOLS --> PROCESS
PWNTOOLS --> DOMAIN
COMPOSITION --> HOPPER
COMPOSITION --> GHIDRA
COMPOSITION --> IDA
@@ -140,3 +145,8 @@ graph TD
TESTS -.-> BROWSER
TESTS -.-> EVALUATION
TESTS -.-> ERRORS
COMPOSITION --> EVM
APP -->|EVM interface port| EVM
EVM --> PROCESS
EVM --> DOMAIN
+131
View File
@@ -0,0 +1,131 @@
# Offline binary diagnostics
`inspect_binary_layout` / `inspect-binary-layout` answers what an explicit
file contains: section/segment layout, original symbols/relocations, static
linkage names and mitigation indicators. It works without an active Hopper,
Ghidra or IDA target. CTF is one possible use; crash triage, compatibility and
ordinary binary inspection use the same modular capability.
## Bring your own engine
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 \
rea inspect-binary-layout ./selected.elf --json
```
```json
{
"name": "inspect_binary_layout",
"arguments": { "path": "/artifacts/selected.elf" }
}
```
REA never installs Python, packages or GDB, changes a user init file, launches
the selected object as a host process, or requests runtime library resolution. The Python process
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).
## Interpreting results
- Artifact path, SHA-256 and size identify original selected bytes. All parsing
reads an owned stable snapshot; the original is unchanged.
- Addresses, offsets, lengths and flags are hexadecimal strings. A linked
address is not a runtime address. Runtime load base remains null. A zero
EXEC/DYN entry means absent; a relocatable entry is not applicable.
- Each symbol retains its table and entry index. Its reported value can mean
undefined, alignment, absolute value, no address, unknown section index,
section offset, TLS offset or linked virtual address. Duplicate names remain
separate entries. SHN_XINDEX remains unresolved in the selected upstream
representation; its external index table is not silently treated as resolved.
- Relocatable objects have section-relative relocations and no executable entry
claim. Signed REL/RELA relocation addends are decimal strings. Packed RELR
tables retain full original bytes and upstream-decoded offsets as derived
evidence, with per-offset packed-word locations and implicit addends unknown.
Overall relocation inventory completeness remains unknown.
A relocatable SHN_UNDEF target or inactive SHT_NULL header remains an explicit
unknown with the original reported section index and offset.
- REL/RELA symbol references retain their original table and entry indices.
Positive indices resolve through a validated symbol table; symbol index zero
means a zero symbol value without a table lookup, including when no table is
linked. Malformed references fail with the affected section and symbol index.
- Section-name and symbol tables require declared SHT_STRTAB links.
SHN_UNDEF explicitly means no section names: display is empty, raw name and
location unknown, with the original name offsets retained. Extended section-name
indices resolve through section zero and undergo the same validation.
- Symbol tables require a declared SHT_STRTAB link. Name offsets and terminators
must stay inside the declared string table; malformed references fail before
unrelated bytes can become names. Dynamic dependencies use DT_STRTAB/DT_STRSZ
with a unique file-backed mapping; ambiguous or unbacked mappings are
explicitly unsupported.
- Name display strings use UTF-8 replacement for opaque bytes, including
sectionless dependency names and interpreter paths. Raw name
bytes and string-table ranges retain observed identity where resolvable;
ranges include the terminating NUL and base64 bytes exclude it.
- NOBITS and NULL sections provide no file bytes. Reported file-backed ranges
are checked against snapshot size again at the public boundary. PT_NULL payload
fields are unused: reported numbers remain, file backing is none and interpreted
permissions are null. Segments with zero file size also have no file bytes.
- Sectionless dynamic images still report dependency names with raw bytes and
file locations where uniquely mapped. Missing symbol/relocation section tables
do not establish that those runtime tables are absent.
Upstream RELRO/canary heuristics rely on sections and can be incomplete in
this case; their reported candidates retain an explicit coverage limitation.
- DT_NEEDED/PT_INTERP are reported names, not resolved runtime paths. GOT/PLT
maps are derived convenience views and may collapse aliases. PLT inference
can emulate selected instructions in Unicorn; its outputs are static candidates.
Exec syscall tracing observes host process launches. Map completeness
remains unknown and original upstream warnings are returned inline.
- Canary, PIE, NX, executable-stack and RELRO are static inferences. NX and
executable-stack are separate upstream indicators: on x86-64 an executable
stack can accompany an unknown (`null`) NX indicator. ET_DYN can be a shared
library, and an absent canary symbol does not prove every function unprotected.
Complete results have a 32 MiB input, 64 MiB reply, 1 MiB combined diagnostics
and 30-second owned command deadline. The reply budget applies to the decoder
record; the CLI/MCP Evidence envelope adds copies and encoding overhead. The
pinned MCP SDK defaults to a 10 MiB receive buffer. Large complete results need
a caller-configured `StdioClientTransport({ maxBufferSize: ... })`; the large
extended-index verification uses 256 MiB and an explicit five-minute request
timeout in its separate opt-in MCP lane. REA does not silently truncate the result. Python lowers resource soft limits to at most 3 GiB virtual address space,
30 CPU seconds and 64 MiB file output. Inherited tighter soft/hard limits are
retained, and the effective values are returned in the result limitations. Virtual address space is not RSS;
Unicorn needs a large virtual map. Limits fail with no partial success, and
owned process/root cleanup completes independently of cancellation.
Core files, GDB session/state/control, optional pwndbg enrichment and instruction
inspection are separate increments tracked by #969. This increment makes no
real support claim for those features or other operating systems/architectures.
Typed decoder failures retain bounded `captured_output` (stdout, stderr and an
explicit truncation flag) alongside the original failure category and reason.
Successful `diagnostics` also exposes the supervisor's `truncated` flag. Output
beyond the complete diagnostic budget is rejected, including when the decoder
writes a valid reply; cleanup and late cancellation reuse the observed flag.
Memory allocation failures use `resource_constraint`, with effective resource
limits (or an explicit unknown) and memory-specific recovery guidance. A
MemoryError alone does not establish the exact failed allocation or that a
particular budget was exhausted. Invalid undersized symbol entries are rejected
before REA reports source ranges for them.
The bridge reserves 1 MiB to report allocation failures. If even error reporting
or serialization fails with MemoryError, its reserved exit status preserves the
resource classification with unknown effective limits.
Reserved exits require a matching private marker written by the bridge's actual
failure branch. Bare launcher exits or missing/unwritable markers retain process
diagnostics with the resource cause unverified.
Signal termination alone is never classified as observed memory exhaustion.
The small bootstrap also covers catchable MemoryError during adapter imports,
compilation and initialization. Python interpreter startup failures before the
bootstrap runs retain their observed process diagnostics without guessed causes.
SIGXCPU retains a CPU resource diagnostic and CPU-specific recovery advice.
Observed EFBIG writes or SIGXFSZ termination retain a file-size resource
diagnostic and file-size-specific recovery advice. If a tight file limit also
prevents writing the limit record, effective values remain unknown.
The bridge records actual limits in owned storage before analysis; missing or
malformed limit reports remain unknown with their read failure preserved. A
received signal alone does not establish its exact cause. Dynamic tags require
a complete DT_NULL inside PT_DYNAMIC; interpreter names and ranges end at the
first NUL, with any padding still represented by the original segment range.
+5 -3
View File
@@ -133,7 +133,7 @@ REA retains local observation data and removes only URL userinfo credentials, ex
- 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.
- 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. 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.
- 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 property paths/types, and discarded. `Network.getResponseBody` is never sent unless JSON body shape capture is selected.
- 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.
@@ -157,9 +157,11 @@ coverage. It reports excluded items and any values omitted or truncated by
validation; it does not drop records to meet an internal collection quota.
Disallowed-origin entries are filtered before retention.
CDP discovery, WebSocket connection, and commands continue until the peer responds or the caller cancels the operation; REA has no internal byte, message-count, or pending-command quota for these exchanges. Without caller cancellation, a stalled peer can leave an operation waiting. On cancellation, REA closes only its own WebSocket, waiting up to one second before terminating it. Malformed unsolicited protocol events poison the connection so later commands fail closed.
CDP discovery, WebSocket connection, and commands continue until the peer responds or the caller cancels the operation; REA has no internal byte, message-count, or pending-command quota for these exchanges. Without caller cancellation, a stalled peer can leave an operation waiting. On cancellation, REA 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. When a correlated reply reports a session ID, it must match the command's selected session. An optional method is reported unavailable only when the browser rejects it as unknown; other command rejections retain their reported failure.
Network and console coverage starts only after REA attaches and enables the relevant CDP domains. `prior_activity_available` is always `false`; absence from these arrays is not evidence that an event never occurred. 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. Every requested map, mapping, original source, AST node, and finding from parsed content is returned inline; failed maps remain explicitly unavailable and make bundle completeness partial. Original source artifacts with `.ts`, `.tsx`, `.mts`, or `.cts` suffixes carry `text/typescript`; other source names retain `text/javascript`.
Network and console coverage starts only after REA attaches and enables the relevant CDP domains. `prior_activity_available` is always `false`; absence from these arrays is not evidence that an event never occurred. If CDP reuses a request ID without supplying redirect metadata, REA retains the predecessor observation and marks network coverage incomplete.
Source maps are not fetched by inspection. Bundle analysis fetches them only when `fetch_source_maps` is selected, with exact-origin redirect checks and no credentials, cookies, or referrer. Each fetch operation has a 30-second deadline and a 64 MiB aggregate streamed-response budget across redirects. A deadline or byte-limit failure remains explicitly unavailable and makes bundle completeness partial. Every requested map, mapping, original source, AST node, and finding from parsed content is returned inline. Original source artifacts with `.ts`, `.tsx`, `.mts`, or `.cts` suffixes carry `text/typescript`; other source names retain `text/javascript`.
## Non-goals and threat model
+50
View File
@@ -385,6 +385,30 @@ claims. Actual Swift/Xcode, assetutil, native NIB, bundle signing and DMG lifecy
claims require the real Apple lane. Native binary API/value-flow semantics and
shared cross-domain contract aggregates keep their separate owners.
## Artifact inventory and extraction ownership
`src/artifacts/inventory/` owns format classification, reader selection, inventory
policy, canonical scanning and artifact graph construction. The shared artifact
provider and JavaScript reconstruction workflow consume this owner. Snapshot
semantics remain in `src/domain/artifactInventorySnapshot.ts`.
`src/artifacts/extraction/` owns member selection, integrity checks and transactional
materialization through the existing readers and safe output tree. Reader cleanup,
cancellation, rollback, archive containment and caller-visible results retain their
existing behavior. Acquisition guards reject imports from these producer owners
into application workflows, production composition or CLI/MCP adapters.
`src/application/artifacts/ArtifactExtractionDestination.ts` owns the caller's
default temporary output-root choice for both CLI and MCP. The adapters pass this
destination to the existing provider operation; format handling and member writes
remain in the artifact owner.
Add format-specific acquisition behavior beside the inventory readers and exercise
its actual producer representation in filesystem boundary tests. Add caller
workflow behavior in the application owner and verify both adapters. The installed
package lane checks inventory identity and extraction through CLI and MCP; the
real Apple bundle lane separately establishes DMG mount and detach behavior.
## Binary production composition closure
`src/composition/binary.ts` is the shared production owner for deep candidate
@@ -425,3 +449,29 @@ native-value verifier locations are retained until their own proof lanes migrate
interactive macOS UI success and real dependency tracing require their respective
host/engine workflows. Portable source fixtures and injected call-route tests
establish their narrower boundaries, without expanding provider/platform support.
## Process capture and analyst workflow ownership
`src/domain/process/` owns scenario/capture validation, portable process trees,
trace specifications, observations, comparisons and the canonical process Evidence
identity. The standalone contract example lives in `src/contracts/process/`;
shared session contracts retain their broader investigation owner.
`src/process/capture/` owns the actual PTY boundary, terminal rendering, sampling,
selected child environment, filesystem snapshots/effects, runtime path admission,
event journal, settlement and resource cleanup. It reuses `src/process/` ownership
primitives and the Windows host substrate. Its identity import follows the
existing pure declaration directly. A source guard rejects outward application,
composition and caller imports; no new provider or lifecycle interface is added.
`src/application/process/` retains Evidence projection and file-backed CLI
capture/comparison workflows. Capture helper tests use the forked adapter lane;
Evidence/host integration tests use the serial process-boundary lane. Their
fixtures and assertions remain unchanged. Installed capability-probe URLs follow
the new compiled adapter path, including the optional-dependency failure probe.
Windows PTY capture retains its existing unavailable outcome because descendant
cleanup is not yet verified. Native Job Objects and a PTY binary do not establish
that capture workflow. Real POSIX capture/cleanup and installed terminal/PTY module
resolution require their actual host and package lanes; portable comparison
fixtures establish their separate evidence semantics.
+184
View File
@@ -0,0 +1,184 @@
# CLI and Evidence
Use the CLI for a direct inspection or a script. It uses the same application
workflows and evidence contracts as REA's MCP server. Each CLI invocation is a
separate process; save results when you want to use them in a later command.
## Run a command
After [installing REA](installation.md), use `rea`. You can also run a command
without a global installation:
```bash
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
Use an extracted application directory or an ASAR as the target. Static
JavaScript analysis returns the application graph and Evidence directly.
Generic `rea analyze PATH` selects this workflow for directories and `.asar`
files when neither `--provider` nor `--snapshot` is supplied. See
[JavaScript artifact reconstruction](javascript-artifact-reconstruction.md)
for results, integrity checks and coverage.
## Native analysis
Configure [Hopper or Ghidra](installation.md#hopper), or the
[IDA adapter](ida-provider.md), before analyzing a native target. Substitute
your target, search text and function name or address in these examples:
```bash
rea analyze /absolute/path/to/program --provider ghidra --json
rea search /absolute/path/to/program "search" --provider ghidra --json
rea function /absolute/path/to/program main --provider ghidra --json
rea decompile /absolute/path/to/program 0x1000 --provider ghidra --json
rea xrefs /absolute/path/to/program 0x1000 --provider ghidra --json
rea trace /absolute/path/to/program "search" --provider ghidra --json
```
`analyze` and `inspect` share the native overview workflow. `function` returns
a function dossier; `decompile` returns pseudocode. For assembly instructions
alone, use `rea instructions`. A macOS `.app` bundle can be supplied directly.
Run `rea --help` or a command's `--help` for arguments and output options.
The [generated catalog](product-catalog.json) lists all CLI commands and MCP
tools. Provider-specific target support is described in the
[native guide](native-investigation.md), [DOS guide](ghidra-dos.md), and
[Windows Ghidra guide](windows-ghidra-p0.md).
## Choose a provider
List the available providers and their supported operations:
```bash
rea providers --json
rea capabilities --json
```
These commands describe binary-session providers and auxiliary capabilities.
For the complete MCP surface, use the connected server's tool list and the
session's advertised availability; individual guides describe prerequisites.
With automatic selection, REA uses the single available provider that supports
the target. When several providers support it, select one explicitly:
```bash
rea analyze /absolute/path/to/program --provider hopper
```
Set `REA_ANALYSIS_PROVIDER` for a standing preference. An explicit `--provider`
overrides it. In MCP, pass `provider_id` to `open_binary`:
```json
{
"path": "/absolute/path/to/program",
"provider_id": "hopper"
}
```
The selected provider remains bound to the session until an explicit switch
or close. Provider failures are returned with their original reason. For an
`ambiguous` selection error, choose from `details.candidate_ids`; for
`provider_unavailable`, run `rea doctor --provider ID --json` to diagnose the
selected engine. See [task readiness](installation.md#check-readiness-for-your-task)
and [provider selection](adr/0001-provider-selection-and-analysis-profiles.md).
The session reports work still in progress through `analysis_activity`.
A client timeout can end its wait while the provider continues analyzing.
`cleanup_incomplete` identifies resources whose shutdown or removal could not
be verified. See [MCP contracts](mcp-contracts.md) for session lifecycle and
[Ghidra first-query deadlines](mcp-contracts.md#ghidra-first-query-deadlines-and-recovery)
for import, client timeouts and recovery.
## Save and reuse analysis snapshots
A snapshot retains successful analysis results for later queries. REA reuses
an exact result when the target bytes, operation, parameters, provider and
settings match. Mutations and cursor-dependent calls are excluded from the
cache. Snapshot files are local and use owner-only permissions.
```bash
rea analyze /absolute/path/to/program --provider ghidra --snapshot /absolute/path/to/analysis/program.json
# Repeat the same query to reuse its saved result.
rea analyze /absolute/path/to/program --provider ghidra --snapshot /absolute/path/to/analysis/program.json
```
An exact CLI cache hit is read before starting a provider process.
In MCP, `open_binary` accepts `snapshot_path` to import a snapshot atomically
for its matching target; an MCP provider may still start before returning a
cached result. `close_binary` accepts `snapshot_path` and optional
`overwrite: true` to save before releasing provider resources. A failed save
leaves the session open so the caller can resolve the output failure.
## Import, export and compare Evidence
Evidence records retain artifact identity, source locations, observations,
inferences and unresolved findings. Validate an existing bundle, export its
canonical form, or compare two supplied bundles:
```bash
rea evidence-import /absolute/path/to/evidence/bundle.json
rea evidence-export /absolute/path/to/evidence/bundle.json /absolute/path/to/evidence/canonical.json
rea compare /absolute/path/to/evidence/left.json /absolute/path/to/evidence/right.json
```
Exports preserve an existing destination unless `--overwrite` is explicit.
For application traces and version comparisons, see
[JavaScript application workflows](javascript-application-workflows.md).
MCP [retained Evidence references](mcp-contracts.md#retained-application-evidence-inputs)
belong to one connection. Separate CLI calls consume full saved records.
## Import historical source
Import an older source tree to compare with the current artifact:
```bash
rea import-reference-source /absolute/path/to/source
```
The import records hashes and metadata for the supplied files, separately
from observations of the current app. File names do not automatically exclude
files. Set `REA_REFERENCE_SECRET_PATTERNS_JSON` to a JSON array of ignore
patterns when you want to exclude selected paths.
Historical-source import requires safe no-follow file opens on Linux or
macOS. Native Windows returns `unsupported_host`; use Linux REA inside WSL
or another supported host. See
[source-to-bundle comparison](javascript-application-workflows.md#historical-source-to-bundle-comparison)
for mapping identities, inferences and unknowns.
## Capture runtime behavior
Choose the guide for the target and intended interaction:
- [Browser observation](browser-observation.md): inspect a selected page through CDP.
- [Browser scenarios](browser-scenario-contract.md): run declared interactions and capture their results.
- [Electron observation](electron-observation.md): inspect an Electron renderer or capture an application scenario.
- [Node/Electron Inspector](javascript-runtime-observation.md): record script locations and execution contexts.
- [Process capture](process-capture.md): run an executable and compare terminal, exit and filesystem observations.
Runtime requests name the target and actions. Launched targets run with your
user permissions; consult the chosen guide for host requirements and effects.
## Output and exit status
The default terminal format is TOON. Use `--json` when saving results for a
JSON consumer. Output selection and formatting do not change operation status.
| Status | Meaning |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0` | The operation completed. Its result may include partial evidence, warnings or unresolved questions. |
| `1` | The operation could not complete. Structured output identifies invalid input, permissions, cancellation, timeouts or another failure when available. |
| `128 + N` | Signal `N` ended the process, where the shell or runtime preserves the conventional signal-derived status. |
`setup --dry-run` returns `planned` and exits `0`; a cancelled setup also exits
`0`. Setup returns `1` for `needs_confirmation` or `needs_human`.
`doctor` returns `1` when required checks in its selected readiness scope fail;
unavailable optional providers remain informational for unrelated tasks.
Enable `pipefail` in a supporting shell so a downstream formatter preserves
REA's failure status:
```bash
set -o pipefail
rea inspect-artifact ./app.asar --json | jq . > inspection.json
```
+12 -3
View File
@@ -86,9 +86,18 @@ rea capture-electron-scenario scenario.json --json
The result records action status and targets, correlated app/window/WebContents,
preload, session, navigation, shell, permission, popup, download, protocol,
native-addon, process, and IPC timeline events. IPC channels are capped at
1,024 characters and argument-shape metadata at 32 entries; values are never
retained. Renderer crash/restart and deep-link actions are synthetic
native-addon, process, and IPC timeline events. Event retention has a 16 MiB
accounting budget for full channel strings, argument-shape arrays, and an
estimated object-graph cost. Admission computes an exact JSON byte length
incrementally, then charges twice that size plus 256 bytes per event and 8 bytes
per argument shape. The doubled JSON charge covers the timeline and IPC views;
the additional charge estimates their in-process object and array storage. This
is a resource estimate, not an exact V8 heap measurement. An event that does not
fit is dropped whole. The result reports the estimate, serialized-byte upper
bound, dropped event families, and
observed/dropped totals for IPC and runtime events. Retained sequence numbers
keep their original capture identities. Payload values are never retained.
Renderer crash/restart and deep-link actions are synthetic
scenario controls; their attempted and observed outcomes remain in the
timeline. The active hook blocks and records external shell/navigation,
permission, download, popup, updater, and OS-integration effects. The timeline
+192 -32
View File
@@ -10,10 +10,12 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
@@ -29,7 +31,79 @@
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"code": { "type": "string", "const": "access_denied" },
"category": {
"type": "string",
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
},
"message": { "type": "string", "minLength": 1 },
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"code": { "type": "string", "const": "artifact_changed" },
"category": {
"type": "string",
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
},
"message": { "type": "string", "minLength": 1 },
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
@@ -44,10 +118,12 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
@@ -63,7 +139,7 @@
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
@@ -78,10 +154,12 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
@@ -97,7 +175,43 @@
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"code": { "type": "string", "const": "unsupported_target" },
"category": {
"type": "string",
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
},
"message": { "type": "string", "minLength": 1 },
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
@@ -112,10 +226,12 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
@@ -131,7 +247,7 @@
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
@@ -146,10 +262,12 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
@@ -165,7 +283,43 @@
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"code": { "type": "string", "const": "resource_constraint" },
"category": {
"type": "string",
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
},
"message": { "type": "string", "minLength": 1 },
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
@@ -180,10 +334,12 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
@@ -199,7 +355,7 @@
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
@@ -214,10 +370,12 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
@@ -270,10 +428,12 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
@@ -289,7 +449,7 @@
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
@@ -304,10 +464,12 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
@@ -323,7 +485,7 @@
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
@@ -338,10 +500,12 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
@@ -357,7 +521,7 @@
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
@@ -372,10 +536,12 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
@@ -391,7 +557,7 @@
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
@@ -406,10 +572,12 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
@@ -425,7 +593,7 @@
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
@@ -440,10 +608,12 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
@@ -459,7 +629,7 @@
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
@@ -474,10 +644,12 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
@@ -493,7 +665,7 @@
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
@@ -508,10 +680,12 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
@@ -527,7 +701,7 @@
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
@@ -542,10 +716,12 @@
"enum": [
"invalid_input",
"unsupported_provider",
"unsupported_target",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"resource_constraint",
"unavailable",
"execution_failure"
]
@@ -561,27 +737,11 @@
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
"additionalProperties": {}
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
"additionalProperties": false
}
],
"$defs": {
"__schema0": {
"anyOf": [
{ "type": "string" },
{ "type": "number" },
{ "type": "boolean" },
{ "type": "null" },
{ "type": "array", "items": { "$ref": "#/$defs/__schema0" } },
{
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
}
]
}
}
]
}
+84
View File
@@ -0,0 +1,84 @@
# Offline EVM bytecode
`inspect_evm_interface` / `inspect-evm-interface` infers interface candidates
from one explicitly selected local bytecode carrier. The capability is general:
contract investigation, compatibility, audit and CTF showcases use the same
portable primitive. It does not require an active native binary target.
```sh
rea inspect-evm-interface ./runtime.hex hex --json
rea inspect-evm-interface ./runtime.bin raw --json
```
```json
{
"name": "inspect_evm_interface",
"arguments": { "path": "/artifacts/runtime.hex", "encoding": "hex" }
}
```
Initial real verification covers flat legacy bytecode on Linux x64 and unchanged
bundled EVMole 0.9.3. EOF-style EF00 containers return `unsupported_target`,
with the selected path and format reason. Provider/configuration failures retain
their distinct unavailable-provider or capability classification.
The host must already provide util-linux `prlimit`, normally `/usr/bin/prlimit`;
`REA_EVM_PRLIMIT_COMMAND` can select another absolute executable. REA installs
no system tools and changes no user configuration. [Upstream provenance and
license](https://github.com/morluto/rea/blob/main/third_party/evmole/README.md) are packaged.
## Evidence semantics
- `encoding` is required: raw bytes or UTF-8 hex, with optional `0x` prefix and
outer ASCII whitespace. Odd hex, interior separators, BOM, non-ASCII whitespace
and invalid UTF-8 are rejected. Empty bytecode is valid and has unknown coverage.
- Carrier SHA-256 identifies the exact original file, including prefix/casing
and whitespace. Decoded-byte SHA-256 identifies actual selected EVM bytes.
Neither digest is the Ethereum Keccak code hash.
- Full bytecode, upstream function offsets and original producer representation
are returned inline. Offsets are byte positions in decoded bytecode.
- Upstream also returns terminal metadata during selector recovery. Its reported
values remain in `raw_result`; integer precision and non-text-key coverage are
unknown in the JS binding. Full bytecode retains the original encoded metadata.
- Selectors, argument strings and state mutability are **inferences**. Function
names/signatures are not looked up. Optimized code may produce different
mutability candidates; recovered types are not a verified source ABI.
- Runtime/init-code identity, deployed authenticity, hardfork and discovery
completeness remain unknown. Empty candidates do not prove no callable interface.
- Analysis is offline: no target runtime execution, implicit chain/RPC/explorer
lookup, wallet action, transaction broadcast or network request.
## Resource and lifecycle limits
Inputs are limited to 4 MiB, complete replies to 16 MiB and combined diagnostic
output to 1 MiB. An owned worker has a 256 MiB JavaScript heap, 3 GiB virtual
address-space limit, 30 CPU seconds and a 30-second command deadline. Virtual
address space differs from resident memory. The worker retains inherited tighter
soft/hard limits, reports its configured soft limits inline and leaves hard limits
unchanged. WASM trap reservations are disabled
so the engine can run within that virtual limit; actual memory is not equated to
its 3 GiB allowance. Limits fail without partial success. Stable snapshots,
private files and owned processes are cleaned independently of caller cancellation.
Invalid-input, unsupported, cancellation, timeout and malformed-reply failures
retain bounded stdout/stderr under `details.captured_output`, including whether
diagnostic output was truncated. Cleanup failures preserve the
original error projection and any captured output.
Successful results expose `diagnostics.truncated` from the supervisor as well.
Excess diagnostic output fails the complete-output contract even when the worker
has written a valid reply.
SIGXCPU retains a CPU resource diagnostic and CPU-specific guidance. Observed
file-size write failures (EFBIG) and SIGXFSZ likewise retain a file-size resource
diagnostic. Exact signal/write causes remain unknown. Configured
soft limits are reported separately from unknown effective limits; the received
signal alone does not establish its exact cause.
Reserved file-size exits require a matching private marker written by the
worker's EFBIG branch. Bare launcher exits or unwritable markers retain process
diagnostics with the resource cause unverified.
Source/metadata, proxy resolution, storage/CFG and fork-explicit execution/traces
remain separate increments under #972, with materially different result or
execution authority. This primitive makes no deployment or runtime behavior claim.
The real verification lane traces representative raw/hex CLI workflows and checks
that only declared Node/prlimit launchers and process ownership inspection run.
Socket traces permit local stdio socket pairs and reject attempted network requests.
These observations verify those fixtures; they are not a general network sandbox.
+3 -2
View File
@@ -129,8 +129,9 @@ tree semantics, vulnerability scanning, emulation or hardware runtime capture.
Unblob one worker. No automatic parallel extraction engines.
- **120-second** deadline; inherited **1 GiB per-process address-space** limit,
120 CPU seconds and zero core-dump allowance through `prlimit`.
- JSON reports: at most **8 MiB**; oversized reports fail without a truncated
success. Diagnostic output is bounded with exact captured byte counts.
- JSON reports: at most **8 MiB** and valid UTF-8; malformed encoding and
oversized reports fail without repairing or truncating producer data.
Diagnostic output is bounded with exact captured byte counts.
- Default staging budget: **256 MiB** and **10,000 entries**, configurable using
`max_output_bytes` / `max_output_files` (CLI `--max-output-bytes` /
`--max-output-files`). Depth defaults to 3, allowed range 1–10.
+6 -2
View File
@@ -1,7 +1,7 @@
# DOS MZ and COM analysis with Ghidra
REA can analyze DOS MZ executables and explicitly selected COM images through the bring-your-own Ghidra adapter.
The Linux x64 verification lane exercises actual 16-bit disassembly and
The Linux x64 and macOS arm64 verification lanes exercise actual 16-bit disassembly and
decompilation through the CLI and stdio MCP. Provider admission accepts Ghidra
12.1.x and that installation's declared JDK range. The verification lane uses
Ghidra 12.1.4 and JDK 21; no DOS emulator or cross-compiler is required.
@@ -87,6 +87,10 @@ address for subsequent lookups and leaves instruction decoding context with
Ghidra. These coordinates are analysis load addresses, not file offsets or
observations of a running DOS system.
Offsets that Ghidra would truncate to a different address are rejected before
reading or annotating a function. This also applies to explicit or encoded
address-space names; leading zeros preserve the numeric offset.
`procedure_info` and function dossiers include `body` evidence:
```json
@@ -150,7 +154,7 @@ are separate from the initialized MZ module. Raw COM files and DOS extenders
without an admitted MZ real-mode entry are outside this import boundary.
DOS MZ is available through Ghidra, with no Hopper support claim. It is outside
the Windows native PE P0 boundary. macOS DOS verification remains unverified.
the Windows native PE P0 boundary. macOS x64 DOS verification remains unverified.
## Real verification
+169 -13
View File
@@ -68,13 +68,18 @@ selects the npm release, and persistent MCP registrations are pinned to the
version that performed setup. Installing newer instructions does not update a
running server or its registration.
The release checked on **2026-10-06** was **4.1.0** (125 MCP tools). That artifact
includes main's Windows native control bundle, Android/JADX and firmware
tools, Ghidra function annotation workflow, and retained application-Evidence
references.
Main's catalog describes the current code; a source build or a subsequent
release containing later changes is required for anything newer. Package startup alone does not verify a
provider's real platform workflow.
The release checked on **2026-10-07** was **5.0.0** (133 MCP tools), published
from the fixed checkpoint
[`b33236ec`](https://github.com/morluto/rea/releases/tag/rea-agents-5.0.0).
The public CLI, MCP catalog and target-free session, and isolated update from
4.1.0 to 5.0.0 were verified through npm. The artifact includes Windows native
controls, Android/JADX and firmware tools, Ghidra function annotations, and
retained application-Evidence references.
Main's catalog describes the current code. A source build or a subsequent
release containing changes after this checkpoint is required for newer
functionality. Package startup alone does not verify a provider's real
platform workflow.
To check the published version, run `npm view rea-agents dist-tags.latest`.
Use the connected server's actual tool list and advertised input schemas for
@@ -90,7 +95,7 @@ npx skills add morluto/rea --skill reverse-engineer-anything
This installs agent instructions and bundled references, not REA MCP
registration or analysis engines. Follow the skill's
[conditional connection guide](https://github.com/morluto/rea/blob/main/skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed).
[conditional connection guide](https://github.com/morluto/rea/blob/main/skill-src/reverse-engineer-anything/SKILL.md#connect-only-when-needed).
Working tools can be used immediately. If tools are missing, inspect the current
client's registration with `doctor --client codex --json` (substitute its client
ID), then plan repairs with `setup --client codex --dry-run --json`. Show and
@@ -110,7 +115,7 @@ npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --
It returns the complete Evidence record directly and requires no native engine.
Provider failures in doctor do not prevent unrelated target-free tools. To check
readiness for one task instead of auditing every integration, see
[Check readiness for the task at hand](https://github.com/morluto/rea/blob/main/README.md#check-readiness-for-the-task-at-hand).
[Check readiness for your task](#check-readiness-for-your-task).
## Supported agents
@@ -130,12 +135,27 @@ Setup can configure these clients for REA's local MCP server:
| GitHub Copilot CLI | `copilot_cli` |
| Command Code | `commandcode` |
| VS Code | `vscode` |
| Grok Build | `grok_build` |
| Grok Bot | `grok_bot` |
For OpenCode, setup writes the V1 `mcp.rea` entry, which OpenCode V1 and V2
both load. If the configuration already uses OpenCode V2's native
`mcp.servers` table, setup registers REA there instead and replaces any earlier
`mcp.rea` entry from REA.
Grok Build loads `[mcp_servers.rea]` from `$GROK_HOME/config.toml`, or from
`~/.grok/config.toml` when `GROK_HOME` is unset. Setup edits that server
table, `[mcp_servers.rea.env]`, and a root `disabled_mcp_servers` entry that
names `rea`. It sets `startup_timeout_sec = 30` and leaves every other name
in that list. The shared skill installed under `~/.agents/skills` is already
on Grok Build's skill path.
Grok Bot is detected from `~/.grokbot`, or from `SAND_DATA_ROOT` when that
value is an absolute path. Setup writes `mcpServers.rea` to `mcp.json` in that
directory. The desktop app keeps the connectors it actually runs in the
signed-in account. That file is the registration to add there; setup does not
call the account API.
## Review setup changes
`rea setup` first offers the supported agents in a multi-select. Existing REA
@@ -204,7 +224,7 @@ rea setup --yes --all-detected --install-hopper --json
Setup pins package-runner MCP registrations to the exact installed REA version,
installs the matching skill and on-demand references in the same plan, and adds
`startup_timeout_sec = 30` for Codex. `rea update` installs the exact resolved
`startup_timeout_sec = 30` for Codex and Grok Build. `rea update` installs the exact resolved
release into the npm prefix that owns the running package, then checks the new
executable's version before reporting success. It does not reopen onboarding.
Release lookup and installation both use npm's configured registry.
@@ -213,12 +233,44 @@ installed REA skill. Run the returned scoped setup command to review and approve
those changes, then restart affected agents. The plan is returned in terminal,
non-TTY, and JSON modes without applying configuration changes.
## Check readiness for your task
Run `rea doctor` when you need diagnosis. It reads host prerequisites and agent
configuration without changing them. Without options, it audits every detected
registration, the installed skill and optional analysis engines; its overall
`healthy` value can be false while your chosen workflow works.
Select the part you want to check:
| Task | Readiness check |
| ----------------------------------- | ------------------------------------------------------------------------ |
| Static JavaScript/Electron analysis | Run `rea analyze-javascript-application PATH --json` directly. |
| One analysis engine | `rea doctor --provider ghidra --json` (or `hopper`, `ida`) |
| One agent registration | `rea doctor --client codex --json` (see [client IDs](#supported-agents)) |
| Installed workflow instructions | `rea doctor --skill --json` |
A scoped report has `scope.mode: "explicit"`. Its `scope_checks` determine
`healthy` and the exit status. Other checks appear in `informational_checks`;
`environment_healthy` summarizes the full audit. `--target` adds a target check,
but the report remains audit-wide unless a scope option is also supplied.
Follow the remediation for the failed check. If a native target has several
available providers, choose one with `--provider` on the CLI or `provider_id`
on `open_binary`. A `capability_unavailable` failure carries
`details.selection_reason`: `ambiguous` asks you to choose from
`details.candidate_ids`, while `provider_unavailable` asks you to repair the
selected engine. See [provider selection](cli.md#choose-a-provider).
## Hopper
Hopper is separate commercial software with its own license. Its free demo has
vendor-defined limits, and a paid license is optional. REA reuses any detected
installation and preserves Hopper during uninstall.
The supported native host baseline is macOS 12+, Ubuntu 24.04+, Fedora 41+,
64-bit Arch Linux, or CachyOS. Ghidra and IDA have their own provider-specific
host requirements; Windows Ghidra uses the [experimental P0 boundary](windows-ghidra-p0.md).
On macOS, approved setup downloads the official DMG, checks its published size
and digest, validates the application bundle, and atomically installs it to
`~/Applications/Hopper Disassembler.app`. REA then opens Hopper so the
@@ -247,6 +299,89 @@ host mount and the rest of `/tmp` remain unchanged; this fallback never invokes
`sudo`. `rea doctor --provider hopper --json` reports the selected
strategy and both host and effective mount facts.
### Launcher paths and troubleshooting
On macOS, REA uses
`/Applications/Hopper Disassembler.app/Contents/MacOS/hopper` by default.
On Linux, it prefers executable `/opt/hopper/bin/Hopper`, then executable
`~/.local/share/rea/hopper/bin/Hopper`; if neither exists, the former remains
the diagnostic fallback. `HOPPER_LAUNCHER_PATH` overrides those choices:
```bash
export HOPPER_LAUNCHER_PATH=/absolute/path/to/Hopper
rea doctor --provider hopper --json
```
If a Linux launcher exists but cannot start, inspect its shared libraries:
```bash
ldd /absolute/path/to/Hopper | grep 'not found'
```
Install the missing packages and rerun the scoped check. Linux demo sessions
need Xvfb, Python 3, X11 and XTEST; approved setup installs those dependencies
on supported distributions. Add the curl installer's reported executable
directory, commonly `~/.local/bin`, to your shell `PATH` if needed.
REA starts Hopper when needed. On macOS, its launcher can bring a window or
first-run dialog forward; choose demo mode or activate your existing license.
Hopper serializes analysis requests. Cancelling a wait can leave provider work
running, which the session reports. Successful decompilation is cached until
a relevant rename or comment changes it.
Analysis and annotation calls stay bound to the active target's native Hopper
document, even when GUI focus changes or other documents have the same display
name. Use `open_binary` to change targets. Byte reads stop at a segment boundary
and return the readable prefix with `complete: false`. File-offset mapping checks
the reverse lookup and original executable bounds; synthetic external-symbol
memory has no original file offset. For FAT Mach-O, offsets refer to the original
container file. Results retain Hopper's image-relative offset, the observed slice
base, and the source executable path.
Loader selection reads the Mach-O container header, including FAT files with a
single architecture. For FAT64, REA validates the architecture table and selected
Mach-O header, prepares a private thin image, and loads it with Hopper's native
Mach-O loader. It checks the entire source's SHA-256 while copying the slice;
source identity and reported offsets still refer to the original container.
An ambiguous architecture subtype requires an explicitly extracted thin image;
REA does not guess. Configured loader arguments remain explicit overrides.
FAT64 is distinct from the CPU architecture: FAT32 containers can contain 64-bit
executable images. This preparation avoids the Raw Binary loader dialog observed
with native FAT64 loading on Hopper 6.1.0-demo.
The prepared image and its owned document close together, including on MCP exit;
if document closure is unconfirmed, REA retains the backing image and reports
`cleanup_incomplete` with its path. Ordinary documents retain their existing
MCP-exit behavior.
Startup deadlines report missing bridge readiness and preserve the launcher
outcome; a successful helper exit does not prove that a loader dialog completed.
Cursor navigation returns the observed object start when Hopper snaps an interior
address; adjacent-object navigation rejects unmapped inputs and document ends.
Native API text rejects NUL characters and unpaired Unicode surrogates before
annotation changes. Renames preserve unselected label owners; use a batch with
all affected addresses to move or swap existing labels explicitly. Every rename
destination must be mapped, and native symbol names must fit Hopper's 1024 UTF-16
code-unit limit. Oversized names fail before any batch edits; bookmarks and
literal string results are not subject to that symbol-name limit. Rename success
requires exact final readback. New bookmarks must point into mapped memory;
existing legacy bookmarks outside it can still be removed.
String results read each native typed object's complete bytes, retain the original
provider display in `provider_value`, and report its encoding, byte length, and
termination. `encoding_status: inferred` distinguishes REA's decoding from an
observed source encoding. Hopper can split long literals into adjacent
unterminated objects; search matches each object's decoded bytes independently.
Undecodable objects retain native display text with `decoding.available: false`
and a reason, so one uncertain object does not block unrelated inspections.
Function dossiers retain this same string evidence. Native call edges retain
Hopper's partial `CallReference` classification and exact endpoints; detailed
reference flags remain unavailable rather than being invented.
Regex searches use ECMAScript Unicode syntax in a cancellable worker with a
five-second matching deadline. Deadline or cancellation stops matching while
leaving the Hopper API available. Literal mode retains Unicode casefold matching.
Closing or switching a target closes its bound Hopper document, shuts down REA's
bridge and removes its temporary socket directory while preserving the Hopper
application and unrelated documents. A `cleanup_incomplete`
result identifies resources whose cleanup could not be verified.
### Hopper in CI
REA's unattended Linux path is validated against Hopper's offered demo mode.
@@ -289,7 +424,7 @@ entry comments atomically and returns refreshed analysis. These session metadata
edits leave executable bytes unchanged and are discarded on close. GUI controls
require Hopper; Windows P0 remains read-only.
Windows P0 admits native x86-64 PE applications on fixed local NTFS volumes.
Windows P0 admits native x86 and x86-64 PE applications on fixed local NTFS volumes.
The npm package bundles native Job Object ownership, protected private runtime
DACLs, and handle-based path admission; no separate addon installation is needed.
See the [Windows Ghidra P0 guide](windows-ghidra-p0.md) for verified scope and
@@ -333,7 +468,17 @@ home/cache/config/temp paths. REA passes `-readOnly`, `-deleteProject`, uses
Ghidra's default analysis and resource settings, and loads its packaged Java
bridge via `-scriptPath`; it never opens an existing user project. Linux and
macOS use a current-user-only local bridge socket and descriptor. The
experimental Windows transport uses authenticated IPv4 loopback with a
project remains under the selected temporary directory. If its Unix socket
pathname would exceed the host's byte limit, REA allocates a separate mode-0700
socket directory under `/tmp` and removes it on close, cancellation, or failure.
Diagnostics retain the actual endpoint and both owned directories.
On macOS, REA starts the inspected JVM directly using Ghidra's own LaunchSupport
configuration. Apple platform shell wrappers hide their environments from
ownership inspection, so retaining those wrappers would prevent verified
process-group cancellation during startup.
If ownership remains unverifiable, it reports the reason and retains the process
supervisor and private runtime instead of removing files beneath a live provider.
The experimental Windows transport uses authenticated IPv4 loopback with a
private native-owned bearer descriptor and Job Object process ownership.
Operations begin only after default auto-analysis completes. `open_binary`
@@ -389,6 +534,17 @@ Ghidra/JDK installation; neither is inferred from startup alone.
`rea doctor --json` is strictly read-only. `rea update` updates only the npm installation that owns the running CLI. Source checkouts and package-runner copies must be updated through the mechanism that owns them; a fresh package-runner invocation can use `npx rea-agents@latest`. `rea uninstall` removes only REA-owned agent registrations and skill files; `--purge-data` additionally removes REA cache and state paths.
```bash
rea update
rea uninstall
rea uninstall --purge-data
```
Uninstall preserves Hopper, Node.js, Evidence files, captures, unrelated skills
and other MCP servers. Purging removes only REA's cache and state under
`~/.rea`; malformed client configuration or purge-path symlinks stop the
operation. See the [CLI guide](cli.md#output-and-exit-status) for exit statuses.
## MCP Registry
REA is published in the official MCP Registry as `io.github.morluto/rea`. Registry
@@ -408,7 +564,7 @@ For a client that requires manual configuration, use:
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@5.0.0", "mcp"]
"args": ["-y", "rea-agents@6.0.0", "mcp"]
}
}
}
+114 -8
View File
@@ -31,6 +31,11 @@ command and the generic route return their complete results inline. Native
targets, single JavaScript files, `.app` bundles, and explicit deep-provider or
snapshot requests retain the native deep-analysis route.
Both CLI routes emit `rea_progress` JSON lines on stderr as inventory, source
parsing, graph construction, validation, and Evidence hashing begin. Stdout
remains the selected result document. Completion is reported after Evidence
creation; a failed analysis retains its typed diagnostic result.
Configure an MCP client with the ordinary REA setup command:
```bash
@@ -52,22 +57,65 @@ manifest and graph commitments, JavaScript Application Graph, static
Electron summary, reconstruction statistics, and explicit limitations. It does
not require a live Hopper, Ghidra, browser, or Electron process.
## ASAR integrity
ASAR inventory checks Electron integrity metadata for embedded archive entries
and supplied `.asar.unpacked` companion files. An integrity failure identifies
the logical path, declared and calculated SHA-256 values, and whether the entry
was unpacked. By default, a mismatch is returned as a failure with its artifact
context. Requests that support `integrity_policy` can explicitly select
`record-and-continue` to inspect verified siblings while retaining the mismatch.
An unpacked entry whose companion bytes were not supplied remains
`unavailable`. REA continues analyzing embedded JavaScript and records the
missing native/resource bytes as unknown. See [what is reconstructed](#what-is-reconstructed)
for the inventory fields and [MCP integrity handling](mcp-contracts.md#integrity-record-and-continue)
for the tool-result contract.
## Large results
Application analysis projects semantic relationships one source file at a time.
After projection, it retains only the lexical module facts and exported return
shapes needed to build application relationships. Full source IR does not stay
resident for every file together. Callable ownership and contained-reference
queries use per-file range indexes, while call sites use exact-range lookups.
Existing node budgets and coverage reporting remain in effect.
Progress identifies the source file being parsed and projected. Cancellation is
checked between files, with event-loop yields that allow notifications and
completed file-local allocations to be released. A single file's synchronous
parser and semantic extraction still need to finish before that boundary.
Graph and Evidence identifiers hash canonical JSON incrementally, without
assembling a single string for the whole graph. The canonical bytes and existing
identifiers remain unchanged. The opt-in regression check is
`npm run verify:javascript:digests`;
it hashes a value larger than the running Node engine's single-string limit.
Output formatters and MCP transport serialization still assemble whole strings.
For a large CLI result, select the needed fields
before formatting, for example `--format json --filter-output
evidence_id,normalized_result.statistics`. Complete serialization of a result
beyond the engine's string limit is not established by this digest check.
Client framing limits also apply: the pinned Node MCP SDK's stdio transport
defaults to a 10 MiB buffer. Its caller-selected `maxBufferSize` must accommodate
the complete response, including text and structured Evidence projections.
Both application CLI routes stream complete JSON and JSONL output in bounded
chunks, including field filters and `--full-output`. They wait for each stdout
write instead of constructing or reparsing a document-sized string. The opt-in
`npm run verify:javascript:output` checks the actual CLI formatting boundary with
a temporary output file larger than the running engine's string limit, verifies
its bytes and digest, and removes it. Use `-- jsonl` for the compact JSONL check.
Each check needs space for one output file plus a 1 GiB free-space reserve.
Other CLI formats and `--token-count` still assemble whole strings.
Field selection remains useful when the caller needs a
smaller view, for example `--format json --filter-output
evidence_id,normalized_result.statistics`. Streaming output does not bound the
memory needed to construct the analysis graph itself.
MCP prepares the complete repeated response incrementally against the pinned
SDK's 10 MiB stdio receive-buffer budget. Oversized results return an actionable
transport constraint and the exact same-session Evidence reference. Use
`trace_application_feature` to inspect a selected module's relationships, or
`export_evidence_bundle` to write the complete canonical bundle without a
document-sized allocation. Same-session analysis reads reuse authenticated
immutable snapshots; foreign inline Evidence is still parsed and authenticated.
See [MCP tool results](mcp-contracts.md#tool-results) for larger client buffers
and `REA_MCP_MAX_RESPONSE_BYTES`. Follow-up results remain complete and can also
exceed the transport budget. Compact result views are tracked separately in
[#1050](https://github.com/morluto/rea/issues/1050).
## What is reconstructed
@@ -77,6 +125,13 @@ SHA-256 digests, inventory IDs, ASAR container identity, and `.asar.unpacked`
status. Direct ASAR inputs and filesystem-backed ASAR files nested beneath a
directory are supported.
JavaScript sources (`.js`, `.jsx`, `.mjs`, and `.cjs`) and TypeScript sources
(`.ts`, `.tsx`, `.mts`, and `.cts`) are parsed as inert text; analysis does not
execute them.
JavaScript and HTML source ranges retain an initial UTF-8 BOM as one UTF-16
code unit, matching the original bytes identified by the artifact digest.
If an ASAR declares an unpacked companion entry but the corresponding
`<archive>.unpacked` file is absent from the operator-supplied artifact set, REA
keeps the ASAR occurrence with `hash_status: unavailable`, records an explicit
@@ -85,6 +140,9 @@ missing bytes. This allows static JavaScript/Electron reconstruction to proceed
for the embedded files while preserving the missing native/resource bytes as an
unknown instead of silently treating them as absent or verified.
Plain `.ts` artifact sources use TypeScript syntax without JSX, including
angle-bracket type assertions. `.tsx` and `.jsx` retain JSX parsing.
Selected bounded text is then parsed as inert data to recover:
- `package.json` metadata and declared main or renderer entrypoints;
@@ -108,6 +166,12 @@ Selected bounded text is then parsed as inert data to recover:
- utility-process entrypoints and native `.node` binding requests without
parsing or executing the add-on.
Overloaded `.open` calls use lexical receiver facts: ambient browser window and
document receivers are treated as browsing-context or document operations,
while locally shadowed receivers can still contribute network endpoint
candidates. A template recovered with a parser error and no cooked value stays
dynamic; its raw spelling is not treated as a valid JavaScript string.
Each recovered bundle module retains the exact factory-source digest. A complete
bounded AST also receives a `babel-ast-v1` structural fingerprint that ignores
ordinary identifier names while retaining syntax, literals, operators, object
@@ -169,6 +233,22 @@ These safeguards are not caller-selectable output budgets. Source-map parsing
reports truncation when its format or parser safety boundary is reached. The
application graph has no aggregate node, edge, root, or observation prefix cap.
Semantic evaluation retains at most 256 distinct primitive candidates per
expression and at most 1 MiB of worst-case JSON string bytes across a normalized
primitive value's string candidates. The byte budget estimates six JSON bytes
per UTF-16 code unit, the expansion bound for escaped strings, and bounds both
retained candidate strings and temporary canonical-key serialization. This is
an in-memory semantic allocation bound independent of CLI or MCP transport
budgets. Source literal bytes remain in the parsed source and artifact evidence;
when a normalized semantic value exceeds the bound, its value is unknown and
coverage is partial. Expression
evaluation and provenance walks stop after 256 nested levels. Union products
and string lengths are checked before allocating combinations or concatenated
strings. When a boundary is reached, the value stays unknown, semantic coverage
becomes partial, and graph evidence retains the expression location and typed
limiting reason. The number of unresolved alternatives is unknown; it is not
reported as an exact omission count.
Byte, entry, path, and graph-shape bounds are hard limits. The parse deadline is
checked before and between bounded parsing and traversal phases; the synchronous
Babel and JSON parser calls cannot be preempted mid-call, so their input byte
@@ -193,3 +273,29 @@ paired, ambiguous, and unpaired IPC, validation candidates, utility processes,
and native binding requests. These fixtures establish parser and artifact-reader
claims; they do not replace the later operator-supplied real-application
benchmark.
URI schemes are classified independently of local file names: a reference such
as `web3:app.js` remains external even when an artifact has that literal name.
Scheme characters may include digits after the initial letter; a relative path
such as `./web3:app.js` still names a local artifact.
Package `exports` fallback arrays are supported both at the top level and under
the root `"."` entry. The resolver selects targets in declared order using the
same conditional and invalid-entry handling as nested exports arrays.
ESM relative module paths and selected package exports targets use URL suffix handling
and one percent-decoding pass. CommonJS relative paths and legacy package main
fields retain literal filename punctuation. Exports targets must start with
`./` and exclude `.`, `..`, and `node_modules` path segments, including their
encoded forms, before URL normalization. Arrays skip invalid exports targets
in declared order; URL decoding and file lookup failures for a selected target
do not select a later entry. Rejected references retain the package metadata
path, original target, failed constraint, and importing source location.
HTML script references resolve to exact inventoried files after applying the
document base and query/fragment rules. CommonJS module lookups retain extension
and directory resolution.
Unresolved HTML references retain their declaration, source range, and resolution
reason in the renderer observations.
HTML script source ranges follow the HTML parser across LF, CRLF, and bare CR
line endings, preserving UTF-16 columns.
+12 -6
View File
@@ -119,12 +119,18 @@ targets belong to the same Electron application.
## Capture and determinism
Every observation records its time window and returns all valid script and
execution-context events received during it. Per-location protocol validation
remains in force. Scripts are validated after capture, deduplicated by stable
metadata, and canonically sorted. Wall-clock timestamps and protocol script IDs
are excluded from the durable result, so identical inputs and captured metadata
produce the same Evidence ID.
Capture retains valid script and execution-context metadata within an 8 MiB
aggregate budget, including conservative estimates for UTF-16 strings, record
and collection overhead, and projected URL fields. Each CDP message also has a
16 MiB transport limit. A message over that limit fails with `payload_limit`;
when the aggregate budget is reached, later metadata events are dropped and
the partial result reports `events_dropped`, `truncated: true`, and
`retained_metadata_budget_exceeded`. Repeated context transitions update the
current context record instead of accumulating duplicate objects. Scripts are
validated after capture, deduplicated by stable metadata, and canonically
sorted. Wall-clock timestamps and protocol script IDs are excluded from the
durable result, so identical inputs and captured metadata produce the same
Evidence ID.
Inspector attach is inherently incomplete. Enabling `Debugger` reports known
and uncollected scripts, but scripts collected before attachment may be
+8 -2
View File
@@ -25,7 +25,7 @@ part of the current tool set.
This guide describes the implementation and verification of
[ADR-0003](adr/0003-managed-code-evidence-and-provider-boundary.md). The canonical
tool inventory is [`product-catalog.json`](https://github.com/morluto/rea/blob/main/docs/product-catalog.json).
tool inventory is the [build-generated catalog](mcp-contracts.md#generated-catalog).
## Shipped scope
@@ -125,7 +125,7 @@ The upstream analyzer currently gates its own analysis to x86-64. NativeAOT's
broader platform matrix does not establish REA coverage: PE/COFF, ELF, Mach-O,
shared libraries, target architectures, and runtime metadata versions each need
their own provider verification. REA's experimental Windows Ghidra boundary is
limited to admitted native x86-64 PE applications, so it does not currently
limited to admitted native x86 and x86-64 PE applications, so it does not currently
establish coverage for PE DLLs. Mobile NativeAOT targets should remain
experimental until REA has a matching provider and package-level verification.
@@ -137,6 +137,12 @@ function implements a specific managed method.
## Evidence record shape
Member comparison, application graph projection, and managed/native verification
authenticate supplied inspection Evidence and check that any subject SHA-256
matches the normalized artifact SHA-256. A valid Evidence ID alone does not
establish that those two identities agree. Evidence without a subject remains
usable, with no subject digest available to cross-check.
Every planned operation returns a provider result and Evidence with four
commitment groups:
+64
View File
@@ -1,5 +1,15 @@
# MCP runtime contracts
## Generated catalog
Run `npm run build:cached` in a source checkout to generate the machine-readable
catalog at `docs/public/product-catalog.json`. Documentation deployments serve
the same file at [/rea/product-catalog.json](/rea/product-catalog.json). PR CI retains it with the packaged
skill and portable conformance projections in the `generated-docs` artifact.
These outputs describe the exact source revision being built; they are not
checked-in snapshots. For a running server, `binary_session` remains the
authoritative source of catalog identity and tool availability.
## Identity and discovery
`binary_session` reports the active package, server, SDK, and negotiated
@@ -13,6 +23,24 @@ are currently unavailable. Opening or closing a target or observing a provider
health transition leaves that catalog unchanged and does not emit
`notifications/tools/list_changed`.
Advertised input and output schemas contain no reachable recursive references.
The input compatibility profile limits nesting to ten object, array, and
`anyOf`/`oneOf`/`allOf` levels, following local references per path. Property
maps, reference definitions, and example data do not add schema levels. Tests
check the complete catalog after SDK conversion and its generated counterpart.
This is REA's local compatibility profile; individual model APIs can impose
additional limits.
For passive `compare_web_captures` inputs, pass each complete
`inspect_web_page` result in `before.inspection` or `after.inspection`, with an
optional complete `discover_webmcp_tools` result in the matching `webmcp` field.
The input schema describes these producer-result objects as round-trip payloads.
For scenario comparisons, pass complete `capture_browser_scenario` results in
`before_scenario` and `after_scenario`. REA validates all nested fields with the
original capture schemas; observation output schemas remain complete. Scenario
authoring inputs and comparison normalization options retain their full
advertised structure.
Call `binary_session` with `{}` and read `result.tool_availability` to choose a
callable operation for the current target, provider, host, and negotiated client
capabilities. The default result includes the complete inventory with each
@@ -50,9 +78,25 @@ Provider calls receive the request cancellation signal. Artifact traversal,
hashing, version comparisons, Hopper requests, and process capture
check the same signal. Cancellation is distinct from timeout. A cleanup failure
uses `cleanup_incomplete` and lists only the owned resource kinds that remain.
Native call tracing and process capture retain available observations in
`details.partial_observation` on failure, including when cleanup succeeds.
The observation reports its partial coverage; cleanup details describe host
state separately from the execution failure.
Derived comparisons and reconstruction verification yield before computation
and before publication, so cancellation cannot race with successful Evidence.
`analyze_javascript_application` also yields between reconstruction phases and
during graph/result sealing, cross-graph binding checks, Evidence JSON validation,
and canonical hashing. Its final result validation reuses exact graphs whose
owned constructors validated and completely sealed them; imported graphs still
receive full schema and commitment checks.
Cancellation observed before completion returns `cancelled` and prevents the
provisional result from entering the session ledger; prior Evidence stays usable.
Single-file parsing, graph construction, and validation of imported graphs
still run synchronously, so control messages can wait for those
phases to release the event loop. A rejected client promise alone does not
establish that the server has stopped its work.
CLI calls work without a progress token and translate SIGINT into the same
AbortSignal used by providers. Existing controlled-process cleanup and provider
shutdown rules still apply; REA never kills a process it cannot prove it owns.
@@ -136,6 +180,26 @@ directly to `compare_functions`, and `inspect_artifact` Evidence to
`compare_artifacts`. Use `get_evidence_bundle` when the task needs broader
retained session history or an explicit bundle for transfer.
REA prepares complete MCP results within the pinned stdio client's 10 MiB
receive-buffer budget, including both text and structured representations and
room for the JSON-RPC envelope. If a result cannot fit, REA returns
`resource_constraint` with `details.resource: "transport"` before constructing
a document-sized string. Analysis Evidence remains complete in the current
session. Its exact reference is reported in
`details.reported_limits.evidence_reference`; use it with a focused application
workflow, or call `export_evidence_bundle` with a destination path. Complete
bundle exports stream canonical JSON into an atomically published file. A broad
follow-up or `get_evidence_bundle` can also exceed the response budget; exporting
preserves the complete session without sending it through a single MCP frame.
Clients that explicitly configure a larger receive buffer can set the REA
server's `REA_MCP_MAX_RESPONSE_BYTES` environment variable to the same byte
count. This setting must be a safe decimal integer at least 10485760; REA
reserves 1024 bytes for the envelope. Raising it restores complete inline
delivery for responses that fit that buffer and Node's single-string limit.
It does not change the client's buffer, analysis coverage, or retained content.
Ordinary responses keep their existing complete result contract.
## Retained application Evidence inputs
`trace_application_feature`, `trace_javascript_semantics`,
+160 -1
View File
@@ -12,6 +12,19 @@ input file and include the observed lipo slice offset for universal binaries.
If that offset is unavailable, segment evidence locations are omitted with an
explicit limitation; architecture inventory locations remain available.
Native Xcode command execution uses shared process supervision with a 60-second
deadline and a 64 MiB aggregate stdout/stderr budget. Timeout, cancellation,
stream failure, and output exhaustion retain the captured output, exit details,
and cleanup outcome in the error result. Truncated output is never parsed as a
complete observation.
- `trace_dylib_resolution` / `rea trace-dylib-resolution <app-or-mach-o>`
parses Mach-O load commands in TypeScript and follows dyld's path expansion
for every executable in an app bundle, or for one Mach-O within its directory.
It reports each `@rpath`, `@loader_path` and `@executable_path` candidate with
its outcome. Paths outside the analyzed root, including shared-cache system
libraries, stay undetermined. See
[Apple application analysis](apple-application-analysis.md#dylib-load-resolution).
- `inspect_asset_catalog` / `rea inspect-asset-catalog <app>` reads compiled
`Assets.car` metadata through macOS `assetutil --info`. Catalog digests, raw
rendition fields, pagination and exact UI resource-name matches are returned.
@@ -30,6 +43,21 @@ explicit limitation; architecture inventory locations remain available.
connections and controller/class names. `verify:interface-builder` compiles
a source-owned AppKit XIB with `ibtool`. Storyboards require an installed
iOS platform; unsupported archive forms remain explicit.
XML plist archives accept UTF-8 and BOM-marked UTF-16 in either byte order.
The shared keyed-archive and Interface Builder decoder consumes an initial
BOM and rejects malformed byte sequences, unsupported encoding declarations,
and declarations that disagree with the detected encoding. It never substitutes
UTF-8 for a declared encoding it cannot process. Archive and evidence digests
still identify the original serialized bytes. Interface Builder decoding
limits aggregate input archives to 32 MiB.
Before decoding, it also bounds the serialized structure: expanded binary
plist references, NIB table records, XML elements and text, plus their
projected representations must fit the 256 MiB aggregate decode budget.
This is a format-derived representation estimate, not a process-wide heap
ceiling.
Archives beyond either aggregate budget are omitted with their paths and an
explicitly partial `archive_decode` facet; previously decoded documents and
their identities remain available.
- `inspect_native_dispatch_metadata` /
`rea inspect-native-dispatch-metadata <app-or-binary>` prefers a validated macOS Mach-O byte reader. It decodes
64-bit little-endian Objective-C class/metaclass records, superclass pointers,
@@ -59,6 +87,54 @@ explicit limitation; architecture inventory locations remain available.
Ghidra supplies three exact-object operations:
Ghidra accepts hexadecimal addresses with or without a `0x` prefix, in either
letter case, and explicit address-space coordinates such as `EXTERNAL:0x1`.
Results use canonical lowercase hexadecimal offsets. For a procedure identifier,
an explicit `0x` or address-space prefix selects an address; otherwise an exact
database symbol name takes precedence over a bare hexadecimal address. Thus a
function renamed to `dead` remains selectable by name. Use the names returned
by the inventory, including their namespaces and any platform symbol prefix.
Overloads can share a fully qualified name; ambiguity errors return every
matching entry address so the caller can select the intended function directly.
`resolve_containing_procedure` also resolves an exact external entry. Its empty
body remains explicit; nearby external addresses do not inherit that identity.
Ghidra function references cover the complete function-body AddressSet and its
exact entry, including references into instruction interiors and references
from embedded data. Addresses inside an enclosing span but outside the owned
body are excluded. `procedure_references` returns internal and external edges;
`analyze_function` omits incoming edges whose sources belong to the same function.
The reference collector's semantics participate in the analysis profile, so
older snapshots cannot replay the former incomplete results under this profile.
Ghidra regex searches use Java Pattern semantics. If compiling or matching a
pattern exhausts the engine's stack, REA returns a resource constraint and
preserves the active database and annotations. Retry in literal mode or simplify
the regex; successful literal searches retain complete matching strings.
Cancelling an active request terminates the ephemeral database and discards its
annotations. REA retains the selected target and imports the original artifact
again on the next Ghidra query.
Opening the same target and profile again retains the live database, including
annotations. It does not make those edits eligible for an immutable snapshot.
Snapshot closes drain earlier provider requests before saving and keep later
requests waiting until the save and close finish. A concurrent successful edit
therefore rejects the snapshot save and leaves the edited session open.
After metadata edits, snapshot saves and imports remain unavailable in that
session. Use `export_evidence_bundle` to retain the observations, close without
`snapshot_path`, and reopen the target before importing or saving a snapshot.
Ghidra checks its private import copy against the artifact digest selected by
`open_binary`. If the source changes before that copy is acquired, the query
reports `artifact_changed` with the selected path and both digests. Reopen the
stable target to acquire its current identity. An already imported database
continues to describe its captured bytes even if the original path is changed
or removed; this identity failure does not mark the Ghidra installation unavailable.
On Linux and macOS, an unreadable source reports `access_denied`; a removed or
nonregular replacement reports `artifact_changed`. Source copying is cancellable
and never replaces an existing private snapshot.
```bash
rea inspect-native-instruction <binary> <address> --provider ghidra
rea resolve-native-call-targets <binary> <call-site> --provider ghidra
@@ -159,6 +235,73 @@ owned helper per scenario, removes its temporary compiler cache and stops its
helper on cancellation. It does not launch or own the selected application. UI
actions may change application data or trigger network activity.
## Native call observation
`observe_native_calls` / `rea observe-native-calls <app-or-mach-o> <input-json>`
launches the active Mach-O as a new, owned process under LLDB. It stops at
caller-selected entries, records them and continues:
- functions by exact symbol name, optionally limited to one image;
- Objective-C methods by selector, optionally limited to one class and to
instance or class methods.
```sh
rea observe-native-calls /Applications/Example.app '{"breakpoints":[{"kind":"objc-method","class_name":"NSURLSession","selector":"dataTaskWithRequest:completionHandler:"},{"kind":"function","name":"open","module":"libsystem_kernel.dylib"}],"duration_ms":5000,"backtrace_frames":4}' --json
```
Each event is an observation: thread, image, symbol, load and file address,
the raw integer argument registers, optional caller frames and, for an
Objective-C method, the selector read from `_cmd` and the receiver's dynamic
class. Breakpoints stop at the symbol itself, so the registers hold the ABI
arguments. The receiver class comes from LLDB's Objective-C runtime reader
without running target code; no expression is evaluated, and REA never
attaches to a process it did not launch.
The run ends when the process exits, `max_events` entries are recorded,
`duration_ms` elapses, or the aggregate trace resource budget is exhausted.
Bounded termination reports partial coverage. REA checks process termination
using a launch-time OS start identity; if ownership or cleanup cannot be
verified, the result or error preserves that uncertainty. The result reports:
- the outcome, exit status and captured stdout/stderr (1 MiB kept per stream
while excess bytes are drained and counted without growing capture files).
Each stream reports `complete`; when draining could not finish, its byte
count is an observed lower bound and truncation is explicit;
- caller-selected arguments, working directory, and environment overrides;
- `selected_file_sha256` and LLDB module identity observations. These checks do
not pin the executable against concurrent mutation; `loaded_image_sha256`
remains unknown;
- signal or exception stops;
- every breakpoint's resolved locations; a breakpoint that matched no loaded
code makes coverage partial;
- limitations.
The bridge limits retained event JSON to 8 MiB and caller frames to 65,536
across the run. It checks admission before retaining another complete event;
`resource-limit` reports which observations could not be completed without
silently changing the requested event or frame settings.
Resolved breakpoint locations have a separate aggregate 8 MiB metadata budget.
If their details exceed it, `location_count` retains the full observed match
count, coverage is partial, and `breakpoint_locations_truncated` is true. This
post-run metadata limit does not imply that the target was killed.
Accepted entries and signal stops are also flushed to a bounded 8 MiB journal.
If cancellation, timeout, capture failure, or a later bridge error prevents a
full result, the error retains this evidence in `details.partial_observation`
with partial coverage. Available output prefixes remain inline; their byte
counts are observed lower bounds when final drain counters are unavailable.
The original failure classification and cleanup uncertainty remain separate.
An interrupted journal row is ignored with an explicit limitation.
Only entries are observed: return values, floating-point and stack arguments,
and inlined or `objc_direct` calls are not. The target runs with the current
user's permissions and may change files, show UI or use the network.
Debugging must be allowed for the user (Developer Tools access). A
hardened-runtime target without `com.apple.security.get-task-allow` cannot be
debugged; `inspect_signature` reports this as the debugger-attach facet, and
the tool fails with a `debugger-attach-denied` reason.
## Provider and verification boundaries
Install a Ghidra 12.1.x release and the 64-bit full JDK it declares, then configure REA to
@@ -169,8 +312,17 @@ protected runtime DACLs, and handle-based path admission. See [Windows Ghidra P0
[issue #527](https://github.com/morluto/rea/issues/527).
On Linux and macOS, `annotate_native_function` atomically edits a function name
and entry comments in the ephemeral database, returning refreshed analysis
without changing executable bytes. Windows P0 remains read-only. Ghidra has no
without changing executable bytes. Annotation text must contain no NUL or
unpaired Unicode surrogate; a rejection identifies the field and UTF-16 index
and leaves every annotation unchanged. CRLF, supplementary Unicode characters,
and combining characters are preserved. Windows P0 remains read-only. Ghidra has no
GUI authority, and REA never falls back automatically to Hopper.
Ghidra name edits preserve the existing namespace. Supply either a leaf name
such as `renamed` or a fully qualified name in that namespace, such as
`alpha::renamed`. The returned qualified name can be reused as an idempotent
rename input. Edits retain the existing namespace; other namespace-like text
remains literal leaf-name text. A qualified name with an empty leaf is rejected
before any comment or name is changed.
- `npm run verify:ghidra`: host-native debug/stripped targets, native type layout,
instruction/call facts, value dependencies and process/project cleanup.
@@ -190,3 +342,10 @@ GUI authority, and REA never falls back automatically to Hopper.
macOS ARM64 is the real host verified during this implementation. Admission of
macOS Intel does not claim an Intel verification run. Unsupported metadata and
unresolved runtime/value semantics remain visible in results.
## Interface Builder hierarchy coverage
A keyed-archive hierarchy UID without an object-table entry marks the hierarchy
as partial while preserving decoded objects and known links. UID 0 remains
archived nil and does not count as a missing reference. Archive and evidence
digests identify the original serialized bytes.
+29 -2
View File
@@ -30,6 +30,14 @@ different recovery: reinstall REA for the active platform, architecture, and
Node.js version with optional dependencies enabled. Capability diagnostics
distinguish this from the Windows capture-adapter limitation.
macOS also requires Apple's Swift compiler through `xcrun` for process
ownership inspection. REA compiles its packaged, narrow process-inspection
helper into a private temporary directory before a capture or owned provider
process starts; the same prerequisite applies to owned provider-process
supervision on macOS. REA removes that directory when it exits. REA does not
install Xcode, Command Line Tools, or other software. Capability checks prepare
this helper before reporting macOS capture as available.
## Capture a command
Write a JSON scenario and pass its path to the CLI:
@@ -94,6 +102,20 @@ run the same owned-process cleanup path. Settlement reports whether the
sampled process group quiesced or whether cleanup was needed or unverifiable;
sampling cannot prove that every short-lived or detached descendant was seen.
When the host withholds an unrelated process’s ownership token, REA leaves that
process untouched and records its PID and reason in `cleanup.unverified_processes`
and process residual unknowns. Successful cleanup verifies the owned group;
it does not attribute those unrelated processes. Related or otherwise unexplained
unreadable processes still prevent successful cleanup.
On macOS, changing a Node process's `process.title` can make its run token
unreadable. Node documents that [setting the title overwrites argv memory](https://nodejs.org/download/release/v24.18.0/docs/api/process.html#processtitle).
With the pinned toolchain, a live child retained a readable start identity
while its token became unavailable after this change; npm changes its title
as well. A newly started unreadable process can prevent verified cleanup even
after the selected command exits. REA preserves this uncertainty and leaves
that process untouched.
## Compare two captures
Compare saved capture Evidence with:
@@ -116,7 +138,12 @@ causal evidence. Trace comparison returns `unknown` when a capture lacks the
required complete event journal or contains relevant residual unknowns.
Each capture carries commitments for the selected scenario, executable,
comparison contract, and normalization rules. Comparison rejects captures
whose comparison contracts differ. Captures are local Evidence files; keep
comparison contract, and normalization rules. The comparison contract covers
the working directory, explicit environment, filesystem observation paths,
terminal size, scripted events, timeouts, limits, and normalization; the
executable and its arguments may differ. Comparison rejects captures whose
comparison contracts differ and names the differing fields, so run both
scenarios from the same absolute working directory and observation paths.
Captures are local Evidence files; keep
their source artifacts and invocation context available when interpreting a
difference.
-684
View File
@@ -1,684 +0,0 @@
{
"package": {
"name": "rea-agents",
"version": "5.0.0",
"sdk": {
"server": "2.3.1",
"client": "2.3.1",
"core": "2.3.1"
},
"skill_version": "27"
},
"tools": {
"total": 133,
"families": [
{
"id": "direct",
"surface": "official-proxy",
"count": 41,
"tools": [
"address_name",
"address_to_file_offset",
"annotate_native_function",
"comment",
"current_address",
"current_document",
"current_procedure",
"goto_address",
"inline_comment",
"inspect_native_data_type",
"inspect_native_instruction",
"inspect_native_load_image",
"list_bookmarks",
"list_documents",
"list_names",
"list_procedures",
"list_segments",
"list_strings",
"next_address",
"prev_address",
"procedure_address",
"procedure_assembly",
"procedure_callees",
"procedure_callers",
"procedure_info",
"procedure_pseudo_code",
"procedure_references",
"read_bytes",
"read_function_instructions",
"resolve_containing_procedure",
"resolve_native_call_targets",
"search_procedures",
"search_strings",
"set_address_name",
"set_addresses_names",
"set_bookmark",
"set_comment",
"set_current_document",
"set_inline_comment",
"unset_bookmark",
"xrefs"
]
},
{
"id": "enhanced",
"surface": "enhanced",
"count": 14,
"tools": [
"analyze_function",
"analyze_swift_types",
"batch_decompile",
"binary_overview",
"find_xrefs_to_name",
"get_call_graph",
"get_objc_classes",
"get_objc_protocols",
"inspect_native_api",
"inspect_native_dispatch_metadata",
"trace_call_path",
"trace_feature",
"trace_native_ui_action",
"trace_native_values"
]
},
{
"id": "native",
"surface": "native-provider",
"count": 7,
"tools": [
"capture_native_ui_scenario",
"demangle_swift",
"inspect_macho",
"inspect_plist",
"inspect_signature",
"list_architectures",
"observe_native_ui"
]
},
{
"id": "artifact",
"surface": "artifact-provider",
"count": 5,
"tools": [
"decode_interface_builder",
"extract_artifact",
"inspect_artifact",
"inspect_asset_catalog",
"inspect_keyed_archive"
]
},
{
"id": "managed",
"surface": "managed",
"count": 7,
"tools": [
"compare_managed_members",
"import_managed_reconstruction",
"inspect_managed_artifact",
"inspect_managed_members",
"inspect_managed_native_boundaries",
"project_managed_application_graph",
"verify_managed_native_boundaries"
]
},
{
"id": "firmware",
"surface": "firmware-provider",
"count": 2,
"tools": ["extract_firmware", "inspect_firmware_regions"]
},
{
"id": "android",
"surface": "android-provider",
"count": 5,
"tools": [
"inspect_android_class",
"inspect_android_method",
"inspect_android_package",
"search_android_classes",
"trace_android_references"
]
},
{
"id": "browser",
"surface": "browser-provider",
"count": 11,
"tools": [
"analyze_web_bundle",
"capture_browser_scenario",
"capture_web_screenshot",
"compare_web_captures",
"compare_web_screenshots",
"discover_webmcp_tools",
"inspect_web_event_listeners",
"inspect_web_page",
"list_browser_targets",
"observe_web_execution",
"observe_web_session"
]
},
{
"id": "electron",
"surface": "electron-provider",
"count": 5,
"tools": [
"analyze_javascript_application",
"capture_electron_scenario",
"inspect_electron_page",
"list_electron_targets",
"reconcile_javascript_runtime"
]
},
{
"id": "javascript-runtime",
"surface": "runtime-provider",
"count": 2,
"tools": [
"list_javascript_runtime_targets",
"observe_javascript_runtime"
]
},
{
"id": "application",
"surface": "application-workflow",
"count": 13,
"tools": [
"build_reconstruction_obligation_ledger",
"compare_application_versions",
"compare_javascript_export_shapes",
"compare_source_to_bundle",
"evaluate_reconstruction_coverage",
"export_web_scripts",
"project_android_application_graph",
"project_apple_application_graph",
"recover_javascript_sources",
"trace_application_feature",
"trace_javascript_semantics",
"trace_web_module_imports",
"trace_web_source_location"
]
},
{
"id": "session",
"surface": "session",
"count": 21,
"tools": [
"binary_session",
"build_call_path",
"capture_process_scenario",
"close_binary",
"compare_artifacts",
"compare_bundles",
"compare_functions",
"compare_process_captures",
"correlate_static_and_runtime",
"export_evidence_bundle",
"find_changed_behavior",
"get_evidence_bundle",
"get_navigation_context",
"import_evidence_bundle",
"inspect_address_context",
"list_unknowns",
"open_binary",
"record_unknown",
"update_unknown",
"verify_reconstruction",
"verify_unknown_resolution"
]
}
]
},
"providers": [
{
"id": "binwalk",
"name": "Binwalk",
"version": "3.1.0",
"capabilities": ["inspect_firmware_regions"]
},
{
"id": "ghidra",
"name": "Ghidra",
"version": null,
"capabilities": [
"address_name",
"address_to_file_offset",
"analyze_function",
"annotate_native_function",
"inspect_native_data_type",
"inspect_native_instruction",
"inspect_native_load_image",
"list_documents",
"list_names",
"list_procedures",
"list_segments",
"list_strings",
"procedure_address",
"procedure_assembly",
"procedure_callees",
"procedure_callers",
"procedure_info",
"procedure_pseudo_code",
"procedure_references",
"read_bytes",
"read_function_instructions",
"resolve_containing_procedure",
"resolve_native_call_targets",
"search_procedures",
"search_strings",
"xrefs"
]
},
{
"id": "hopper",
"name": "Hopper Disassembler",
"version": null,
"capabilities": [
"address_name",
"address_to_file_offset",
"analyze_function",
"comment",
"current_address",
"current_document",
"current_procedure",
"goto_address",
"inline_comment",
"list_bookmarks",
"list_documents",
"list_names",
"list_procedures",
"list_segments",
"list_strings",
"next_address",
"prev_address",
"procedure_address",
"procedure_assembly",
"procedure_callees",
"procedure_callers",
"procedure_info",
"procedure_pseudo_code",
"procedure_references",
"read_bytes",
"read_function_instructions",
"resolve_containing_procedure",
"search_procedures",
"search_strings",
"set_address_name",
"set_addresses_names",
"set_bookmark",
"set_comment",
"set_current_document",
"set_inline_comment",
"unset_bookmark",
"xrefs"
]
},
{
"id": "ida",
"name": "IDA Pro MCP adapter",
"version": "1",
"capabilities": [
"analyze_function",
"list_procedures",
"list_strings",
"procedure_address",
"procedure_assembly",
"procedure_callees",
"procedure_callers",
"procedure_pseudo_code",
"read_function_instructions",
"search_procedures",
"search_strings",
"xrefs"
]
},
{
"id": "jadx",
"name": "JADX headless",
"version": "0.7.1",
"capabilities": [
"inspect_android_class",
"inspect_android_method",
"inspect_android_package",
"search_android_classes",
"trace_android_references"
]
},
{
"id": "native-macos",
"name": "macOS native inspection utilities",
"version": null,
"capabilities": [
"capture_native_ui_scenario",
"demangle_swift",
"inspect_macho",
"inspect_plist",
"inspect_signature",
"list_architectures",
"observe_native_ui"
]
},
{
"id": "rea-android-application",
"name": "REA Android application workflows",
"version": "1",
"capabilities": ["project_android_application_graph"]
},
{
"id": "rea-apple-application",
"name": "REA Apple application workflows",
"version": "1",
"capabilities": ["project_apple_application_graph"]
},
{
"id": "rea-artifact-graph",
"name": "REA safe artifact graph provider",
"version": "1",
"capabilities": [
"decode_interface_builder",
"extract_artifact",
"inspect_artifact",
"inspect_asset_catalog",
"inspect_keyed_archive"
]
},
{
"id": "rea-cdp-browser",
"name": "REA Chrome DevTools Protocol observation provider",
"version": "2",
"capabilities": [
"analyze_web_bundle",
"capture_web_screenshot",
"compare_web_captures",
"compare_web_screenshots",
"discover_webmcp_tools",
"inspect_web_event_listeners",
"inspect_web_page",
"list_browser_targets",
"observe_web_execution",
"observe_web_session"
]
},
{
"id": "rea-cdp-electron",
"name": "REA Electron file-page CDP observation provider",
"version": "1",
"capabilities": ["inspect_electron_page", "list_electron_targets"]
},
{
"id": "rea-dotnet-static",
"name": "REA managed static analysis provider",
"version": "1",
"capabilities": [
"inspect_managed_artifact",
"inspect_managed_members",
"inspect_managed_native_boundaries"
]
},
{
"id": "rea-dotnet-workflows",
"name": "REA managed-code workflows",
"version": "1",
"capabilities": [
"compare_managed_members",
"import_managed_reconstruction",
"project_managed_application_graph",
"verify_managed_native_boundaries"
]
},
{
"id": "rea-javascript-application",
"name": "REA JavaScript application analyzer",
"version": "1",
"capabilities": ["analyze_javascript_application"]
},
{
"id": "rea-javascript-application-workflows",
"name": "REA JavaScript application workflows",
"version": "1",
"capabilities": [
"build_reconstruction_obligation_ledger",
"compare_application_versions",
"compare_javascript_export_shapes",
"compare_source_to_bundle",
"evaluate_reconstruction_coverage",
"trace_application_feature",
"trace_javascript_semantics"
]
},
{
"id": "rea-javascript-runtime-reconciliation",
"name": "REA JavaScript runtime reconciliation",
"version": "1",
"capabilities": ["reconcile_javascript_runtime"]
},
{
"id": "rea-playwright-browser-scenario",
"name": "REA Playwright browser scenario capture provider",
"version": "1",
"capabilities": ["capture_browser_scenario"]
},
{
"id": "rea-playwright-electron-active",
"name": "REA Playwright active Electron observation provider",
"version": "1",
"capabilities": ["capture_electron_scenario"]
},
{
"id": "rea-v8-inspector",
"name": "REA passive Node/Electron V8 Inspector provider",
"version": "1",
"capabilities": [
"list_javascript_runtime_targets",
"observe_javascript_runtime"
]
},
{
"id": "rea-web-script-export",
"name": "REA captured script export",
"version": "1",
"capabilities": [
"export_web_scripts",
"trace_web_module_imports",
"trace_web_source_location"
]
},
{
"id": "unblob",
"name": "Unblob",
"version": "26.6.4",
"capabilities": ["extract_firmware"]
},
{
"id": "wakaru",
"name": "Wakaru",
"version": "1.13.0",
"capabilities": ["recover_javascript_sources"]
}
],
"setup_clients": [
{
"id": "claude_code",
"display_name": "Claude Code",
"format": "json",
"configuration": "managed"
},
{
"id": "claude_desktop",
"display_name": "Claude Desktop",
"format": "json",
"configuration": "managed"
},
{
"id": "codex",
"display_name": "Codex",
"format": "toml",
"configuration": "managed"
},
{
"id": "cursor",
"display_name": "Cursor",
"format": "json",
"configuration": "managed"
},
{
"id": "gemini_cli",
"display_name": "Gemini CLI",
"format": "json",
"configuration": "managed"
},
{
"id": "windsurf",
"display_name": "Windsurf",
"format": "json",
"configuration": "managed"
},
{
"id": "devin",
"display_name": "Devin",
"format": "json",
"configuration": "managed"
},
{
"id": "opencode",
"display_name": "OpenCode",
"format": "opencode",
"configuration": "managed"
},
{
"id": "antigravity",
"display_name": "Antigravity",
"format": "json",
"configuration": "managed"
},
{
"id": "copilot_cli",
"display_name": "GitHub Copilot CLI",
"format": "copilot_cli",
"configuration": "managed"
},
{
"id": "commandcode",
"display_name": "Command Code",
"format": "commandcode",
"configuration": "managed"
},
{
"id": "vscode",
"display_name": "VS Code",
"format": "vscode",
"configuration": "managed"
}
],
"cli": {
"primary_count": 88,
"commands": [
"address-to-file-offset",
"analyze",
"analyze-javascript-application",
"analyze-web-bundle",
"annotate-native-function",
"build-reconstruction-obligation-ledger",
"capabilities",
"capture-browser-scenario",
"capture-electron-scenario",
"capture-native-ui-scenario",
"capture-process",
"capture-web-screenshot",
"compare",
"compare-application-versions",
"compare-javascript-export-shapes",
"compare-managed-members",
"compare-process-captures",
"compare-source-to-bundle",
"compare-web-captures",
"compare-web-screenshots",
"decode-interface-builder",
"decompile",
"demangle-swift",
"discover-webmcp-tools",
"doctor",
"evaluate-reconstruction-coverage",
"evidence-export",
"evidence-import",
"export-web-scripts",
"extract-artifact",
"extract-firmware",
"function",
"import-managed-reconstruction",
"import-reference-source",
"inspect",
"inspect-android-class",
"inspect-android-method",
"inspect-android-package",
"inspect-artifact",
"inspect-asset-catalog",
"inspect-electron-page",
"inspect-firmware-regions",
"inspect-keyed-archive",
"inspect-macho",
"inspect-managed-artifact",
"inspect-managed-members",
"inspect-managed-native-boundaries",
"inspect-native-api",
"inspect-native-data-type",
"inspect-native-dispatch-metadata",
"inspect-native-instruction",
"inspect-native-load-image",
"inspect-plist",
"inspect-signature",
"inspect-web-event-listeners",
"inspect-web-page",
"instructions",
"list-architectures",
"list-browser-targets",
"list-electron-targets",
"list-javascript-runtime-targets",
"observe-javascript-runtime",
"observe-native-ui",
"observe-web-execution",
"observe-web-session",
"project-android-application-graph",
"project-apple-application-graph",
"project-managed-application-graph",
"providers",
"read-bytes",
"reconcile-javascript-runtime",
"recover-javascript-sources",
"resolve-native-call-targets",
"search",
"search-android-classes",
"setup",
"trace",
"trace-android-references",
"trace-application-feature",
"trace-javascript-semantics",
"trace-native-ui-action",
"trace-native-values",
"trace-web-module-imports",
"trace-web-source-location",
"uninstall",
"update",
"verify-managed-native-boundaries",
"xrefs"
],
"aliases": [
{
"name": "compare-bundles",
"target": "compare"
}
]
},
"runtime_catalog": {
"counts": {
"cli_commands": 88,
"mcp_tools": 133,
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "af7fe81ec689c2a77f04579b6f4beb4abe86aa71d49c4844d8e0f4d7ed7a3a1e",
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
"combined_sha256": "83b4c3445ed0ec003c0aeefa3188f123d9ae319484f8b1e2b611d2fe278c9c72",
"providers_sha256": "aea15f517c822d80918ccaba57a4fa06557f484cf27b8acbaed4e1a660fac896"
}
}
}
+11 -11
View File
@@ -18,7 +18,7 @@ comments atomically, verifies readback, and returns refreshed function analysis.
Changes live only in the session database and leave executable bytes unchanged.
Ghidra does not expose GUI controls; Windows P0 remains read-only.
Windows x64 P0 supports native x86-64 PE applications on local NTFS with
Windows x64 P0 supports native x86 and x86-64 PE applications on local NTFS with
bundled Job Object ownership, protected runtime DACLs, and handle-based path
admission. Real ordinary-user verification covers all 25 read-only operations
through the packaged CLI and MCP, target integrity, and cleanup. This remains
@@ -49,8 +49,8 @@ follow.
- Report GUI cursor/navigation and persistent mutation operations as unavailable
until their semantics and project ownership are explicitly designed.
- Verify real claims on Linux with at least two distinct source-owned binaries.
Verify the Windows host claim with the deterministic source-owned x86-64 PE
fixture and all admitted operations; compare normalized semantic facts rather
Verify the Windows host claim with the deterministic source-owned x86 and x86-64 PE
fixtures and all admitted operations; compare normalized semantic facts rather
than provider-specific pseudocode text.
## Shipped foundation boundary
@@ -62,7 +62,7 @@ maximum, when the release leaves those at the 12.1 defaults). Otherwise doctor
probes `java`/`javac` or `java.exe`/`javac.exe` from `PATH`. Supported Linux and macOS
hosts accept compatible ELF, PE, and Mach-O executable targets. Host admission
and target compatibility are separate checks. Windows x64 P0 admits only native,
non-managed, non-DLL x86-64 PE applications on fixed local NTFS, with the packaged
non-managed, non-DLL x86 and x86-64 PE applications on fixed local NTFS, with the packaged
native authority available before launch.
The launcher creates one ephemeral runtime root with project,
@@ -136,7 +136,7 @@ Mach-O target coverage. It requires `clang`, LLD, and `lld-link`; set
these tools before compilation. Keeping this matrix separate lets Linux
host/provider acceptance run with only the host compiler.
`npm run verify:ghidra:windows` checks the source-owned native x86-64 PE
`npm run verify:ghidra:windows` and its `-- --x86` variant check the source-owned native x86 and x86-64 PE
fixture, all 25 read-only operations, target/snapshot/import SHA-256 linkage,
and project, endpoint, process, and runtime cleanup. The independent native
lane checks DACLs, handle admission, cancellation, and Job Object lifecycle.
@@ -165,12 +165,12 @@ only the generic process mechanisms.
## Shortlist
| Provider | License / automation surface | What it brings | REA fit and blockers |
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Static analysis, multiple processors and formats, scripting, and project/database workflows | 25 read-only operations shipped on Linux/macOS with installation checks, temporary projects, a serial API queue, and real ELF/PE/Mach-O conformance. Experimental Windows x64 P0 supports native x86-64 PE on local NTFS with bundled native controls and ordinary-user CLI/MCP verification; Linux/macOS additionally support atomic session function annotations; Windows mutation and all Ghidra GUI operations are unavailable. |
| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. |
| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. |
| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. |
| Provider | License / automation surface | What it brings | REA fit and blockers |
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Static analysis, multiple processors and formats, scripting, and project/database workflows | 25 read-only operations shipped on Linux/macOS with installation checks, temporary projects, a serial API queue, and real ELF/PE/Mach-O conformance. Experimental Windows x64 P0 supports native x86 and x86-64 PE on local NTFS with bundled native controls and ordinary-user CLI/MCP verification; Linux/macOS additionally support atomic session function annotations; Windows mutation and all Ghidra GUI operations are unavailable. |
| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. |
| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. |
| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. |
## Recommended order
+88
View File
@@ -0,0 +1,88 @@
# Recorded crash evidence
`inspect_recorded_crash` / `rea inspect-recorded-crash` inspect a caller-selected
Linux ELF64 x86-64 little-endian core independently of an active binary target.
```sh
export REA_PWNTOOLS_PYTHON=/absolute/existing/venv/bin/python
rea inspect-recorded-crash ./crash.core --json
```
The selected Python needs unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2.
REA does not install these dependencies. ELFFile/its original ELF structures and
pwntools' original Linux amd64 ctypes provide parsing and ABI layout. The adapter
adds bounds, representation preservation and lifecycle ownership.
The initial Linux note profile accepts System V/unspecified and GNU/Linux OSABI
with ABI version zero. Other declared ABIs return `unsupported_target` before
note interpretation; an unspecified OSABI alone does not prove OS origin.
## Recorded observations
- Artifact path, original SHA-256 and size; full normalized result and Evidence
are inline through CLI and MCP.
- Original program headers and recorded segment file/memory ranges. Undumped
memory and current executable/library identity remain unknown.
- Every note's original identity, owner/descriptor bytes and source ranges,
including unfamiliar owners and note kinds.
- Every exact Linux CORE PRSTATUS note as a separate recorded thread, with
historical PID, signal and lossless uint64 registers bound to source bytes.
Each supported note has exactly one interpretation; each thread contains the
complete 27-register ABI set, with names bound to their original ABI offsets.
Physical note ranges cannot overlap or leave segment gaps. ELF and note headers
are independently bound to the snapshot, including extended program counts.
- Signed SIGINFO number/code/errno. Initially only SIGSEGV codes 1/2 establish the
fault-address union; other variants retain raw bytes and unknown meaning.
Historical PIDs are metadata. They never select a live process or authorize
attach/control. Missing notes do not establish absence at the crash; note
interpretation completeness and signal-thread association remain unknown.
## Optional debugger mapping context
```sh
export REA_PWNDBG_GDB=/absolute/existing/gdb
export REA_PWNDBG_GDBINIT=/absolute/unchanged/pwndbg/gdbinit.py
export REA_PWNDBG_VENV_PATH=/absolute/existing/pwndbg-venv
rea inspect-recorded-crash ./crash.core --debugger-context --json
```
MCP uses `{ "path": "/artifacts/crash.core", "include_debugger_context": true }`.
Omitting this meaningful facet requests only recorded evidence. Requested but
unavailable context returns an actionable failure; it never silently claims
debugger coverage. GDB defaults to `/usr/bin/gdb`; the plugin and virtual
environment require explicit absolute paths. Initial local verification covers
Linux x64, GDB 13.1 and unchanged pwndbg 2026.09.15.
The outer provider owns both processes and one stable core snapshot. An owned
Python launcher lowers inherited soft limits before replacing itself with GDB.
GDB starts without initfiles, auto-load, debuginfod or automatic shared-library
loading. Owned empty library search paths and caches are used, with plugin
updates disabled. Fixed package code verifies a core connection and no paired
executable before querying mapping candidates. No caller GDB command language is
exposed. The exact observed GDB/plugin version strings are retained.
Mapping names are producer display metadata, with unknown current file identity.
Map permissions are derived from reported flags; zero/unfamiliar flags leave
permissions unknown. These maps do not establish page contents or an executable
match. Core source bytes remain available independently of this enrichment.
An Evidence envelope containing these maps is `derived`; inspection without
debugger context is `observed`.
## Bounds and verification
Initial Linux x64 limits: 32 MiB input, 64 MiB decoder reply, 1 MiB diagnostics per
stage, 30 seconds owned wall deadline per stage, 3 GiB address-space soft ceiling
and 30 seconds CPU soft ceiling. Tighter inherited soft/hard limits are retained;
hard limits are not raised. Complete oversized results fail explicitly. These
process limits do not constitute a sandbox for caller-supplied engine code.
Resource exit statuses need matching private failure-branch markers. Unmarked
launcher failures retain their actual status/output and unknown resource cause.
Cancellation, output truncation and independent owned cleanup remain explicit.
`npm run verify:recorded:crash` uses source-owned fixtures and caller-supplied
GDB/Python/pwndbg/strace. Fixture generation explicitly launches its owned test
program; subsequent inspection only reads the resulting core. Real acceptance
checks recorded threads, high registers, missing/malformed notes, opaque owners,
CLI/MCP parity, optional debugger context and historical PID collision with an
owned live sentinel. Original inputs and host configuration remain unchanged.
+144 -44
View File
@@ -1,15 +1,53 @@
# Releasing from a checkpoint
REA releases use a frozen source checkpoint. Main can continue accepting
changes while a release is tested and published. The Release workflow runs
on `release/*` branches; main pushes do not refresh release PRs.
Pushes to main automatically open or refresh a Release Please proposal with
the next version and changelog. This proposal cannot create a GitHub release
or publish packages. It previews the next release as main continues changing.
## 1. Cut the release branch
Publication uses an explicit source checkpoint. A maintainer selects a frozen
release branch and dispatches preparation and publication, so main can keep
accepting changes without changing the candidate being tested.
Choose the next version from the unreleased Conventional Commits, including
## 1. Select the source
Use the next minor version proposed by Release Please, including releases with
breaking changes. Record the full source SHA and create `release/VERSION` at
that commit. The version is a maintainer choice; Release Please's proposed
version must agree before publication.
that commit. The selected version and Release Please's proposed version must
agree before publication.
Use the automatic main proposal to review the suggested version and notes.
Keep that proposal unmerged: select the application commit from main, create
the frozen branch, and prepare its own release PR using the steps below.
Merging the proposal into main would advance the release baseline before
publication. The reviewed frozen-branch PR is the publication candidate.
`release/VERSION` also sets the expected version for the workflow. Use an exact
SemVer such as `release/6.1.0` or `release/6.1.0-rc.1`, without build metadata.
The checkpoint validator rejects a candidate whose package, lockfile, manifest,
registry metadata or changelog disagrees with that version before creating a tag.
### Compatibility and version selection
REA uses Release Please's `always-bump-minor` strategy: every release increments
minor and resets patch, including releases with breaking changes. For example,
the next release after 6.0.0 is 6.1.0. Version numbers use the SemVer format,
but a minor increment does not promise backward compatibility. Review the
changelog's breaking-change section and migration notes before upgrading.
The supported public surface includes documented CLI commands/options, MCP tool
names and input/result contracts, saved evidence formats and supported runtime
requirements. Before adding `!` or a `BREAKING CHANGE` footer, a PR must identify
a previously valid call or configuration that will fail or change meaning,
explain why compatibility cannot be preserved, and give its migration. Prefer
optional additions, compatibility adapters and a documented deprecation period.
Keep breaking markers so the bot includes migration notes without incrementing
major. Maintainers review this impact before merging; the bot parses markers
and cannot infer compatibility.
For example, adding an optional inspection tool is a minor change. Rejecting a
relative MCP path previously accepted by the public contract is breaking;
rejecting an input that the existing contract already prohibited is a fix.
Changing the version number alone does not restore compatibility.
For example, after selecting a reviewed commit for 5.0.0:
@@ -25,43 +63,80 @@ older checkpoint, backport only the release infrastructure first and record
that additional commit. Do not merge later implementation changes into the
release branch or rebase the candidate onto a moving main.
The branch push runs Release Please against that branch and opens a PR for
the version, changelog, and registry metadata. The generation step normalizes
the product catalog. To retry preparation after a failed run, select the same
frozen branch explicitly:
## 2. Prepare the bot PR
Run the workflow definition from main and select the frozen source separately:
```bash
gh workflow run release.yml --ref release/5.0.0
gh workflow run release.yml --ref main \
-f release_branch=release/5.0.0 -f phase=prepare
```
## 2. Review and merge the bot PR
Release Please targets that branch and prepares its version, changelog, and
registry metadata. The workflow uses an optional `RELEASE_PLEASE_TOKEN`
repository secret and otherwise uses the built-in `GITHUB_TOKEN`. A dedicated
GitHub App or personal access token can start PR CI automatically. With the
built-in token, `opened`, `synchronize`, and `reopened` PR events create runs
that require a maintainer with write access to approve them. See
[GitHub's workflow trigger documentation](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow).
Record the final bot PR head after normalization. Approve GitHub-blocked bot
workflow runs for that head when needed. Review the candidate's version,
notes, generated metadata, and package contents. Wait for the candidate's CI
and relevant real-provider checks. Routine local iterations need focused
checks; CI owns full deterministic coverage and platform lanes. If an artifact
or real-provider check is unavailable, report that limit before deciding to
publish.
The generation step normalizes the product catalog. Preparation cannot create
a GitHub release or publish a package. Record the final bot PR head after
normalization, then approve that head's blocked runs from the PR page. Updates
to the bot branch can create new approval-required runs; approval of an older
head does not verify the normalized candidate. Wait for the final head's CI.
Merge the reviewed PR into `release/5.0.0` with its head SHA matched. **This
merge authorizes and triggers publication.** Further main commits do not change
this candidate. Do not push or merge anything else into the release branch
while publication is running. A necessary release fix must be reviewed and
tested before the bot PR merges and establishes a new recorded checkpoint.
The validator reads the actual ancestry range from the published baseline tag
to the selected checkpoint, including visible first-parent Conventional Commits
and the PR titles in GitHub's default merge messages. The report preserves the
original merge subject alongside the extracted Conventional Commit title.
This matters after merging a side-branch release back into a newer main:
Release Please's chronological history cutoff can omit unreleased mainline
commits. Under `always-bump-minor`, unreleased breaking markers allow a minor
increment and still require references in the new release's breaking-change
migration section. Checkpoints using the default strategy retain the major
increment requirement. Other omitted entries are reported for review.
Historical notes from an older release cannot satisfy
the new release's migration check. This audits declared markers and references;
it does not prove API compatibility or the quality of a migration explanation.
## 3. Verify publication
If preparation reports a mismatch or omitted breaking changes, inspect the
recorded ancestry report, correct the release PR's version artifacts and notes,
regenerate documentation, and review/test its final head. Do not repeatedly
prepare over manual corrections. Publication independently checks the merged
candidate again before Release Please can create a tag.
The merge push runs Release Please again, creates the tag and GitHub release,
and publishes npm followed by MCP Registry metadata. Both publishers check
out Release Please's exact release SHA. The triggering workflow also runs
from that release commit so npm's provenance records the actual source.
Review the candidate's version, notes, generated metadata, and package
contents. Wait for the candidate's CI and relevant real-provider checks.
Routine local iterations need focused checks; CI owns full deterministic
coverage and platform lanes. If an artifact or real-provider check is
unavailable, report that limit before deciding to publish.
Before invoking Release Please, the workflow checks that the release branch
still equals the triggering SHA and rejects main or tag refs. Before either
registry publish, it rejects a mismatch between the tagged release SHA and
the triggering SHA. These checks detect moved branches; maintainers must also
keep the release branch frozen throughout publication.
Merge the reviewed PR into `release/5.0.0` with its head SHA matched. Further
main commits do not change this candidate. A necessary release fix belongs
on the release branch, must be reviewed and tested, and establishes a new
recorded checkpoint.
## 3. Publish the reviewed merge
After the release PR has merged:
```bash
gh workflow run release.yml --ref release/5.0.0 \
-f release_branch=release/5.0.0 -f phase=publish
```
Publication creates the release from the merged bot PR without preparing or
updating another PR. Both npm and MCP Registry jobs check out Release Please's
exact release SHA. They do not build the current main tip or a mutable branch.
Stable versions publish to npm's `latest` tag; prerelease versions publish to
the `next` tag so they cannot replace the stable install by default.
The publish dispatch runs from the frozen release branch so npm's provenance
records the actual release commit. Before Release Please creates a tag, the
workflow accepts only `prepare` or `publish` and requires the selected branch
tip to equal the dispatch SHA. Registry jobs run only during `publish`. After
a tag exists, a mismatch between that tag's SHA and the dispatch SHA stops
both registry publishes.
See [npm's provenance implementation](https://github.com/npm/cli/blob/v11.16.0/workspaces/libnpmpublish/lib/provenance.js)
for the use of GitHub's workflow ref and commit SHA.
@@ -78,16 +153,40 @@ Record these outcomes separately:
A GitHub tag alone does not establish npm or MCP Registry publication.
## Sync metadata back to main
## 4. Sync metadata back to main
Post-release synchronization is part of completing the release. Finish it
before cutting the next checkpoint; otherwise main retains the previous
release baseline and can propose an already-published version again.
After publication, open a PR from the release branch back to main. Preserve
main's later implementation changes and resolve generated-file conflicts by
regenerating from the combined contracts with the released package version.
main's later implementation changes and regenerate ignored build outputs from
the combined contracts with the released package version.
Review and test this synchronization PR, then use a merge commit so the release
tag remains in main's ancestry. Keep the released tag unchanged.
Close any superseded rolling release PR. Future releases repeat the checkpoint
procedure from main; never resume automatic release-PR refreshes on main pushes.
The synchronization must include `.release-please-manifest.json`,
`package.json`, both root versions in `package-lock.json`, `CHANGELOG.md`,
`server.json`, and the versioned documentation examples. Run
`npm run docs:generate`, `npm run docs:check`, and the release configuration
tests against the combined tree. After the merge, verify that the released
tag is an ancestor of main:
```bash
git fetch origin main --tags
git merge-base --is-ancestor rea-agents-5.0.0 origin/main
```
Close the superseded main proposal before merging the synchronization PR.
That merge's main push opens a proposal for the following release using the
updated baseline. Future publication repeats the checkpoint procedure;
automatic proposals do not move frozen branches.
Preparation generates and validates the catalog, portable conformance
projections, and packaged skill from the candidate checkout without committing
them. The checkpoint validator binds tracked version authority to the source
SHA; generated-catalog and package checks validate the derived representation.
No generated-metadata workflow pushes follow-up commits onto feature or release
branches.
## Partial publication and retries
@@ -97,7 +196,8 @@ publishing it again. Re-run failed jobs in the original publication run so its
release SHA and outputs stay fixed. If only MCP publication failed, retry that
job after checking that the npm canary succeeded.
Do not start a fresh workflow run to repair an already-created release:
Release Please will not create the same release again. Do not move the tag,
delete the release, or unpublish npm as a retry. A defective public package
requires a reviewed correction and a new version.
A branch tip that moved after dispatch fails before Release Please creates a
tag. Do not dispatch a fresh publish phase to repair an already-created
release: Release Please will not create the same release again. Do not move
the tag, delete the release, or unpublish npm as a retry. A defective public
package requires a reviewed correction and a new version.
+26 -2
View File
@@ -20,6 +20,30 @@ validation, provider prerequisites, target identity, cancellation, and owned
resource cleanup remain part of each tool's contract. Setup still discloses
installation changes and requires approval before writing or installing.
## Planned work
The [website guides](https://rea.tools/guides/) and
[generated catalog](product-catalog.json) describe available workflows.
Current development priorities are:
- Keep generated metadata and narrative documentation aligned with tools,
providers, setup options and releases.
- Expand native architecture, type and indirect-call verification across
Hopper and Ghidra.
- Connect additional static extractors and runtime observations to
cross-layer feature traces.
- Improve obfuscated .NET comparisons and links between managed findings
and verified native analysis.
- Extend process, protocol, filesystem, reconnect and build-comparison
coverage, plus browser and Electron scenario actions.
- Evaluate native runtime observation through LLDB, Frida, system logs and
API tracing, and additional tools and targets such as Binary Ninja, Rizin,
LIEF, Windows-native workflows, mobile applications and firmware.
Provider additions and platform support require the corresponding real
verification lanes; see [testing](testing.md) and
[provider evaluation](provider-evaluation.md).
## Remaining evidence and provider work
The [platform roadmap](https://github.com/morluto/rea/issues/32),
@@ -43,7 +67,7 @@ REA setup lets you select agent integration and optional Hopper installation.
It installs the bundled workflow, configures detected agents, and can save
verified paths for an existing Ghidra installation. It configures Claude Code,
Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity,
GitHub Copilot CLI, Command Code, and VS Code using each client's configuration format.
GitHub Copilot CLI, Command Code, VS Code, Grok Build, and Grok Bot using each client's configuration format.
Ghidra analysis supports Linux x64 and macOS x64/arm64 with Ghidra 12.1.x and the
64-bit full JDK that installation declares. Current 12.1 releases require JDK 21
@@ -62,7 +86,7 @@ Mach-O cross-target fixtures. See [testing](testing.md) for the prerequisites
and exact commands.
Experimental Windows x64 P0 now supplies bundled Job Object ownership,
protected runtime DACLs, and handle-based admission for native x86-64 PE
protected runtime DACLs, and handle-based admission for native x86 and x86-64 PE
applications on local NTFS. Real ordinary-user CLI/MCP verification covers
all 25 read-only operations and cleanup. The [Windows P0 guide](windows-ghidra-p0.md)
describes this boundary and unverified broader coverage.
+291 -18
View File
@@ -6,10 +6,57 @@ from real captured inputs. Keep focused module tests for distinct failure or
semantic cases that these workflows cannot reliably reproduce. A test's path
or suite name does not establish its behavioral depth.
Avoid tests that mirror getters, enum mappings, fixture helpers, or prescribed
call sequences, or assert cache object identity without a caller-visible contract.
Prefer representative workflows over a Cartesian matrix when rows cross the
same boundary. Retain combinations that exercise a distinct interaction, such as
filtering within a full-output envelope. Keep distinct command handlers,
producer representations, failure reasons, and lifecycle states covered.
Consolidate their shared setup without hiding the inputs or expected evidence.
Tests are grouped into Vitest projects so ownership, allowed dependencies and
runtime cost are visible from their paths. Before pruning a test, identify the
replacement scenario and its assertions; passing success journeys do not
replace malformed input, cancellation, permission or cleanup coverage.
runtime cost are visible from their paths. Before pruning a distinct boundary
regression, identify the replacement scenario and its assertions; passing
success journeys do not replace malformed input, cancellation, permission or
cleanup coverage. Trivial helper checks do not need a replacement. If only
tests consume a production module, trace CLI/MCP and verifier imports,
including imports of compiled files, and remove abandoned scaffolding with its
tests when it has no runtime consumer.
When a test expects a `Result` to succeed, throw its error before asserting the
returned value. An `.ok` assertion immediately before the same throwing guard
adds no coverage and hides the typed failure. A removed parser matrix can leave
one golden malformed-input case and a real filesystem lifecycle that proves
configure, update, readback, and removal; an add-only journey does not cover the
whole lifecycle.
Protocol fixtures must model the commands they support and their producer reply
shapes. Reject unmodeled commands instead of returning a generic success; an
empty result is appropriate only for a supported command with no result fields.
Keep transport-only in-memory MCP sessions in the MCP boundary lane and direct
filesystem workflows in the filesystem boundary lane. Real process-tree capture
belongs in the serial process boundary lane, including captures invoked through
MCP. Report unavailable capture authority as a named skip rather than returning
from the test without assertions. Reserve acceptance paths for journeys
through the compiled public CLI or MCP runtime. Calling a compiled private
workflow or injecting a provider still establishes its narrower integration
claim, even if a directory name calls it acceptance.
Emit a verifier's final success report only after awaited cleanup. Observe final
process lineage after closing its owned resources.
The Vitest runner releases cached native process inspectors during awaited
worker teardown. Fork termination does not run Node's normal exit hooks; a
per-file teardown would instead retire shared inspectors before later files
can use them.
Measure slow files before removing capacity regressions. Optimize repeated
process startup, fixture construction, and production algorithms when they
dominate runtime. Keep at least one input beyond each formerly failing size or
depth; smaller fixtures do not establish that truncation or stack exhaustion
has been fixed. Record the toolchain, coverage mode, and cache conditions when
comparing durations.
## Behavioral depths
@@ -87,7 +134,22 @@ extension, a login item, a privileged helper, launchd plists, and a helper tool,
signed ad hoc. It packs the app as a directory, a `ditto` ZIP, and an APFS DMG,
then checks that `inspect-artifact` plus `project-apple-application-graph`
report the same bundle anatomy for all three through the CLI, with stdio MCP
parity. It also checks that the DMG is detached afterwards. It runs in macOS CI.
parity. It also checks that the DMG is detached afterwards. The same app
covers `trace-dylib-resolution`:
- each resolution status and finding, with CLI/MCP parity;
- for every traced image, dependencies, rpaths, and install names equal to
`otool -l`;
- for the main executable and an XPC service, a predicted load order equal to
the images dyld actually loads under `DYLD_PRINT_LIBRARIES`.
The lane also compiles executable/library pairs with empty embedded directory,
versioned-path, and suffix settings. It checks present and removed dependencies
through CLI and MCP, and compares actual `DYLD_PRINT_SEARCHING` diagnostics for
root-level candidates. These cases distinguish an empty search directory from
an empty versioned scan or a suffix that only repeats the original path.
It runs in macOS CI.
Apple artifact verifiers live in `scripts/verify/apple/`, with the macOS bundle
builder under `scripts/fixtures/apple/` and NIB byte fixtures beside the decoder
@@ -103,6 +165,21 @@ names remain unchanged.
The existing Apple CI job also runs the host Swift-demangling CLI/MCP regression
suites, including option-like symbols, carriage returns and multiline rejection.
Process semantics/tests live in `src/domain/process/`; PTY capture implementation
and forked helper tests live in `src/process/capture/`. Evidence projection and
file workflows live in `src/application/process/`; producer-backed Evidence/host
cases run in the serial `tests/boundary/process/` lane. Installed-package probes
load the compiled capture capability owner, including the missing-optional-module
case. Real capture tests preserve actual descendant and cleanup checks.
The existing Apple job also exercises the relocated filesystem snapshot identity,
cancellation and descriptor cleanup regressions on macOS.
Prepare the native inspector before cases that measure producer output or exit
behavior; keep startup deadlines and cancellation in distinct cases. Run real
process-capture verification separately from package or build checks. On macOS,
new npm processes can become token-unreadable after changing their display
title and prevent verified cleanup during a capture's ownership sweep.
MCP SDK transport tests with recording providers remain integration tests.
They are useful for schema drift and failure projection but do not prove that
Hopper, Ghidra or another substituted engine works. `verify:package` proves
@@ -110,6 +187,46 @@ packaging/install behavior and fake-provider integration; use the corresponding
real-provider lanes for engine claims. Real Apple dispatch and Interface
Builder verifiers currently prove format integration through production readers.
`verify:hopper` exercises an installed Hopper through the production stdio MCP
server and CLI. It checks source-owned call chains, CFG edges, references and
complete large inventories, then probes unnamed bookmarks, annotation clearing,
batch validation before mutation, malformed addresses and regexes, segment-end
partial reads, and synthetic file-offset rejection. Advertised schemas are
validated in their JSON Schema dialect and successful replies are checked against
their advertised output schemas. Navigation checks cover interior-object cursor
readback and mapped-memory boundaries. Annotation probes verify invalid native
text and duplicate destinations/names before mutation, preserve unselected label
owners, and exercise explicit batch label swaps. Function locals retain observed
names and stack displacements. Graph probes check symbol/interior-address parity
and a source-owned recursive cycle; literal tracing preserves complete queries
and whitespace. Disposable binary copies prove that switching and closing
actually removes the native document, and that CLI byte, function-dossier,
literal-trace results and invalid-address diagnostics agree with MCP.
No provider is mocked in this lane.
Real search probes cover Unicode names, literal metacharacters, case and regex
modes, annotation cache invalidation, complete native fragments of long literals
checked against byte reads, escaped UTF-8/control text, byte-preserving Latin-1
decoding, and Hopper's UTF-16 symbol-name truncation boundary. It verifies native
CallReference classifications across reference and dossier results, retains long
string fragment metadata in dossiers, and exercises pathological regex deadline
and cancellation followed by successful requests in the same native session. When the macOS
Objective-C fixture is present, the lane also verifies native UTF-16 string objects
and their inferred encodings against their actual bytes.
Unmapped annotation destinations and
oversized later batch names fail before any earlier edit is applied.
The Linux demo lane remains a separate `verify:hopper:linux` command.
`verify:hopper:fat` is a separate macOS lane requiring installed Hopper and the
existing Xcode clang/lipo toolchain. It compiles arm64/x86-64 thin executables and
one- and two-slice FAT32 containers, verifies exact/interior address mappings
against bytes in the original files, checks CLI/MCP parity, and checks owned
runtime cleanup. `verify:hopper:fat64` additionally checks FAT64 preparation through
Hopper's native Mach-O loader, source-container mappings, profile identity,
malformed and ambiguous slice rejection, and temporary-image lifetime. Both
lanes have been verified on Hopper 6.1.0-demo; this establishes REA's prepared
FAT64 workflow, not native FAT64-loader support.
Cross-architecture fixture compilation is not required by `verify:hopper`.
Golden tests use immutable captured text inputs with producer/source provenance
under `tests/fixtures/golden/`. Expected results are reviewed for the semantic
claim; capture commands do not automatically approve new expected outputs.
@@ -171,7 +288,7 @@ prove behavior on the verified Ghidra 12.1.4 and JDK 21 build.
| `npm run verify:ghidra:switch` | Linux x64 ELF; GCC/Clang optimized and stripped switch fixtures | GCC, Clang, GNU nm/objdump/strip, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 ELF; byte/halfword tables; host ARM64 Mach-O | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:cross-format` | Any supported Ghidra host; also analyzes AArch64 ELF, x86-64 PE, and x86-64 Mach-O | `clang`, LLD, and `lld-link` in addition to host-lane prerequisites |
| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE | Ghidra 12.1.4, full JDK 21, and the matching native artifact |
| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE; `-- --x86` selects native x86 PE | Ghidra 12.1.4, full JDK 21, and the matching native artifact |
Windows native conformance runs with `npm run verify:windows-native` and does
not require Ghidra or Java. An optional independently compiled Windows fixture
@@ -184,6 +301,57 @@ The host-native Ghidra lane also verifies native value tracing through the
production CLI and a separate stdio MCP process. It compares complete dependency
graphs, validates Evidence and upstream/workflow profiles, checks capability
discovery, and closes the MCP session. No provider or transport is mocked.
It also validates every advertised input/output JSON Schema and the exercised
MCP outputs, probes address spelling and name/address ambiguity, and checks
direct versus targetless calls, byte-read completeness, invalid input diagnostics,
CLI/MCP parity, atomic annotation rollback, refreshed inventories, unchanged
executable bytes, and discarded edits after reopen. A deliberately long temporary
path exercises private Unix socket allocation and cleanup, including cancellation
after a real headless process launches. Native annotation probes reject NUL and
unpaired Unicode surrogates without partial edits or a broken bridge, preserve
supported Unicode and control text, and check lossless malformed-text diagnostics.
Memory-to-file mapping is checked against original artifact bytes.
The fixture also stores a pointer one byte past a function entry; exact
xrefs, raw procedure references, and CLI/MCP dossiers must retain that data edge.
A valid legacy snapshot reconstructs the former omitted edge under its older
profile; CLI and MCP must reject that binding with a mismatch reason and
recovery advice. The rejected open must preserve the active live session.
Exact external entries must resolve while retaining an empty body; unknown
external addresses remain unresolved and external annotations are rejected.
An adversarial regex over a full 12 KB literal must report stack exhaustion as
a resource constraint, preserve live annotations, and allow complete literal
searches afterward; CLI and MCP must agree on both results and recovery advice.
Real snapshot lifecycle checks retain edited API results as Evidence while
rejecting immutable snapshot saves and imports before and after a repeated
open of the same target. They verify unchanged live annotations and run identity,
absent rejected output files, an unchanged source snapshot, and successful
snapshot import/save after closing and recreating the database. The pristine
snapshot is written by an independent real CLI session.
They also start a real annotation and snapshot close concurrently: the edit
must succeed, the snapshot must be rejected without creating a file, and the
edited session must remain usable until explicitly closed.
Source-admission probes change a caller-owned fixture after `open_binary` but
before the first Ghidra query. They require an actionable `artifact_changed`
error preserving both digests and the selected path, unchanged provider
availability, failed-copy cleanup, and successful recovery after reopening.
After import, deleting that source must preserve the captured database identity.
Instruction inspection and containing-function lookup also agree across
hexadecimal case, leading zeros, and encoded default address-space spellings.
The same source-acquisition workflow exercises missing and directory-replaced
inputs, plus real read-permission denial on a non-root host. Root runs report
that permission-denial check as unverified. A focused filesystem integration
check retains selected-platform routing and exclusive creation; two copy/digest
checks duplicated by the real workflow have been removed.
Namespace annotation probes compile a separate host C fixture with C++ ABI
symbols, avoiding a C++ runtime prerequisite. Real Ghidra demangling supplies
duplicate leaf names in two top-level namespaces and a nested namespace. The
workflow verifies leaf and qualified renames, repeated reuse of fully qualified
readback, lookup by the returned name, literal namespace-like leaf names,
rejection of empty qualified leaf names without changing comments, CLI
behavior, and independent CLI/MCP database ownership.
Malformed annotation readback, memory completeness, and inventory data remain separate
SDK/provider integration cases; success from a real
provider cannot establish rejection of a contradictory provider response.
The Linux switch lane checks dense, sparse-with-holes, shared-body, nonzero,
negative, and nonexact JSON integer labels plus a comparison-only control.
@@ -249,7 +417,7 @@ are separate proof levels.
Synthetic producer regressions run independently:
```sh
npm run test:focused -- tests/boundary/android/jadxIntegration.test.ts tests/boundary/mcp/androidAnalysisMcp.test.ts
npm run test:focused -- tests/boundary/process/jadxIntegration.test.ts tests/boundary/process/androidAnalysisMcp.test.ts
```
## Optional NativeAOT Ghidra analysis
@@ -321,14 +489,18 @@ a DOS emulator or compiler, then checks real 16-bit decoding, segment
relocation, near/far calls, decompilation, disjoint function body ranges,
stable CLI/MCP observations, unchanged source bytes, and owned process/project
cleanup. Raw p-code address-space selector tokens are reported separately from
the stable observation comparison. Linux x64 is verified; macOS DOS remains
unverified. This lane is separate from host-native and optional cross-format
the stable observation comparison. Linux x64 and macOS arm64 are verified;
macOS x64 remains unverified. This lane is separate from host-native and optional cross-format
verification. See [DOS analysis](ghidra-dos.md).
`npm run verify:ghidra:com` uses a generated headerless fixture with no compiler,
DOS emulator or game data. It exercises explicit admission, BinaryLoader entry
preparation, measured register context, whole-file byte readback, source offsets,
unmapped PSP/partial reads, actual decompilation, CLI/MCP parity and owned cleanup.
Both segmented-address lanes reject oversized default, explicit-space and encoded-space
coordinates through real reads, function queries and annotation attempts. Rejected
annotations must preserve the live function dossier; CLI and MCP must report the
truncation constraint, while leading-zero coordinates still resolve correctly.
It has the same Ghidra/JDK prerequisites as the MZ lane. Neither lane claims DOS
runtime or PC-98 device execution.
@@ -390,7 +562,7 @@ behavior, and complete CI evidence before merging.
| `npm run test:watch:all` | Changed tests from every project; builds at startup, so rebuild after production edits before relying on compiled tests |
| `npm run check:changed` | Cached typecheck/lint and branch-related source feedback |
| `npm run check:pr` | Opt-in complete local deterministic gate and generated-file checks |
| `npm run docs:check` | Committed generated-document freshness |
| `npm run docs:check` | Generated-document validation from current source and build outputs |
For example:
@@ -425,19 +597,23 @@ broad changes or diagnosing CI, rather than after every edit. Package/install
changes additionally need package verification; provider changes need actual
provider evidence.
Local full-suite Vitest runs use up to two workers and schedule projects one
at a time. Process, acceptance and process-global projects serialize their
files to prevent competing lifecycle observations. CI retains its two-worker
budget.
Each Vitest project uses up to two workers, bounded by available host parallelism.
Default-group projects can run together. Only `process-boundary` runs in the
later sequence group and serializes its files because process-tree observations
share host sampling resources. Tests that launch the real process harness belong
in `tests/boundary/process/`, including CLI command and environment tests.
Acceptance and process-global files retain isolated
forks but are not serialized; isolation does not imply serial scheduling. CI uses
the same per-project worker bound.
The pure domain/contracts and recording-port service projects share one worker
module context because their tests own no mutable runtime resources. MCP
boundary files also share the immutable server module graph while creating and
closing independent in-memory sessions. Adapter, composition, acceptance,
process-global, and other boundary projects retain per-file isolation.
`npm test`, `npm run docs:check`, and `npm run docs:generate` share
repository-local locks and fail fast when the same class of command is already
running. The `npm test` build is inside that lock. `check:pr` runs its test task
before starting generated-document validation.
Build and documentation writers use checkout-local locks for their respective
output files. `npm test` delegates its build to the build lock; the Vitest suite
does not hold a test-command lock. `check:pr` runs its test task before starting
generated-document validation.
Vitest and Node persistent compile caches are deliberately not enabled by
default. To evaluate repeated local runs, opt in for both cold and warm
@@ -494,7 +670,20 @@ conformance/vtable fixture.
`positive_e2e: false`; it must not be reported as capture/action proof.
Both commands reject a changed executable digest and clean up the fixture
process and helper. These lanes require an interactive macOS desktop. See [native investigation](native-investigation.md)
for the exact ABI, authority, graph and observation boundaries.
`npm run verify:native-calls` needs macOS with Command Line Tools (`clang`,
`lldb`, `codesign`, `nm`) and Developer Mode enabled for unattended debugger
access. Preflight reports disabled Developer Mode without changing host settings.
It compiles `tests/conformance/native/calls.m` and
runs `observe-native-calls` through the CLI and stdio MCP. It checks:
- the receiver class, selector and argument registers of every entry, and that
breakpoint addresses equal `nm`'s symbol addresses;
- captured stdout and an environment override, plus a 2 MiB flood on each
output stream with bounded retained prefixes and exact drained-byte counts;
- the event-limit and duration outcomes, with the process confirmed gone;
- that a hardened-runtime copy is refused with `debugger-attach-denied`, and
that the same copy signed with `get-task-allow` is traced.
### Firmware adapters
@@ -552,6 +741,18 @@ conditional `real-web-source-map` CI job supplies Chrome and an isolated pinned
fixture compiler; static/unit checks do not acquire a browser. See
[the source location guide](web-source-location.md) for the verified decoder profile.
### JavaScript large-output lane
`npm run verify:javascript:output` exercises the CLI JSON result surface beyond
the running Node engine's single-string limit. Shared input leaves keep the
fixture's graph small; the verifier writes one temporary output file, checks its
complete byte count and an independent digest, then removes it. It requires only
Node and the built REA runtime, with space for the output plus a 1 GiB reserve.
Run `npm run verify:javascript:output -- jsonl` for compact JSONL coverage. This
opt-in lane is separate from routine tests and the canonical hash check
`npm run verify:javascript:digests`. It verifies serialization rather than an
arbitrary third-party application's parsing cost or MCP client capacity.
### Website runtime attribution lane
`npm run verify:browser:runtime` uses caller-supplied
@@ -567,3 +768,75 @@ same checks through an isolated installed package. The conditional
compiler. Ordinary unit/static gates acquire no browser. See
[website runtime attribution](web-runtime.md) for effects, resource bounds and
coverage limits.
### Offline binary layout
`npm run verify:binary:layout` requires Linux x64, GCC/binutils, absolute
`REA_PWNTOOLS_PYTHON` with pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2 and
absolute `REA_VERIFY_STRACE_COMMAND`. It compiles ephemeral source-owned ELF
fixtures and checks public CLI/MCP, lossless addresses/names, file ranges,
mitigation inferences, malformed/unsupported input, original file hashes and
released process ownership. Exec syscall tracing must identify only the declared
Node/Python launchers; no target binary is executed. Core/debugger claims need
separate verification lanes. Pass an installed package entrypoint as the script's
first argument to verify packaging independently of the checkout.
The valid SHN_XINDEX fixture has 65,281 full section rows; CLI is checked in
the ordinary lane. Its large MCP transfer is opt-in with
`REA_VERIFY_LARGE_ELF_MCP=1` (or the workflow dispatch `large_mcp` input), an
explicit 256 MiB SDK receive buffer and five-minute request timeout. Ordinary
MCP fixtures retain the pinned SDK defaults.
### Offline EVM interface
`npm run verify:evm:interface` requires Linux x64, an absolute
`REA_VERIFY_STRACE_COMMAND`, caller-supplied util-linux `prlimit` and `REA_VERIFY_SOLC_MODULE` selecting the absolute module path for
solc 0.8.30. It compiles source-owned plain/optimized/via-IR Cancun fixtures in
private storage and checks actual CLI/MCP selector evidence, raw/hex identity,
unknowns, malformed carriers and independent cleanup. An optional positional
entrypoint verifies a fresh installed package. It acquires no engine, compiler
or chain dependency and does not execute a contract on a chain.
### Recorded crash evidence
`npm run verify:recorded:crash` is a separate Linux x64 lane. It requires GCC,
GDB, absolute `REA_PWNTOOLS_PYTHON` with the offline ELF profile above,
`REA_PWNDBG_GDBINIT` and `REA_PWNDBG_VENV_PATH` with unchanged pwndbg 2026.09.15,
and `REA_VERIFY_STRACE_COMMAND`. Its disposable CI runner installs GDB, checks out the exact upstream
commit and installs its frozen lockfile in isolated runner storage. No developer
host configuration or core-pattern setting changes.
Fixture generation explicitly runs an owned source-built two-thread program
under GDB to create a recording. Subsequent public CLI/MCP inspection verifies
lossless high registers, signed signals, note source bytes, malformed/missing
notes, unfamiliar owners, optional core-only mapping context and actionable
missing/unsupported plugin errors. A historical-PID collision fixture references
an owned live sentinel; inspection syscall traces reject process attach/memory
access, provider lookups of that PID's `/proc` files and attempted Internet sockets. Traces admit
the observed upstream startup helpers (`iconv -l`, the selected checkout's Git
version lookup) and REA ownership inspection separately from target execution.
This is fixture evidence, not a sandbox claim. Inputs remain unchanged and the
sentinel must stay alive; owned cleanup and empty verifier descendants are required. Pass an
installed package entrypoint as the script's first argument for package coverage.
## Agent evaluation and conformance records
Evaluate native, JavaScript, managed and browser investigation tasks through a
real local Codex CLI with:
```bash
npm run verify:agent
```
Its report records tool selection, repeated calls, token use, completion quality,
and handling of permissions and unknowns.
Regenerate the managed conformance manifest and Evidence completion ledger from
live verification results, or check them for drift:
```bash
npm run evidence:generate
npm run evidence:check
```
The records preserve unsupported and unverified coverage as explicit unknowns.
Run the matching real-tool prerequisites described in this guide.
+12 -1
View File
@@ -80,7 +80,18 @@ or model-authored loop when one call can answer the question.
## Implement and evaluate
- Put shared user workflows in the application layer; keep MCP translation in
the server adapter and CLI behavior aligned with the same workflow.
the server adapter and CLI behavior aligned with the same workflow. Share
Evidence provenance, unknown projection, and eligible snapshot binding through
application/session owners. Matching result payloads alone do not establish
equivalent retention or replay behavior.
- Derive each facet's coverage from examined records, unsupported representations,
and exhaustiveness. Keep issues structured until presentation; do not select a
facet by matching diagnostic prose or infer completeness from an empty failure
list. Preserve available observations independently of cleanup success.
- Measure the resource consumed before choosing a limit. Value alternatives,
retained records, trace-frame products, output bytes, and temporary-file growth
need different accounting. Enforce a justified budget before the costly step
and report its effect; an input-byte or graph-node cap does not bound every stage.
- Update the canonical contract inventory, output schemas, examples, generated
catalog, and relevant docs together.
- Verify valid, malformed, boundary, unavailable, cancellation, and partial
+6 -6
View File
@@ -68,12 +68,12 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
58,src/application/SetupHost.ts:unsupported Node,Runtime version cannot run setup,"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ and rerun setup.",Low risk; exact dependency and retry,Keep current copy,unchanged,verified by setup host tests
59,src/application/Setup.ts:unsupported macOS,macOS version is missing or older than 12,Upgrade to macOS 12 or newer.,Low risk; exact support floor,Keep current copy,unchanged,verified by setup host tests
60,src/application/Setup.ts:unsupported Linux,Linux distribution is outside supported set,"REA supports Hopper on Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and CachyOS.",Low risk; exact supported alternatives,Keep current copy,unchanged,verified by setup unit test
61,src/application/ProcessCli.ts:readJson read,Process scenario or evidence file cannot be read,Raw filesystem exception through CLI framework,Can expose local paths/codes and no recovery,"Process input file could not be read. Check that the path exists and is readable.",implemented,verified by focused CLI test
62,src/application/ProcessCli.ts:readJson size,Process scenario or evidence file exceeds 64 MiB,Process JSON input exceeds byte limit,Does not state limit or recovery,"Process input file is too large. Reduce it below 64 MiB, then try again.",implemented,verified by focused sparse-file CLI test
63,src/application/ProcessCli.ts:readJson parse,Process scenario or evidence file is malformed JSON,Raw JSON parser exception through CLI framework,Can expose parser implementation and lacks recovery,"Process input file is not valid JSON. Repair the file, then try again.",implemented,verified by focused CLI test
64,src/application/ProcessCli.ts:capture scenario parse,Scenario fails Process Capture schema,Raw Zod validation exception through CLI framework,Exposes schema internals and field paths without stable recovery,"Process scenario is invalid. Check its required fields and limits, then try again.",implemented,verified by focused CLI test
66,src/application/ProcessCli.ts:wrong evidence,Comparison receives valid Evidence from another workflow,Expected REA process capture Evidence,Uses internal schema name and does not explain recovery,"Capture evidence is not from the current process-capture workflow. Create new capture evidence, then try again.",implemented,verified by focused CLI test
67,src/application/ProcessCli.ts:unexpected,Unexpected parser/comparison/canonicalization failure escapes expected classifications,Raw thrown exception through CLI framework,Can expose stack or internal invariant,"Process command could not complete. Check the input files and run `rea doctor`, then try again.",implemented,verified by direct unexpected-cause projection test
61,src/application/process/ProcessCli.ts:readJson read,Process scenario or evidence file cannot be read,Raw filesystem exception through CLI framework,Can expose local paths/codes and no recovery,"Process input file could not be read. Check that the path exists and is readable.",implemented,verified by focused CLI test
62,src/application/process/ProcessCli.ts:readJson size,Process scenario or evidence file exceeds 64 MiB,Process JSON input exceeds byte limit,Does not state limit or recovery,"Process input file is too large. Reduce it below 64 MiB, then try again.",implemented,verified by focused sparse-file CLI test
63,src/application/process/ProcessCli.ts:readJson parse,Process scenario or evidence file is malformed JSON,Raw JSON parser exception through CLI framework,Can expose parser implementation and lacks recovery,"Process input file is not valid JSON. Repair the file, then try again.",implemented,verified by focused CLI test
64,src/application/process/ProcessCli.ts:capture scenario parse,Scenario fails Process Capture schema,Raw Zod validation exception through CLI framework,Exposes schema internals and field paths without stable recovery,"Process scenario is invalid. Check its required fields and limits, then try again.",implemented,verified by focused CLI test
66,src/application/process/ProcessCli.ts:wrong evidence,Comparison receives valid Evidence from another workflow,Expected REA process capture Evidence,Uses internal schema name and does not explain recovery,"Capture evidence is not from the current process-capture workflow. Create new capture evidence, then try again.",implemented,verified by focused CLI test
67,src/application/process/ProcessCli.ts:unexpected,Unexpected parser/comparison/canonicalization failure escapes expected classifications,Raw thrown exception through CLI framework,Can expose stack or internal invariant,"Process command could not complete. Check the input files and run `rea doctor`, then try again.",implemented,verified by direct unexpected-cause projection test
68,src/server/registerEvidenceTools.ts:object guard,Validated provider-tool input is unexpectedly not an object,Provider tool input was not an object,Exposes provider architecture and implies caller can fix impossible state,Remove the impossible duplicate guard and parse through the declared JSON-object schema.,removed,verified by typecheck and MCP integration tests
69,src/server/registerEnhancedTools.ts:object guard,Validated enhanced-tool input is unexpectedly not an object,Validated enhanced input was not a JSON object,Exposes validation/JSON implementation detail,Remove the impossible duplicate guard and parse through the declared JSON-object schema.,removed,verified by typecheck and enhanced MCP integration tests
70,src/server/registerOfficialTools.ts:object guard,Validated direct-tool input is unexpectedly not an object,Validated MCP tool input was not a JSON object,Exposes MCP validation implementation detail,Remove the impossible duplicate guard and parse through the declared JSON-object schema.,removed,verified by typecheck and official MCP integration tests
1 harm_rank location trigger current_copy user_risk proposed_replacement implementation_status verification_result
68 58 src/application/SetupHost.ts:unsupported Node Runtime version cannot run setup Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ and rerun setup. Low risk; exact dependency and retry Keep current copy unchanged verified by setup host tests
69 59 src/application/Setup.ts:unsupported macOS macOS version is missing or older than 12 Upgrade to macOS 12 or newer. Low risk; exact support floor Keep current copy unchanged verified by setup host tests
70 60 src/application/Setup.ts:unsupported Linux Linux distribution is outside supported set REA supports Hopper on Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and CachyOS. Low risk; exact supported alternatives Keep current copy unchanged verified by setup unit test
71 61 src/application/ProcessCli.ts:readJson read src/application/process/ProcessCli.ts:readJson read Process scenario or evidence file cannot be read Raw filesystem exception through CLI framework Can expose local paths/codes and no recovery Process input file could not be read. Check that the path exists and is readable. implemented verified by focused CLI test
72 62 src/application/ProcessCli.ts:readJson size src/application/process/ProcessCli.ts:readJson size Process scenario or evidence file exceeds 64 MiB Process JSON input exceeds byte limit Does not state limit or recovery Process input file is too large. Reduce it below 64 MiB, then try again. implemented verified by focused sparse-file CLI test
73 63 src/application/ProcessCli.ts:readJson parse src/application/process/ProcessCli.ts:readJson parse Process scenario or evidence file is malformed JSON Raw JSON parser exception through CLI framework Can expose parser implementation and lacks recovery Process input file is not valid JSON. Repair the file, then try again. implemented verified by focused CLI test
74 64 src/application/ProcessCli.ts:capture scenario parse src/application/process/ProcessCli.ts:capture scenario parse Scenario fails Process Capture schema Raw Zod validation exception through CLI framework Exposes schema internals and field paths without stable recovery Process scenario is invalid. Check its required fields and limits, then try again. implemented verified by focused CLI test
75 66 src/application/ProcessCli.ts:wrong evidence src/application/process/ProcessCli.ts:wrong evidence Comparison receives valid Evidence from another workflow Expected REA process capture Evidence Uses internal schema name and does not explain recovery Capture evidence is not from the current process-capture workflow. Create new capture evidence, then try again. implemented verified by focused CLI test
76 67 src/application/ProcessCli.ts:unexpected src/application/process/ProcessCli.ts:unexpected Unexpected parser/comparison/canonicalization failure escapes expected classifications Raw thrown exception through CLI framework Can expose stack or internal invariant Process command could not complete. Check the input files and run `rea doctor`, then try again. implemented verified by direct unexpected-cause projection test
77 68 src/server/registerEvidenceTools.ts:object guard Validated provider-tool input is unexpectedly not an object Provider tool input was not an object Exposes provider architecture and implies caller can fix impossible state Remove the impossible duplicate guard and parse through the declared JSON-object schema. removed verified by typecheck and MCP integration tests
78 69 src/server/registerEnhancedTools.ts:object guard Validated enhanced-tool input is unexpectedly not an object Validated enhanced input was not a JSON object Exposes validation/JSON implementation detail Remove the impossible duplicate guard and parse through the declared JSON-object schema. removed verified by typecheck and enhanced MCP integration tests
79 70 src/server/registerOfficialTools.ts:object guard Validated direct-tool input is unexpectedly not an object Validated MCP tool input was not a JSON object Exposes MCP validation implementation detail Remove the impossible duplicate guard and parse through the declared JSON-object schema. removed verified by typecheck and official MCP integration tests
@@ -1,105 +0,0 @@
{
"ledger_id": "ecl_132e857a8a93f0b366cd72f9cfa1c338ffd4100460a7834ef69abff936c84472",
"records": [
{
"claim_id": "managed.application-graph-projection",
"status": "pass",
"evidence_ids": [
"ev_b061a5a559acb3748cb9784c637f6693f74faeb564ba2b11d6fc32619e757459"
]
},
{
"claim_id": "managed.decompiler-reconstruction-import",
"status": "pass",
"evidence_ids": [
"ev_13f3401a3aaddc3f6d087ad160fa928374e142cdb9861254359b46865b8c992e"
]
},
{
"claim_id": "managed.dotnet-framework-x86-pinvoke",
"status": "pass",
"evidence_ids": [
"ev_6f026216826ca06e080b77ff608f02634fa945fbd2e151b4cd261c1444f8bbb1"
]
},
{
"claim_id": "managed.ilspy-reconstruction-oracle",
"status": "unsupported",
"evidence_ids": [
"ev_65a94c4de03f9c6ebdaff3f381cca377821f349d3ca6dfad47a31cc586fae939"
]
},
{
"claim_id": "managed.malformed-metadata",
"status": "pass",
"evidence_ids": [
"ev_deae4259ea4395623176de40d06c2abcbc2b3a77f0bfb068d76e568d801f45a8"
]
},
{
"claim_id": "managed.managed-native-verification",
"status": "pass",
"evidence_ids": [
"ev_0edcbbcaa1191a4e5324a437f739957b77f9ac45e7897724337f2d6d06b3f9d1"
]
},
{
"claim_id": "managed.modern-dotnet-anycpu",
"status": "pass",
"evidence_ids": [
"ev_d1af50e8dc04cd589385769efd8758e82095425f6e9c1cc4bf2f530c6b434c93"
]
},
{
"claim_id": "managed.mvid-and-token-drift",
"status": "pass",
"evidence_ids": [
"ev_5493f40bc875535488d789a490361abcc1f76bfbdf52536d09ff127442a8e854"
]
},
{
"claim_id": "managed.not-managed",
"status": "pass",
"evidence_ids": [
"ev_03ce0cc0183ee0710f8120e837565129559ab3bc41927df326066370c2341699"
]
},
{
"claim_id": "managed.operator-local-manifest",
"status": "unsupported",
"evidence_ids": [
"ev_9d3e50da1d372c55f46649e69eae702cbe8f0ba6a81d1e46afc3e1a284c81757"
]
},
{
"claim_id": "managed.operator-manifest-self-test",
"status": "pass",
"evidence_ids": [
"ev_f974f4b9d039cda841894c6173cae86427ecef48f4e7a4d6c73edf7b23af2b2a"
]
},
{
"claim_id": "managed.unicode-obfuscated-identifiers",
"status": "pass",
"evidence_ids": [
"ev_c4f38ce76d8359d92ee8e9442503e47b7913550442349a0df688bd274a3def67"
]
},
{
"claim_id": "managed.x64-ready-to-run-native-body",
"status": "pass",
"evidence_ids": [
"ev_3e05ef1d6a796d3d4cb3372fe8e549efec759c59bf09d8ff660059a2e448cbc0"
]
}
],
"summary": {
"total": 13,
"pass": 11,
"fail": 0,
"unsupported": 2,
"truncated": 0,
"unknown": 0,
"complete": false
}
}
@@ -1,201 +0,0 @@
{
"manifest_id": "ecm_880356f1ceceb9e29b43e3e89f7a326aa83132e1e1fae0573245848ba714f404",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
},
"environment": {
"platform": "portable",
"architecture": "portable",
"runtime": "node",
"runtime_version": "portable"
},
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "97dc167b6e0d41ac69953501c96a918319fa6e4b3be0affe2fc2119294e7101d"
}
],
"claims": [
{
"claim_id": "managed.application-graph-projection",
"scenario": {
"id": "managed-application-graph-projection",
"version": 1
},
"artifact_sha256s": [
"f17fe6286cb07c6385e786e0aa877564d44123197f02617dfec70c537a33e4ea"
],
"provider": {
"id": "rea-managed-conformance",
"version": "1"
}
},
{
"claim_id": "managed.decompiler-reconstruction-import",
"scenario": {
"id": "decompiler-reconstruction-import",
"version": 1
},
"artifact_sha256s": [
"cead4cc99733e4e56a5fd0cec8a0813bbc24e249aa72a10eca4c47ee3b0e129d"
],
"provider": {
"id": "rea-managed-conformance",
"version": "1"
}
},
{
"claim_id": "managed.dotnet-framework-x86-pinvoke",
"scenario": {
"id": "dotnet-framework-x86-pinvoke",
"version": 1
},
"artifact_sha256s": [
"f17fe6286cb07c6385e786e0aa877564d44123197f02617dfec70c537a33e4ea"
],
"provider": {
"id": "rea-managed-conformance",
"version": "1"
}
},
{
"claim_id": "managed.ilspy-reconstruction-oracle",
"scenario": {
"id": "ilspy-reconstruction-oracle",
"version": 1
},
"artifact_sha256s": [],
"provider": {
"id": "rea-managed-conformance",
"version": "1"
}
},
{
"claim_id": "managed.malformed-metadata",
"scenario": {
"id": "malformed-metadata",
"version": 1
},
"artifact_sha256s": [
"827e10b6391d29edca48c191e2ddd79f3bc1165e46e4ba2f4edf41ab60f45e78"
],
"provider": {
"id": "rea-managed-conformance",
"version": "1"
}
},
{
"claim_id": "managed.managed-native-verification",
"scenario": {
"id": "managed-native-verification",
"version": 1
},
"artifact_sha256s": [
"9999999999999999999999999999999999999999999999999999999999999999",
"f17fe6286cb07c6385e786e0aa877564d44123197f02617dfec70c537a33e4ea"
],
"provider": {
"id": "rea-managed-conformance",
"version": "1"
}
},
{
"claim_id": "managed.modern-dotnet-anycpu",
"scenario": {
"id": "modern-dotnet-anycpu",
"version": 1
},
"artifact_sha256s": [
"92b588ae43d418117fa108c0004b5d0451f6112238d21bad8d75c0eb1da860f0"
],
"provider": {
"id": "rea-managed-conformance",
"version": "1"
}
},
{
"claim_id": "managed.mvid-and-token-drift",
"scenario": {
"id": "mvid-and-token-drift",
"version": 1
},
"artifact_sha256s": [
"276395c671a656ab57d583158443c10de552dccfb37dc97cd013f673c0f86b43",
"ef009dab7424fdeba996f2524dbe86fd899c96394498cca0463fcfdb228c5572"
],
"provider": {
"id": "rea-managed-conformance",
"version": "1"
}
},
{
"claim_id": "managed.not-managed",
"scenario": {
"id": "not-managed",
"version": 1
},
"artifact_sha256s": [
"de74dab8ab034054a811d4248efcda0bed505187a061a806e274ae87bc453754"
],
"provider": {
"id": "rea-managed-conformance",
"version": "1"
}
},
{
"claim_id": "managed.operator-local-manifest",
"scenario": {
"id": "operator-local-manifest",
"version": 1
},
"artifact_sha256s": [],
"provider": {
"id": "rea-managed-conformance",
"version": "1"
}
},
{
"claim_id": "managed.operator-manifest-self-test",
"scenario": {
"id": "operator-manifest-self-test",
"version": 1
},
"artifact_sha256s": [
"cdc11ec7fd0f392e43b9b23971cb41ec49fd0a997fc33c24fcc553cd4386ae37"
],
"provider": {
"id": "rea-managed-conformance",
"version": "1"
}
},
{
"claim_id": "managed.unicode-obfuscated-identifiers",
"scenario": {
"id": "unicode-obfuscated-identifiers",
"version": 1
},
"artifact_sha256s": [
"cead4cc99733e4e56a5fd0cec8a0813bbc24e249aa72a10eca4c47ee3b0e129d"
],
"provider": {
"id": "rea-managed-conformance",
"version": "1"
}
},
{
"claim_id": "managed.x64-ready-to-run-native-body",
"scenario": {
"id": "x64-ready-to-run-native-body",
"version": 1
},
"artifact_sha256s": [
"e47402428322cdcb06f561036d7d108528cbc6e262d92a21f869218a20c80bfb"
],
"provider": {
"id": "rea-managed-conformance",
"version": "1"
}
}
]
}
+147
View File
@@ -0,0 +1,147 @@
# Historical web network captures
Use `inspect_web_network_capture` / `inspect-web-network-capture` to inspect
retained producer evidence. This is an offline operation: recorded URLs are
never fetched, and no browser or proxy listener is started.
```sh
rea inspect-web-network-capture /absolute/session.har har --json
rea inspect-web-network-capture /absolute/session.har har --record 2 --record 0 --json
```
```json
{
"name": "inspect_web_network_capture",
"arguments": {
"capture_path": "/absolute/session.har",
"format": "har",
"record_ordinals": [2, 0]
}
}
```
Omit ordinals to return every record. Explicit selection preserves caller order
and original zero-based producer ordinals; duplicate or out-of-range selections
are input errors. Every record is validated before selection, so selecting one
entry does not hide a malformed entry elsewhere.
## Formats and upstream profiles
HAR uses unchanged `har-schema@2.0.0` draft-06 schemas, `ajv@8.20.0`,
`ajv-formats@3.0.1`, `jsonc-parser@3.3.1` JSON visitor, and
`lossless-json@4.3.1` numeric representations, pinned in the npm lockfile.
Only the HAR 1.2 profile is supported. Unknown extensions remain reported
evidence. Duplicate object keys, including equal values and escaped spellings,
are rejected before materialization. Prototype-named members such as
`__proto__` are preserved as ordinary own JSON members without prototype
mutation. Original capture files remain unchanged.
The observed mitmproxy 12.2.3 `SaveHar` profile can emit `postData.text: null`
for a missing request body. REA preserves that reported null, omitting the
optional field only from its validation copy for this exact producer profile;
it does not invent an empty string or body bytes.
Native mitmproxy decoding requires a caller-supplied **mitmdump 12.2.3 on
Linux**. Set its absolute executable path:
```sh
REA_MITMDUMP_COMMAND=/absolute/mitmdump rea inspect-web-network-capture /absolute/session.mitm mitmproxy --json
```
REA does not install mitmproxy. Its owned process uses a private configuration
directory, `--no-server`, and the unchanged upstream `mitmproxy.io.tnetstring`
decoder. It does not run `FlowReader` migrations or convert native states to
HAR. Raw IDs, state versions, flow types, backup states and unknown extensions
are preserved without synthesizing connections, timestamps or content. The
tested upstream source is tag `v12.2.3`, commit
`6c09d56e4c29a92f5ad01b03199977584b8ea14f`; other native tool versions return
an explicit unsupported-profile diagnostic. Non-Linux native decoding is not
supported by this adapter.
## Reading the evidence
- `artifact` identifies the caller-selected file and stable snapshot digest.
- `records` preserve producer order/ordinals and original JSON pointers or byte
ranges. No live browser transaction or scenario IDs are invented.
- `reported` retains the producer's fields. HAR container metadata is returned
separately, with `log.entries` replaced by `null` and `records_pointer`
identifying the record collection. This is an inspection representation,
not a reconstructed HAR file.
- `numeric_literals` preserve HAR number lexemes and native integer/float
representations, including unsafe integers and non-finite native floats.
Values that cannot be safely represented as JSON numbers are `null` in the
reported view. Unsafe standard HAR numeric fields fail schema validation;
unsafe extension numbers remain available through their sidecars.
- `binary_fields` retain exposed bytes, base64 and SHA-256 independently of
declared sizes. Valid HAR `content.encoding: "base64"` is decoded exactly;
producer-decoded Unicode text, missing text and unknown encodings do not
establish original body bytes. Post-data parameters do not reconstruct a
multipart body. Native byte fields also have an exact UTF-8 display view
when valid; binary sidecars establish their actual representation. Native
missing content and empty content remain distinct.
- `redactions` identify known transport authentication exclusions and explicit
sensitive-value exclusions. Redacted byte fields retain neither original
bytes nor their digest. Unknown extension names and token-looking query
parameters do not establish sensitivity.
Mark sensitive literal string/UTF-8 byte values with MCP `sensitive_values`, or
repeat CLI `--sensitive-value`. These declarations are never persisted in
Evidence parameters; the count is retained. Explicit text/UTF-8 byte matches
exclude the entire field with a reported `null` rather than substitute a text
marker or rewrite payload bytes. This prevents replacement markers from
reintroducing declared literals. The file digest identifies the original artifact
unless it matches an explicit declaration; in that case `artifact.sha256` and
Evidence `subject` are null with an explanation. A sensitive property name excludes that
entire property and its sidecars; a `scope: "property-name"` redaction points
to its actual parent. No substitute property name or child pointer is invented.
Omitted subtrees are still validated, including canonical HAR base64. Explicitly sensitive artifact
paths are empty in Evidence and omitted from locations, while the original
SHA-256 and size remain available unless the digest itself is explicitly excluded. Failure messages, diagnostics and cleanup
resources follow the same explicit declarations without changing error types;
sensitive issue pointers identify a real ancestor instead of a fabricated
coordinate. Header/cookie authentication values and known
transport URL userinfo are excluded structurally, including native backups.
Generated free-text limitations follow the same literal declarations, including
global, per-record and Evidence explanations added during projection. Contract
field names, typed discriminators and exact decoder identities keep their
canonical meanings; declarations do not rename formats or providers.
Historical evidence does not prove runtime attribution, execution or deployment
authenticity. Mitmproxy messages are upstream-assembled messages, not recovered
wire fragments. Native dictionary duplicate keys have the unchanged upstream
decoder's last-value semantics; this adapter does not claim to recover discarded
duplicates or the original numeric spelling of parsed native floats.
## Resource and lifecycle boundaries
Complete evidence has a 32 MiB input, 96 MiB decoder reply and 64-level nesting
budget. Exceeding a budget returns no partial success.
Oversized selected captures return an input error identifying `capture_path`
and the byte budget; owned reply/output failures remain output or provider errors.
HAR runs in an owned
192 MiB Node old-generation heap with one V8 worker. Native decoding applies
Linux limits of 768 MiB address space, 30 seconds CPU and 96 MiB file output;
both commands have a 30-second wall deadline and independently supervised
process cleanup. Owned decoder diagnostics retain at most 1 MiB across stdout
and stderr; the collector drops overflow as it arrives, reports the limit
failure and verifies cleanup. Heap limits alone do not establish aggregate RSS limits.
Stable regular-file reads reject symlinks, replacement and concurrent changes.
Private snapshots, declarations and tool configuration are removed before the
result returns. Cleanup uncertainty retains the selected capture, resource and
original failure information. The original capture is never changed.
## Excluded coordinates and read failures
Explicitly marked values also exclude matching source coordinates and their sidecars. A hidden record coordinate is reported as `location.kind: "unknown"`; REA does not invent a replacement pointer. Original record ordinals and artifact digests remain available.
Host filesystem read denials return `code: "access_denied"`, `category: "unavailable"`, the actual `EACCES`/`EPERM` code and read-access guidance. Missing files remain input failures. Cleanup failures retain the prior structured error, including its input constraints or provider diagnostics, with explicitly marked text excluded.
An observed replacement or in-place change during stable artifact acquisition
returns `code: "artifact_changed"`, `category: "integrity_mismatch"` and
`retryable: true`, retaining the selected path and reader's reason. Wait until
the capture file is stable before retrying. No unstable snapshot is decoded.
Command cleanup failures retain the original status and both diagnostic streams;
the decoder's ownership preparation receives the same cancellation signal.
MCP advertises the same strict named input contract used by the application. Historical inspection delegates argument validation to that shared application boundary so accepted explicit-sensitive declarations also cover invalid argument names and other correction details. The SDK receives raw arguments through its Standard Schema interface; malformed inputs still fail before decoder or filesystem effects and return the normal structured REA error. Protocol-envelope validation remains owned by MCP.
+3 -1
View File
@@ -20,7 +20,9 @@ Results retain:
- The selected target, frame and document loader identity; local armed/end timestamps and separate backend monotonic coverage timestamps.
- Every admitted producer function/block range, exclusive end offset and execution count in UTF-16 code units, with verified bounds when exact text is available. Nested/overlapping counts are preserved and must not be summed.
- Main-document script inventory, exact session script IDs, resource start coordinates, sourceURL/source-map declarations, producer hash and independently computed UTF-8 text digest.
- Armed-window requests from the selected main frame and selected origins, complete reported initiator objects (including stack hierarchy/descriptions), derived callsites, exact script-ID associations and unresolved async parent IDs. Source association does not establish UI causality.
- Armed-window requests from the selected main frame and selected origins, complete reported initiator objects (including stack hierarchy/descriptions), derived callsites, exact script-ID associations and unresolved async parent IDs. Source association does not establish UI causality. Callsite and listener `line_number`/`column_number` values are CDP's zero-based lines and UTF-16 columns in the resource named by `url`. With a `script_id`, that is the script's enclosing resource: the script file for an external script, or the HTML document for an inline script. `resource_start` is where the script text starts in that resource. A callsite with an empty `script_id` comes from the request's reported initiator rather than a script call frame, such as markup that the parser read in a document or a module import in a script file. It still locates a position in `url`, but has no session-script association, so it cannot be joined to captured source or `resource_start`. `trace_web_source_location` takes one-based, script-relative lines and zero-based UTF-16 columns.
For an external script, where `url` is the script file, use `line = line_number + 1` and keep `column = column_number`; this also applies to an empty-`script_id` initiator position in a script file. For an inline script, which requires a `script_id` and its `resource_start`, use `line = line_number - resource_start.line_number + 1`. Subtract `resource_start.column_number` from `column_number` only when `line_number === resource_start.line_number`; keep the reported column on later lines. Columns remain zero-based. This conversion requires known source-start and column coordinates; preserve unavailable coordinates as unknown.
The coverage counter interval starts at backend acceptance before the armed receipt and ends at the resetting sample after the local timer. Counts may include execution during these command intervals; request events are restricted to the locally armed window. The result retains both backend timestamps and the distinct local clock.
+16
View File
@@ -84,6 +84,22 @@ are made.
## Verification
Browser bundle collection uses absolute section offsets for nested inline maps.
An enclosing column offset contributes only when the nested section starts on
that enclosing section's first generated line. Parent boundaries still clip
preceding browser mappings when a nested section starts with a gap or has no
child leaves. Raw map text, original source identities and the dedicated decoder
profile are preserved.
Browser collection accepts up to 64 MiB of map responses per inspection and
preflights indexed sections, sources, names, generated rows and mapping segments
against a 262144-record decode budget before upstream flattening. Indexed maps
are limited to 64 nested levels. These structural limits bound decoded object
growth while preserving large embedded `sourcesContent` strings within the
response-byte budget; exceeding a decoder limit returns a `fetch_failed` item.
Malformed UTF-8 map responses are reported as invalid rather than repaired with
replacement characters.
Focused tests cover duplicate mappings, indexed offsets/overlap/local indexes,
32-bit VLQ overflow, nullable content, UTF-16/CRLF positions, immutable artifact
identity, cancellation, late output closure, cleanup failures, SDK schemas and
+6 -2
View File
@@ -3,6 +3,8 @@
This guide describes repository main. npm 4.1.0 includes the Windows
native bundle; check the [release boundary](installation.md#released-package-and-main)
before applying these instructions to an older published package.
Native x86 (32-bit) PE support is available on repository main; npm releases
through 5.0.0 admit x86-64 PE targets on Windows.
Status: experimental Windows x64 support for the read-only P0 boundary. Windows
package builds bundle native process, filesystem, and DACL controls. REA
@@ -15,7 +17,7 @@ additional permission flag or degraded mode is required.
- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+;
- an operator-installed Ghidra 12.1.x distribution (verified with 12.1.4);
- a 64-bit full JDK inside that installation's declared Java range (JDK 21 or newer, with no maximum, for current 12.1 releases);
- an explicit native, non-managed, non-DLL x86-64 PE application; and
- an explicit native, non-managed, non-DLL x86 or x86-64 PE application; and
- the 25 read-only Ghidra inventory, memory, and function-analysis operations.
Loaded memory reads and file offsets preserve Ghidra's observed source mappings.
@@ -98,7 +100,7 @@ and artifact SHA-256 before loading the package-owned addon. Native failure
reasons remain distinct from a valid installation and an unsupported target.
PE header classification and SHA-256 come from the same open file. The Windows
provider admits native x86-64 PE applications and rejects unsupported roles,
provider admits native x86 and x86-64 PE applications and rejects unsupported roles,
architectures, managed images, and malformed headers. The original selected
source coordinate is preserved for native admission.
@@ -156,6 +158,8 @@ artifact and manifest.
```powershell
npm run verify:windows-native
npm run verify:ghidra:windows
npm run verify:ghidra:windows -- --x86
npm run verify:ghidra:windows:package -- --x86
npm run verify:ghidra:windows:package -- C:\fixtures\installed-rea C:\fixtures\sample.exe
```
+1 -1
View File
@@ -107,7 +107,7 @@ if [[ "$dry_run" == true ]]; then
fi
printf 'Installing %s@%s...\n' "$PACKAGE" "$version"
npm install --global "${prefix_args[@]}" "$PACKAGE@$version" || fail "npm could not install REA. Check registry access and npm permissions, then retry."
npm install --global ${prefix_args[@]+"${prefix_args[@]}"} "$PACKAGE@$version" || fail "npm could not install REA. Check registry access and npm permissions, then retry."
[[ -x "$install_bin" ]] || fail "npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry."
installed_version="$("$install_bin" --version 2>/dev/null | tr -d '[:space:]')" || fail "the installed REA version could not be read. Reinstall the requested version, then retry."
[[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version. Reinstall the requested version, then retry."
+6 -1
View File
@@ -1,6 +1,11 @@
{
"$schema": "./node_modules/knip/schema.json",
"entry": ["src/main.ts", "src/cli.ts", "src/contracts/errorSchemas.ts"],
"entry": [
"src/main.ts",
"src/cli.ts",
"src/contracts/errorSchemas.ts",
"tests/processOwnershipRunner.ts"
],
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
"vitepress": {
"entry": ["docs/.vitepress/config.ts"]
+27 -5
View File
@@ -277,14 +277,15 @@ static void directory(Runtime& root, const std::wstring& relative) {
}
}
static Handle createPrivateFile(Runtime& root, const std::wstring& relative) {
static Handle createPrivateFile(Runtime& root, const std::wstring& relative,
DWORD access = DELETE | GENERIC_WRITE | GENERIC_READ | READ_CONTROL) {
const auto path = relativePath(root, relative);
const auto parentPath = path.substr(0, path.find_last_of(L'\\'));
auto parent = runtimeDirectory(root, parentPath);
verifyPrivate(parent->get(), parentPath, parentPath == root.path);
Security security;
auto file = createRelative(parent->get(), path.substr(path.find_last_of(L'\\') + 1),
DELETE | GENERIC_WRITE | GENERIC_READ | READ_CONTROL, false, security, path);
access, false, security, path);
verifyPrivate(file.get(), path, false);
return file;
}
@@ -305,6 +306,26 @@ static void dispose(HANDLE handle, const std::wstring& path) {
require(SetFileInformationByHandle(handle, FileDispositionInfo, &deletion, sizeof(deletion)), "Delete owned object by handle failed", path);
}
static void disposeSnapshot(Runtime& root, Handle& file, const std::wstring& path) {
// Snapshot readers do not share DELETE access. Release the writer only for
// failed-copy cleanup, with its parent pinned and its identity recorded.
auto parent = runtimeDirectory(root, path.substr(0, path.find_last_of(L'\\')));
FILE_ID_INFO expected{};
require(GetFileInformationByHandleEx(file.get(), FileIdInfo, &expected, sizeof(expected)),
"Read incomplete snapshot identity failed", path);
file.reset();
auto cleanup = openComponent(path, DELETE | GENERIC_READ | FILE_WRITE_ATTRIBUTES, false);
FILE_ID_INFO observed{};
require(GetFileInformationByHandleEx(cleanup.get(), FileIdInfo, &observed, sizeof(observed)),
"Read snapshot cleanup identity failed", path);
require(expected.VolumeSerialNumber == observed.VolumeSerialNumber &&
std::equal(std::begin(expected.FileId.Identifier), std::end(expected.FileId.Identifier),
std::begin(observed.FileId.Identifier)),
"Incomplete snapshot identity changed before cleanup", path, ERROR_ACCESS_DENIED);
verifyPrivate(cleanup.get(), path, false);
dispose(cleanup.get(), path);
}
void closeRuntime(Runtime& root) {
if (root.closed) return;
// Windows keeps the bearer descriptor and snapshot leases until this owner
@@ -414,8 +435,7 @@ static void completeSnapshot(napi_env env, napi_status status, void* pointer) {
work->root->snapshotPending = false;
const auto reject = [&](const Failure& failure) {
try {
dispose(work->destination.get(), work->path);
work->destination.reset();
disposeSnapshot(*work->root, work->destination, work->path);
napi_reject_deferred(env, work->deferred, failureValue(env, failure));
} catch (const Failure& cleanupFailure) {
const Failure combined(
@@ -447,7 +467,9 @@ static napi_value snapshot(napi_env env, napi_value rootValue, Runtime& root,
auto work = std::make_unique<SnapshotWork>(env, root);
work->source = openFile(sourcePath);
work->path = relativePath(root, relative);
work->destination = createPrivateFile(root, relative);
// Retain the write lease with read-only sharing, but omit DELETE authority:
// Java's RandomAccessFile reader shares read/write, not delete access.
work->destination = createPrivateFile(root, relative, GENERIC_WRITE | GENERIC_READ | READ_CONTROL);
napi_value promise;
check(napi_create_promise(env, &work->deferred, &promise));
check(napi_create_reference(env, rootValue, 1, &work->rootReference));
+80 -12
View File
@@ -1,12 +1,12 @@
{
"name": "rea-agents",
"version": "5.0.0",
"version": "6.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rea-agents",
"version": "5.0.0",
"version": "6.0.0",
"license": "MIT",
"dependencies": {
"@babel/parser": "8.0.4",
@@ -19,14 +19,20 @@
"@modelcontextprotocol/server": "2.3.1",
"@xterm/addon-serialize": "0.14.0",
"@xterm/headless": "6.0.0",
"@zip.js/zip.js": "2.8.26",
"@zip.js/zip.js": "2.15.0",
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"canonicalize": "3.0.0",
"cross-spawn": "7.0.6",
"diff": "9.0.0",
"evmole": "0.9.3",
"har-schema": "2.0.0",
"ignore": "7.0.6",
"incur": "0.6.1",
"isomorphic-git": "1.38.7",
"jsonc-parser": "3.3.1",
"lossless-json": "4.3.1",
"parse5": "8.0.1",
"pino": "10.4.0",
"playwright-core": "1.58.2",
"plist": "5.0.0",
@@ -48,7 +54,6 @@
"@types/write-file-atomic": "4.0.3",
"@types/ws": "8.18.1",
"@vitest/coverage-v8": "5.0.3",
"ajv": "8.20.0",
"husky": "9.1.7",
"jscpd": "5.0.12",
"knip": "6.39.0",
@@ -3945,9 +3950,9 @@
]
},
"node_modules/@zip.js/zip.js": {
"version": "2.8.26",
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.26.tgz",
"integrity": "sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA==",
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.15.0.tgz",
"integrity": "sha512-hYAuHAaWjt0axbofaDL5XUlmrQPsBDcK3f45ApZuh6N+8UGVk26GoGdtGDvcvzahsHytTQ6DtBVXwG9IcCMjjQ==",
"license": "BSD-3-Clause",
"engines": {
"bun": ">=0.7.0",
@@ -3971,7 +3976,6 @@
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
@@ -3984,6 +3988,23 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/algoliasearch": {
"version": "5.59.0",
"resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.59.0.tgz",
@@ -4623,6 +4644,18 @@
"node": ">=18.0.0"
}
},
"node_modules/evmole": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/evmole/-/evmole-0.9.3.tgz",
"integrity": "sha512-UhO5HMAoEPCupSxvBJPlm/L0Qk/KVJ/OJo3r4EyhXADIEHqU5aTkAzsBfzk4lXGjqxVnaYbh1GPSmZtmSHFnQQ==",
"license": "MIT",
"bin": {
"evmole": "dist/evmole_cli.mjs"
},
"engines": {
"node": ">=22"
}
},
"node_modules/expect-type": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
@@ -4660,7 +4693,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-string-truncated-width": {
@@ -4682,7 +4714,6 @@
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
"integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
"dev": true,
"funding": [
{
"type": "github",
@@ -4890,6 +4921,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
"integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==",
"license": "ISC",
"engines": {
"node": ">=4"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
@@ -5315,7 +5355,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/jsonc-parser": {
@@ -5743,6 +5782,12 @@
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/lossless-json": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-4.3.1.tgz",
"integrity": "sha512-SqD/Bg3ZfltBJ2Z14hJ/BihnvtV553WO4g9/ePtlp4lrnl9jF3AdIJt53A/Wkg/0Li+LMfxaBqgx1MiFZdQlpQ==",
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "11.5.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
@@ -6308,6 +6353,30 @@
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parse5": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
"integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
"license": "MIT",
"dependencies": {
"entities": "^8.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5/node_modules/entities": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",
"integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -6609,7 +6678,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
+41 -9
View File
@@ -1,7 +1,7 @@
{
"name": "rea-agents",
"mcpName": "io.github.morluto/rea",
"version": "5.0.0",
"version": "6.0.0",
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
"license": "MIT",
"repository": {
@@ -44,10 +44,14 @@
"bridge/hopper_bridge.py",
"bridge/native/NativeUIChildren.swift",
"bridge/native/ReaNativeUI.swift",
"bridge/native/rea_lldb_tracer.py",
"bridge/process/ProcessRunTokenReader.swift",
"bridge/process/ProcessRunTokenReaderMain.swift",
"bridge/ghidra/ReaGhidraBridge.java",
"bridge/ghidra/ReaGhidraExtensions.java",
"bridge/ghidra/ReaGhidraPrepareCom.java",
"bridge/android/ReaJadxBridge.java",
"bridge/mitmproxy/capture.py",
"scripts/rea.mjs",
"scripts/electron-active-hook.cjs",
"scripts/electron-active-hook-boundaries.cjs",
@@ -59,18 +63,31 @@
"LICENSE",
"native/windows/build/manifest.json",
"native/windows/build/rea-windows-x64.node",
"scripts/verify-windows-native-artifact.mjs"
"scripts/verify-windows-native-artifact.mjs",
"bridge/pwntools/layout.py",
"bridge/pwntools/layout_impl.py",
"third_party/pwntools/README.md",
"third_party/evmole/README.md",
"third_party/evmole/LICENSE",
"bridge/pwntools/decoder_runtime.py",
"bridge/pwntools/recorded_crash.py",
"bridge/pwntools/recorded_crash_impl.py",
"bridge/pwndbg/launch.py",
"bridge/pwndbg/launch_impl.py",
"bridge/pwndbg/core_context.py",
"third_party/pwndbg/README.md",
"third_party/pwndbg/LICENSE.md"
],
"scripts": {
"deps:check": "node scripts/check-dependency-install.mjs",
"metadata:generate": "node scripts/generate-package-metadata.mjs",
"metadata:check": "node scripts/generate-package-metadata.mjs --check",
"mcp-registry:check": "node scripts/verify-mcp-registry-metadata.mjs",
"mcp-catalog:generate": "npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs",
"mcp-catalog:generate": "npm run build:cached",
"mcp-catalog:check": "node scripts/generate-mcp-tool-catalog.mjs --check",
"prebuild": "npm run deps:check && npm run metadata:check",
"build": "node scripts/run-exclusive.mjs build npm run build:unlocked",
"build:unlocked": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json",
"build:unlocked": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs",
"precheck": "npm run deps:check",
"check": "turbo run typecheck lint format:check knip metadata:check",
"precheck:fast": "npm run deps:check",
@@ -121,6 +138,8 @@
"typecheck": "tsc --noEmit",
"verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs",
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
"verify:hopper:fat": "npm run build:cached && node scripts/verify-real-hopper-fat.mjs",
"verify:hopper:fat64": "npm run build:cached && node scripts/verify-real-hopper-fat.mjs --fat64",
"verify:apple-dispatch": "npm run build:cached && node scripts/verify/apple/dispatch.mjs",
"verify:native-ui": "npm run build:cached && node scripts/verify-native-ui.mjs",
"verify:keyed-archive": "npm run build:cached && node scripts/verify/apple/keyed-archive.mjs",
@@ -141,6 +160,7 @@
"verify:inspector": "npm run build:cached && node scripts/verify/inspector/runtime-observation.mjs",
"verify:javascript-runtime": "npm run build:cached && node scripts/verify/javascript/runtime-observation.mjs",
"verify:javascript:digests": "npm run build:cached && node scripts/verify/javascript/digests.mjs",
"verify:javascript:output": "npm run build:cached && node scripts/verify/javascript/output.mjs",
"verify:application-workflows": "npm run build:cached && node scripts/verify/javascript/application-workflows.mjs",
"verify:managed": "npm run build:cached && node scripts/verify/managed/conformance.mjs",
"verify:firmware": "npm run build:cached && node scripts/verify/firmware/analysis.mjs",
@@ -161,17 +181,20 @@
"config:print": "node scripts/print-mcp-config.mjs",
"evidence:generate": "npm run build:cached && node scripts/generate-completion-ledger.mjs",
"evidence:check": "npm run build:cached && node scripts/generate-completion-ledger.mjs --check",
"predocs:dev": "npm run docs:generate",
"docs:dev": "vitepress dev docs",
"predocs:build": "npm run docs:generate",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs",
"docs:generate": "node scripts/run-exclusive.mjs docs npm run docs:generate:unlocked",
"docs:generate:unlocked": "npm run metadata:generate && npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs && node scripts/generate-error-schema.mjs",
"docs:generate:unlocked": "npm run metadata:generate && npm run build:cached && node scripts/generate-error-schema.mjs",
"docs:check": "node scripts/run-exclusive.mjs docs turbo run docs:check:built",
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-mcp-tool-catalog.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check",
"docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs",
"prepack": "node scripts/prepack.mjs",
"prepare": "node scripts/prepare.mjs",
"verify:asset-catalog": "npm run build:cached && node scripts/verify/apple/asset-catalog.mjs",
"verify:native-calls": "npm run build:cached && node scripts/verify-native-calls.mjs",
"verify:native-ui:permissions": "npm run build:cached && node scripts/verify-native-ui.mjs --allow-permission-denial",
"test:focused": "node scripts/run-development-tests.mjs focused",
"build:windows-native": "node scripts/build-windows-native.mjs",
@@ -188,7 +211,11 @@
"verify:javascript:recovery": "node scripts/verify/javascript/recovery.mjs",
"verify:module-boundaries": "node scripts/verify-module-boundaries.mjs",
"verify:browser:source-maps": "npm run build:cached && node scripts/verify-browser-source-maps.mjs",
"verify:browser:runtime": "npm run build:cached && node scripts/verify-browser-runtime.mjs"
"verify:browser:runtime": "npm run build:cached && node scripts/verify-browser-runtime.mjs",
"verify:web:network-captures": "npm run build:cached && node scripts/verify-web-network-captures.mjs",
"verify:binary:layout": "npm run build:cached && node scripts/verify-binary-layout.mjs",
"verify:evm:interface": "npm run build:cached && node scripts/verify-evm-interface.mjs",
"verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs"
},
"lint-staged": {
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
@@ -208,14 +235,19 @@
"@modelcontextprotocol/server": "2.3.1",
"@xterm/addon-serialize": "0.14.0",
"@xterm/headless": "6.0.0",
"@zip.js/zip.js": "2.8.26",
"@zip.js/zip.js": "2.15.0",
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"canonicalize": "3.0.0",
"cross-spawn": "7.0.6",
"diff": "9.0.0",
"har-schema": "2.0.0",
"ignore": "7.0.6",
"incur": "0.6.1",
"isomorphic-git": "1.38.7",
"jsonc-parser": "3.3.1",
"lossless-json": "4.3.1",
"parse5": "8.0.1",
"pino": "10.4.0",
"playwright-core": "1.58.2",
"plist": "5.0.0",
@@ -223,7 +255,8 @@
"smol-toml": "1.9.0",
"write-file-atomic": "7.0.0",
"ws": "8.21.0",
"zod": "4.4.3"
"zod": "4.4.3",
"evmole": "0.9.3"
},
"overrides": {
"vitepress": {
@@ -238,7 +271,6 @@
"@types/write-file-atomic": "4.0.3",
"@types/ws": "8.18.1",
"@vitest/coverage-v8": "5.0.3",
"ajv": "8.20.0",
"husky": "9.1.7",
"jscpd": "5.0.12",
"knip": "6.39.0",
+1 -6
View File
@@ -1,6 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"release-type": "node",
"versioning": "always-bump-minor",
"include-v-in-tag": false,
"changelog-sections": [
{ "type": "feat", "section": "Features", "hidden": false },
@@ -16,7 +17,6 @@
".": {
"package-name": "rea-agents",
"extra-files": [
{ "type": "generic", "path": "README.md" },
{ "type": "generic", "path": "README_zh.md" },
{ "type": "generic", "path": "README_ja.md" },
{ "type": "generic", "path": "README_ko.md" },
@@ -35,11 +35,6 @@
"type": "json",
"path": "server.json",
"jsonpath": "$.packages[0].version"
},
{
"type": "json",
"path": "docs/product-catalog.json",
"jsonpath": "$.package.version"
}
]
}
+60 -42
View File
@@ -3,50 +3,68 @@ import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const output = resolve(root, "build", "fixtures", "rea-ghidra-windows.exe");
const bytes = Buffer.alloc(1024);
const peOffset = 0x80;
const optionalHeader = peOffset + 24;
const sectionTable = optionalHeader + 0xf0;
for (const architecture of ["x86_64", "x86"]) {
const x86 = architecture === "x86";
const output = resolve(
root,
"build",
"fixtures",
x86 ? "rea-ghidra-windows-x86.exe" : "rea-ghidra-windows.exe",
);
const bytes = Buffer.alloc(1024);
const peOffset = 0x80;
const optionalHeader = peOffset + 24;
const optionalHeaderSize = x86 ? 0xe0 : 0xf0;
const sectionTable = optionalHeader + optionalHeaderSize;
bytes.write("MZ", 0, "ascii");
bytes.writeUInt32LE(peOffset, 0x3c);
bytes.write("PE\0\0", peOffset, "binary");
bytes.writeUInt16LE(0x8664, peOffset + 4);
bytes.writeUInt16LE(1, peOffset + 6);
bytes.writeUInt16LE(0xf0, peOffset + 20);
bytes.writeUInt16LE(0x22, peOffset + 22);
bytes.write("MZ", 0, "ascii");
bytes.writeUInt32LE(peOffset, 0x3c);
bytes.write("PE\0\0", peOffset, "binary");
bytes.writeUInt16LE(x86 ? 0x14c : 0x8664, peOffset + 4);
bytes.writeUInt16LE(1, peOffset + 6);
bytes.writeUInt16LE(optionalHeaderSize, peOffset + 20);
bytes.writeUInt16LE(x86 ? 0x102 : 0x22, peOffset + 22);
bytes.writeUInt16LE(0x20b, optionalHeader);
bytes.writeUInt32LE(0x200, optionalHeader + 4);
bytes.writeUInt32LE(0x1000, optionalHeader + 16);
bytes.writeUInt32LE(0x1000, optionalHeader + 20);
bytes.writeBigUInt64LE(0x1_4000_0000n, optionalHeader + 24);
bytes.writeUInt32LE(0x1000, optionalHeader + 32);
bytes.writeUInt32LE(0x200, optionalHeader + 36);
bytes.writeUInt16LE(6, optionalHeader + 40);
bytes.writeUInt16LE(6, optionalHeader + 48);
bytes.writeUInt32LE(0x2000, optionalHeader + 56);
bytes.writeUInt32LE(0x200, optionalHeader + 60);
bytes.writeUInt16LE(3, optionalHeader + 68);
bytes.writeUInt16LE(0x8160, optionalHeader + 70);
bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 72);
bytes.writeBigUInt64LE(0x1000n, optionalHeader + 80);
bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 88);
bytes.writeBigUInt64LE(0x1000n, optionalHeader + 96);
bytes.writeUInt32LE(16, optionalHeader + 108);
bytes.writeUInt16LE(x86 ? 0x10b : 0x20b, optionalHeader);
bytes.writeUInt32LE(0x200, optionalHeader + 4);
bytes.writeUInt32LE(0x1000, optionalHeader + 16);
bytes.writeUInt32LE(0x1000, optionalHeader + 20);
if (x86) bytes.writeUInt32LE(0x400000, optionalHeader + 28);
else bytes.writeBigUInt64LE(0x1_4000_0000n, optionalHeader + 24);
bytes.writeUInt32LE(0x1000, optionalHeader + 32);
bytes.writeUInt32LE(0x200, optionalHeader + 36);
bytes.writeUInt16LE(6, optionalHeader + 40);
bytes.writeUInt16LE(6, optionalHeader + 48);
bytes.writeUInt32LE(0x2000, optionalHeader + 56);
bytes.writeUInt32LE(0x200, optionalHeader + 60);
bytes.writeUInt16LE(3, optionalHeader + 68);
bytes.writeUInt16LE(x86 ? 0x8140 : 0x8160, optionalHeader + 70);
if (x86) {
bytes.writeUInt32LE(0x10_0000, optionalHeader + 72);
bytes.writeUInt32LE(0x1000, optionalHeader + 76);
bytes.writeUInt32LE(0x10_0000, optionalHeader + 80);
bytes.writeUInt32LE(0x1000, optionalHeader + 84);
bytes.writeUInt32LE(16, optionalHeader + 92);
} else {
bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 72);
bytes.writeBigUInt64LE(0x1000n, optionalHeader + 80);
bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 88);
bytes.writeBigUInt64LE(0x1000n, optionalHeader + 96);
bytes.writeUInt32LE(16, optionalHeader + 108);
}
bytes.write(".text", sectionTable, "ascii");
bytes.writeUInt32LE(0x16, sectionTable + 8);
bytes.writeUInt32LE(0x1000, sectionTable + 12);
bytes.writeUInt32LE(0x200, sectionTable + 16);
bytes.writeUInt32LE(0x200, sectionTable + 20);
bytes.writeUInt32LE(0x6000_0020, sectionTable + 36);
bytes.write(".text", sectionTable, "ascii");
bytes.writeUInt32LE(0x16, sectionTable + 8);
bytes.writeUInt32LE(0x1000, sectionTable + 12);
bytes.writeUInt32LE(0x200, sectionTable + 16);
bytes.writeUInt32LE(0x200, sectionTable + 20);
bytes.writeUInt32LE(0x6000_0020, sectionTable + 36);
// entry calls the second function, returns zero; second returns 42.
bytes.set([0xe8, 0x0b, 0, 0, 0, 0x31, 0xc0, 0xc3], 0x200);
bytes.set([0xb8, 0x2a, 0, 0, 0, 0xc3], 0x210);
// entry calls the second function, returns zero; second returns 42.
bytes.set([0xe8, 0x0b, 0, 0, 0, 0x31, 0xc0, 0xc3], 0x200);
bytes.set([0xb8, 0x2a, 0, 0, 0, 0xc3], 0x210);
await mkdir(dirname(output), { recursive: true });
await writeFile(output, bytes);
process.stdout.write(`${output}\n`);
await mkdir(dirname(output), { recursive: true });
await writeFile(output, bytes);
if (!x86) process.stdout.write(`${output}\n`);
}
+14 -18
View File
@@ -61,7 +61,7 @@ const createElectronActiveBoundaryPatches = ({
const target = typeof args[0] === "string" ? args[0] : null;
recordRuntime("preload", "context-bridge-exposure", "attempted", {
target,
argument_shapes: args.map((value) => shape(value)),
argument_shape_values: args,
process_type: processType(),
});
try {
@@ -110,7 +110,7 @@ const createElectronActiveBoundaryPatches = ({
event === "will-download" ? "blocked" : "observed",
{
target: identity(args[0], "webContents"),
argument_shapes: args.map((value) => shape(value)),
argument_shape_values: args,
error: event === "will-download",
},
);
@@ -124,7 +124,7 @@ const createElectronActiveBoundaryPatches = ({
(webContents, permission, callback) => {
recordRuntime("permission", "permission-request", "blocked", {
target: identity(webContents, "webContents"),
argument_shapes: [shape(permission)],
argument_shape_values: [permission],
error: true,
});
if (typeof callback === "function") callback(false);
@@ -137,7 +137,7 @@ const createElectronActiveBoundaryPatches = ({
return originalPermissionCheck.call(this, (webContents, permission) => {
recordRuntime("permission", "permission-check", "blocked", {
target: identity(webContents, "webContents"),
argument_shapes: [shape(permission)],
argument_shape_values: [permission],
error: true,
});
return false;
@@ -148,7 +148,7 @@ const createElectronActiveBoundaryPatches = ({
session.setCertificateVerifyProc = function blockedCertificate() {
return originalCertificate.call(this, (request, callback) => {
recordRuntime("permission", "certificate-error", "blocked", {
argument_shapes: [shape(request)],
argument_shape_values: [request],
error: true,
});
if (typeof callback === "function") callback(-2);
@@ -168,14 +168,14 @@ const createElectronActiveBoundaryPatches = ({
} catch {
recordRuntime("popup-attempt", "window-open-handler", "failed", {
target: contentsId,
argument_shapes: args.map((value) => shape(value)),
argument_shape_values: args,
error: true,
});
return { action: "deny" };
}
recordRuntime("popup-attempt", "window-open-handler", "blocked", {
target: contentsId,
argument_shapes: args.map((value) => shape(value)),
argument_shape_values: args,
error: true,
});
return { action: "deny" };
@@ -223,7 +223,7 @@ const createElectronActiveBoundaryPatches = ({
method === "relaunch" ? "updater" : "protocol",
method,
"blocked",
{ argument_shapes: args.map((value) => shape(value)), error: true },
{ argument_shape_values: args, error: true },
);
return method === "setAsDefaultProtocolClient" ? false : undefined;
};
@@ -250,11 +250,7 @@ const createElectronActiveBoundaryPatches = ({
recordRuntime("native-addon", "dlopen", "attempted", {
artifact_path: typeof filename === "string" ? filename : null,
artifact_sha256: digest,
argument_shapes: [
shape(module),
shape(filename),
...args.map((value) => shape(value)),
],
argument_shape_value_groups: [[module, filename], args],
});
try {
const result = original.call(this, module, filename, ...args);
@@ -280,7 +276,7 @@ const createElectronActiveBoundaryPatches = ({
if (typeof original !== "function") return;
utilityProcess.fork = function patchedFork(...args) {
recordRuntime("process-lifecycle", "utility-process-fork", "attempted", {
argument_shapes: args.map((value) => shape(value)),
argument_shape_values: args,
process_type: "main",
});
const child = original.apply(this, args);
@@ -304,7 +300,7 @@ const createElectronActiveBoundaryPatches = ({
recordRuntime("process-lifecycle", `utility.${event}`, "observed", {
target: childId,
process_type: "utility",
argument_shapes: eventArgs.map((value) => shape(value)),
argument_shape_values: eventArgs,
});
});
if (child && typeof child.postMessage === "function") {
@@ -344,7 +340,7 @@ const createElectronActiveBoundaryPatches = ({
if (typeof original !== "function") continue;
shell[method] = function blockedShellCall(...args) {
recordRuntime("shell-attempt", method, "blocked", {
argument_shapes: args.map((value) => shape(value)),
argument_shape_values: args,
error: true,
});
return Promise.reject(new Error("External shell effects are blocked"));
@@ -359,7 +355,7 @@ const createElectronActiveBoundaryPatches = ({
if (typeof original !== "function") continue;
childProcess[method] = function patchedChildProcess(...args) {
recordRuntime("process-lifecycle", `child.${method}`, "attempted", {
argument_shapes: args.map((value) => shape(value)),
argument_shape_values: args,
});
const child = original.apply(this, args);
const childId = identity(child, "child");
@@ -369,7 +365,7 @@ const createElectronActiveBoundaryPatches = ({
patchEmitter(child, (event, eventArgs) =>
recordRuntime("process-lifecycle", `child.${event}`, "observed", {
target: childId,
argument_shapes: eventArgs.map((value) => shape(value)),
argument_shape_values: eventArgs,
}),
);
return child;
+199 -17
View File
@@ -16,9 +16,19 @@ const ipcEventKinds = new Set([
"ipc-renderer-post-message",
]);
const events = [];
// Budget serialized data and estimated retained object/array storage together.
const retentionBudgetBytes = 16 * 1024 * 1024;
const snapshotEnvelopeReserveBytes = 1024;
let estimatedRetainedBytes = 0;
let serializedBytes = 0;
let observed = 0;
let observedIpc = 0;
let observedRuntime = 0;
let dropped = 0;
let droppedIpc = 0;
let droppedRuntime = 0;
const droppedFamilies = new Map();
const droppedRoles = new Map();
let sequence = 0;
let correlationSequence = 0;
@@ -43,6 +53,73 @@ const shape = (value, depth = 0) => {
}
};
// Measure JSON strings without allocating an encoded copy of a caller value.
const jsonStringBytes = (value) => {
let bytes = 2;
for (let index = 0; index < value.length; index += 1) {
const code = value.charCodeAt(index);
if (code === 0x22 || code === 0x5c) bytes += 2;
else if (
code === 0x08 ||
code === 0x09 ||
code === 0x0a ||
code === 0x0c ||
code === 0x0d
)
bytes += 2;
else if (code <= 0x1f) bytes += 6;
else if (code >= 0xd800 && code <= 0xdbff) {
const next = value.charCodeAt(index + 1);
if (next >= 0xdc00 && next <= 0xdfff) {
bytes += 4;
index += 1;
} else bytes += 6;
} else if (code >= 0xdc00 && code <= 0xdfff) bytes += 6;
else if (code <= 0x7f) bytes += 1;
else if (code <= 0x7ff) bytes += 2;
else bytes += 3;
if (bytes > retentionBudgetBytes) return bytes;
}
return bytes;
};
const jsonScalarBytes = (value) => {
if (value === null) return 4;
if (typeof value === "string") return jsonStringBytes(value);
if (typeof value === "boolean") return value ? 4 : 5;
return String(value).length;
};
const eventFamily = (kind, processTypeValue) => {
if (kind === "preload" && processTypeValue === "main")
return "preload-configuration";
if (kind.startsWith("ipc-renderer"))
return processTypeValue === "renderer" ? "renderer-ipc" : "ipc";
return ipcEventKinds.has(kind) ? "ipc" : kind;
};
const noteDropped = (kind, processTypeValue, isIpc) => {
dropped += 1;
if (isIpc) droppedIpc += 1;
else droppedRuntime += 1;
const family = eventFamily(kind, processTypeValue);
droppedFamilies.set(family, (droppedFamilies.get(family) ?? 0) + 1);
if (typeof processTypeValue === "string")
droppedRoles.set(
processTypeValue,
(droppedRoles.get(processTypeValue) ?? 0) + 1,
);
if (kind === "window-lifecycle")
droppedRoles.set("window", (droppedRoles.get("window") ?? 0) + 1);
if (kind === "web-contents-lifecycle" || kind === "navigation")
droppedRoles.set(
"web_contents",
(droppedRoles.get("web_contents") ?? 0) + 1,
);
if (kind === "preload" && processTypeValue === "preload")
droppedRoles.set("preload", (droppedRoles.get("preload") ?? 0) + 1);
};
const isAllowedNavigation = (value) => {
if (typeof value !== "string") return true;
try {
@@ -83,7 +160,8 @@ const frameIdentity = (event) => {
const record = (event) => {
observed += 1;
if (ipcEventKinds.has(event.kind)) observedIpc += 1;
const isIpc = ipcEventKinds.has(event.kind);
if (isIpc) observedIpc += 1;
else observedRuntime += 1;
const raw = {
sequence: ++sequence,
@@ -109,10 +187,15 @@ const record = (event) => {
error: false,
...event,
};
const argumentShapes = Array.isArray(raw.argument_shapes)
const shapeGroups = Array.isArray(raw.argument_shape_value_groups)
? raw.argument_shape_value_groups.filter(Array.isArray)
: Array.isArray(raw.argument_shape_values)
? [raw.argument_shape_values]
: [];
const existingShapes = Array.isArray(raw.argument_shapes)
? raw.argument_shapes
: [];
events.push({
const retainedEvent = {
...raw,
correlation_id: raw.correlation_id,
event: typeof raw.event === "string" ? raw.event : null,
@@ -121,9 +204,7 @@ const record = (event) => {
receiver: typeof raw.receiver === "string" ? raw.receiver : null,
frame: typeof raw.frame === "string" ? raw.frame : null,
target: typeof raw.target === "string" ? raw.target : null,
argument_shapes: argumentShapes.map((value) =>
typeof value === "string" ? value : "unknown",
),
argument_shapes: [],
result_shape:
typeof raw.result_shape === "string" ? raw.result_shape : null,
process_type:
@@ -132,8 +213,91 @@ const record = (event) => {
typeof raw.artifact_path === "string" ? raw.artifact_path : null,
artifact_sha256:
typeof raw.artifact_sha256 === "string" ? raw.artifact_sha256 : null,
});
return events.at(-1);
};
delete retainedEvent.argument_shape_values;
delete retainedEvent.argument_shape_value_groups;
// All event values are JSON scalars except argument_shapes. Measure them
// incrementally so large channels, paths, or shape lists never require a
// full JSON.stringify temporary allocation before the budget decision.
let eventBytes = 2;
let argumentShapeCount = 0;
const eventKeys = Object.keys(retainedEvent);
for (let index = 0; index < eventKeys.length; index += 1) {
const key = eventKeys[index];
const value = retainedEvent[key];
if (index > 0) eventBytes += 1;
eventBytes += jsonStringBytes(key) + 1;
if (key === "argument_shapes") {
eventBytes += 2;
let shapeOverflow = false;
const countShape = (argument) => {
const encodedShapeBytes = jsonStringBytes(shape(argument));
eventBytes += encodedShapeBytes + (argumentShapeCount === 0 ? 0 : 1);
argumentShapeCount += 1;
shapeOverflow =
eventBytes + estimatedRetainedBytes + snapshotEnvelopeReserveBytes >
retentionBudgetBytes;
};
for (const group of shapeGroups)
for (const argument of group) {
countShape(argument);
if (shapeOverflow) break;
}
if (shapeOverflow) {
noteDropped(raw.kind, retainedEvent.process_type, isIpc);
return null;
}
for (const valueShape of existingShapes)
if (!shapeOverflow) {
eventBytes +=
jsonStringBytes(
typeof valueShape === "string" ? valueShape : "unknown",
) + (argumentShapeCount++ === 0 ? 0 : 1);
shapeOverflow =
eventBytes + estimatedRetainedBytes + snapshotEnvelopeReserveBytes >
retentionBudgetBytes;
}
if (shapeOverflow) {
noteDropped(raw.kind, retainedEvent.process_type, isIpc);
return null;
}
} else eventBytes += jsonScalarBytes(value);
if (
eventBytes + estimatedRetainedBytes + snapshotEnvelopeReserveBytes >
retentionBudgetBytes
) {
noteDropped(raw.kind, retainedEvent.process_type, isIpc);
return null;
}
}
// Reserve room for the only fields mutated after recording: result_shape and
// error. The shape vocabulary's longest value is "undefined".
const reservedMutationBytes = retainedEvent.result_shape === null ? 7 : 0;
// Include one byte for the event-array separator and reserve space for the
// snapshot's counters and JSON delimiters.
// One retained event, one defensive snapshot copy, and up to two serialized
// result views (timeline plus IPC), with per-object and per-array-slot costs.
const eventEstimateBytes = eventBytes * 3 + 512 + argumentShapeCount * 16;
const retainedEventBytes = eventEstimateBytes + reservedMutationBytes + 1;
if (
estimatedRetainedBytes + retainedEventBytes + snapshotEnvelopeReserveBytes >
retentionBudgetBytes
) {
noteDropped(raw.kind, retainedEvent.process_type, isIpc);
return null;
}
for (const group of shapeGroups)
for (const argument of group)
retainedEvent.argument_shapes.push(shape(argument));
for (const valueShape of existingShapes)
retainedEvent.argument_shapes.push(
typeof valueShape === "string" ? valueShape : "unknown",
);
events.push(retainedEvent);
estimatedRetainedBytes += retainedEventBytes;
serializedBytes += eventBytes + reservedMutationBytes + 1;
return retainedEvent;
};
const recordRuntime = (kind, event, phase, details = {}) =>
@@ -145,7 +309,7 @@ const recordIpc = (kind, channel, args, details = {}) =>
return record({
kind,
channel: typeof channel === "string" ? channel : null,
argument_shapes: values.map((value) => shape(value)),
argument_shape_values: values,
...details,
});
})();
@@ -222,7 +386,7 @@ const patchNavigationMethods = (webContents, contentsId) => {
webContents[method] = function patchedNavigation(...args) {
recordRuntime("navigation", method, "attempted", {
target: contentsId,
argument_shapes: args.map((value) => shape(value)),
argument_shape_values: args,
});
if (method === "loadURL" && !isAllowedNavigation(args[0])) {
recordRuntime("navigation", method, "blocked", {
@@ -354,7 +518,7 @@ const patchWebContents = (webContents, windowId) => {
recordRuntime("navigation", event, "blocked", {
target: contentsId,
sender: windowId,
argument_shapes: args.map((value) => shape(value)),
argument_shape_values: args,
error: true,
});
return;
@@ -363,7 +527,7 @@ const patchWebContents = (webContents, windowId) => {
recordRuntime(kind, event, "observed", {
target: contentsId,
sender: windowId,
argument_shapes: args.map((value) => shape(value)),
argument_shape_values: args,
error: kind === "error",
});
if (event === "did-create-window") {
@@ -388,7 +552,7 @@ const patchBrowserWindow = (electron) => {
const targetId = identity(window, "window");
recordRuntime("window-lifecycle", "created", "completed", {
target: targetId,
argument_shapes: args.map((value) => shape(value)),
argument_shape_values: args,
});
const preload = args[0]?.webPreferences?.preload;
if (typeof preload === "string")
@@ -427,7 +591,7 @@ try {
"observed",
{
target: "app",
argument_shapes: args.map((value) => shape(value)),
argument_shape_values: args,
process_type: "main",
},
),
@@ -444,14 +608,14 @@ try {
boundaries.patchChildProcess();
process.once("uncaughtException", (cause, origin) => {
recordRuntime("process-lifecycle", "uncaught-exception", "observed", {
argument_shapes: [shape(cause), shape(origin)],
argument_shape_values: [cause, origin],
error: true,
});
throw cause;
});
process.once("unhandledRejection", (reason, promise) => {
recordRuntime("process-lifecycle", "unhandled-rejection", "observed", {
argument_shapes: [shape(reason), shape(promise)],
argument_shape_values: [reason, promise],
error: true,
});
throw reason;
@@ -461,7 +625,25 @@ try {
}
globalThis.__reaElectronActiveSnapshot = () => ({
events: events.slice(),
events: events.map((event) => ({
...event,
argument_shapes: [...event.argument_shapes],
})),
retention_budget_bytes: retentionBudgetBytes,
estimated_retained_bytes: estimatedRetainedBytes,
event_serialized_byte_upper_bound: serializedBytes,
retained: events.length,
dropped,
dropped_ipc: droppedIpc,
dropped_runtime: droppedRuntime,
dropped_event_families: [...droppedFamilies].map(([family, count]) => ({
family,
count,
})),
dropped_event_roles: [...droppedRoles].map(([role, count]) => ({
role,
count,
})),
observed,
observed_ipc: observedIpc,
observed_runtime: observedRuntime,
+22 -2
View File
@@ -138,7 +138,11 @@ const char *core_chain(void) { return sibling_value() == 7 ? chain_variant() : "
return join(version, "Core");
};
/** The main executable, with a weak dependency deleted after linking. */
/**
* The main executable. It searches a missing `Overrides` rpath before
* `Frameworks`, weakly links a library deleted after linking, and
* delay-links a library, which the linker encodes as `dylib_use_command`.
*/
const buildMainExecutable = async (root, contents, coreBinary) => {
const gone = join(contents, "Frameworks", "libgone.dylib");
await clang(root, {
@@ -147,22 +151,33 @@ const buildMainExecutable = async (root, contents, coreBinary) => {
output: gone,
flags: ["-dynamiclib", "-install_name", "@rpath/libgone.dylib"],
});
const delayed = join(contents, "Frameworks", "libdelay.dylib");
await clang(root, {
name: "delay",
source: `int delay_value(void) { return 3; }\n`,
output: delayed,
flags: ["-dynamiclib", "-install_name", "@rpath/libdelay.dylib"],
});
const executable = join(contents, "MacOS", "MacFixture");
await clang(root, {
name: "main",
source: `#include <stdio.h>
const char *core_chain(void);
int delay_value(void);
extern int gone_value(void) __attribute__((weak_import));
int main(void) {
if (delay_value() != 3) return 1;
printf("chain=%s weak=%s\\n", core_chain(), gone_value ? "present" : "absent");
return 0;
}
`,
output: executable,
flags: [
"-Wl,-rpath,@executable_path/../Overrides",
"-Wl,-rpath,@executable_path/../Frameworks",
coreBinary,
`-Wl,-weak_library,${gone}`,
`-Wl,-delay_library,${delayed}`,
],
});
await rm(gone);
@@ -191,7 +206,12 @@ const buildNestedBundles = async (root, contents, coreBinary) => {
int main(void) { return core_chain()[0] == 'b'; }
`,
output: join(service, "MacOS", "Svc"),
flags: ["-Wl,-rpath,@executable_path/../../../../Frameworks", coreBinary],
// An absolute rpath outside the bundle precedes the bundled framework.
flags: [
"-Wl,-rpath,/opt/rea-fixture-absent/lib",
"-Wl,-rpath,@executable_path/../../../../Frameworks",
coreBinary,
],
});
await write(
join(service, "Info.plist"),
+10
View File
@@ -0,0 +1,10 @@
.globl _start
.globl absolute_value
.set absolute_value, 0x20000000000007
.bss
.lcomm zero_backing, 8192
.text
_start:
mov $60, %eax
xor %edi, %edi
syscall
+17
View File
@@ -0,0 +1,17 @@
#include <stdio.h>
#include <stdlib.h>
int stored_value = 7;
char zero_backing[8192];
__thread int tls_value;
int common_value;
__attribute__((noinline)) int read_values(const char *text) {
char buffer[32];
snprintf(buffer, sizeof(buffer), "%s", text);
return buffer[0] + stored_value + tls_value + common_value;
}
int main(int argc, char **argv) {
return read_values(argc > 1 ? argv[1] : "source-owned");
}
+31 -8
View File
@@ -4,7 +4,7 @@ import { access } from "node:fs/promises";
import { isAbsolute } from "node:path";
/** Generate a real compiler map and serve only source-owned fixture code. */
export async function startBrowserSourceMapSite() {
export async function startBrowserSourceMapSite(options = {}) {
const command = process.env.REA_WEB_SOURCE_MAP_COMPILER;
if (!command || !isAbsolute(command))
throw new Error(
@@ -19,19 +19,40 @@ export async function startBrowserSourceMapSite() {
const compiled = await compiler
.transform(original, {
loader: "ts",
sourcefile: "../src/fixture.ts?v=7#original",
sourcefile: options.sourcefile ?? "../src/fixture.ts?v=7#original",
sourcemap: "external",
minify: true,
format: "iife",
target: "es2022",
})
.finally(() => compiler.stop());
const prefixes = options.prefixes ?? [];
const generated =
prefixes.join("") +
compiled.code +
(options.sourceMapAnnotation
? "\n//# sourceMappingURL=/maps/app.js.map"
: "");
let indexed = JSON.parse(compiled.map);
for (const prefix of prefixes.toReversed()) {
const offset = textPosition(prefix, prefix.length);
indexed = {
version: 3,
sections: [
{
offset: { line: offset.line - 1, column: offset.column },
map: indexed,
},
],
};
}
const map = prefixes.length === 0 ? compiled.map : JSON.stringify(indexed);
const token = '"source-map proof"';
const generatedOffset = compiled.code.indexOf(token);
const generatedOffset = generated.indexOf(token);
const originalOffset = original.indexOf(token);
if (generatedOffset < 0 || originalOffset < 0)
throw new Error("Compiler fixture marker missing");
const position = textPosition(compiled.code, generatedOffset);
const position = textPosition(generated, generatedOffset);
const originalPosition = textPosition(original, originalOffset);
let mapRequests = 0;
const server = createServer((request, response) => {
@@ -42,12 +63,12 @@ export async function startBrowserSourceMapSite() {
if (request.url === "/app.js?build=7")
return response
.writeHead(200, { "Content-Type": "text/javascript" })
.end(compiled.code);
.end(generated);
if (request.url?.startsWith("/maps/app.js.map")) {
mapRequests += 1;
return response
.writeHead(200, { "Content-Type": "application/json" })
.end(compiled.map);
.end(map);
}
response.writeHead(404).end("Unselected");
});
@@ -58,8 +79,10 @@ export async function startBrowserSourceMapSite() {
return {
origin: `http://127.0.0.1:${address.port}`,
original,
generated: compiled.code,
map: compiled.map,
generated,
map,
leafCode: compiled.code,
leafMap: compiled.map,
position,
originalPosition,
originalOffset,
+12
View File
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
// Public source-owned fixture; generated runtime bytecode is never tracked.
contract InterfaceFixture {
uint256 public stored;
function set(uint256 value) external { stored = value; }
function read() external view returns (uint256) { return stored; }
function pair(uint32 a, address b) external pure returns (uint32, address) { return (a, b); }
receive() external payable {}
fallback() external {}
}
@@ -0,0 +1,151 @@
import importlib.util
import json
from pathlib import Path
from mitmproxy import connection, ctx, http
from mitmproxy.io import FlowWriter
from mitmproxy.io import tnetstring
from mitmproxy.addons.savehar import SaveHar
from mitmproxy.websocket import WebSocketData, WebSocketMessage
class Generator:
def load(self, loader):
loader.add_option("rea_fixture_root", str, "", "Owned synthetic fixture root")
loader.add_option("rea_adapter_path", str, "", "REA native adapter under verification")
def running(self):
root = Path(ctx.options.rea_fixture_root)
spec = importlib.util.spec_from_file_location("rea_capture_under_test", ctx.options.rea_adapter_path)
adapter = importlib.util.module_from_spec(spec)
spec.loader.exec_module(adapter)
classifications = [
(adapter.CaptureFailure("format", "Malformed retained record.", "/record"), "format"),
(adapter.CaptureFailure("input-limit", "Nesting budget.", "/extension"), "input-limit"),
(MemoryError("payload must not enter diagnostics"), "resource-limit"),
(RuntimeError("payload must not enter diagnostics"), "decoder"),
(OSError("payload must not enter diagnostics"), "decoder"),
]
for error, expected in classifications:
reply = adapter.failure_reply(error)
assert reply["reason"] == expected, reply
assert "payload must not enter diagnostics" not in json.dumps(reply)
for state, expected in [({"metadata": {1: b"bad-key"}}, "format"), ({"metadata": object()}, "decoder")]:
try:
adapter.project_record(state, [])
except adapter.CaptureFailure as error:
assert error.reason == expected, (error.reason, expected)
assert error.pointer == "/metadata", error.pointer
else:
raise AssertionError("Invalid native representation was accepted")
non_utf8 = adapter.project_record({"request": {"headers": [(b"Referer", b"https://byte-user:byte-secret@example.test/path\xff")] }}, [])
assert non_utf8["reported"]["request"]["headers"][0][1] is None, non_utf8
assert non_utf8["binary_fields"][-1]["state"] == "redacted", non_utf8
for value in [" \tHTTPS://ows-user:ows-secret@example.test/path\t ", b" \tHTTPS://ows-user:ows-secret@example.test/path\t "]:
projected = adapter.project_record({"request": {"headers": [(b"Referer", value)]}}, [])
assert projected["reported"]["request"]["headers"][0][1] == " \tHTTPS://example.test/path\t ", projected
assert "ows-secret" not in json.dumps(projected), projected
if isinstance(value, bytes):
assert projected["binary_fields"][-1]["state"] == "redacted", projected
(root / "native-error-classifications.json").write_text(json.dumps({"passed": len(classifications) + 2}))
first = http.HTTPFlow(connection.Client(peername=("127.0.0.1", 1), sockname=("127.0.0.1", 2)), connection.Server(address=("example.test", 80)))
first.id = "original-producer-id"
first.request = http.Request.make("POST", "http://example.test/a?token=ordinary", b"\x00\xffbody", [(b"Authorization", b"Bearer native-transport-secret"), (b"X-Duplicate", b"one"), (b"X-Duplicate", b"two")])
first.request.path = "http://user:password@example.test/a?token=ordinary#fragment"
first.response = http.Response.make(200, b"\x00\xfeanswer", [(b"Set-Cookie", b"session=cookie-secret"), (b"Location", b"https://redirect-user:redirect-password@example.test/b?token=ordinary")])
first.metadata = {"big_integer": 9007199254740993, "nonfinite": float("inf"), "isLosslessNumber": True, "value": "ordinary-extension", "response": {"headers": [["Authorization", "ordinary-extension-credential-name"]]}}
first.websocket = WebSocketData(messages=[WebSocketMessage(2, True, b"\x00\xfdmessage", 123.5)])
first.backup()
first.comment = "caller-selected-fixture"
second = first.copy()
second.id = first.id
second.request.raw_content = None
second.response.raw_content = b""
second.websocket = None
with (root / "flows.mitm").open("wb") as handle:
writer = FlowWriter(handle)
writer.add(first)
writer.add(second)
har_first, har_second = first.copy(), second.copy()
har_first.request.path = "/a?token=ordinary#fragment"
har_second.request.path = "/a?token=ordinary#fragment"
har_first.response.raw_content = b"\x00\xff\x01\xfe"
markers = {"sensitive": "REDACTED", "bracket": "literal[", "overlap": "secret", "ordinary": "unmarked"}
har = SaveHar().make_har([har_first, har_second])
har["log"]["entries"][0]["_markers"] = markers
har["log"]["entries"][0]["_private_properties"] = {"private-property/~": {"number": 123, "text": "private-property"}, "kept": 7}
(root / "producer.har").write_text(json.dumps(har))
har_text = json.dumps(har)
for name, members in [
("equal-number", '"_duplicate": 1, "_duplicate": 1'),
("equal-null", '"_duplicate": null, "_duplicate": null'),
("equal-object", '"_duplicate": {"value": 1}, "_duplicate": {"value": 1}'),
("escaped-key", '"_duplicate": 1, "\\u005fduplicate": 1'),
]:
duplicate = har_text.replace('"cache": {}', '"cache": {}, ' + members, 1)
assert duplicate != har_text
(root / ("duplicate-" + name + ".har")).write_text(duplicate)
duplicate = har_text.replace('"version": "1.2"', '"version": "1.2", "version": "1.2"', 1)
assert duplicate != har_text
(root / "duplicate-core.har").write_text(duplicate)
prototype = json.loads(har_text)
prototype["_extension"] = {"__proto__": {"preserved": 7}, "constructor": "ordinary"}
(root / "prototype.har").write_text(json.dumps(prototype))
prototype_native = first.get_state()
prototype_native["metadata"]["__proto__"] = {"preserved": 7}
with (root / "prototype.mitm").open("wb") as handle:
tnetstring.dump(prototype_native, handle)
invalid_har = json.loads(json.dumps(har))
invalid_har["log"]["entries"][0]["response"]["content"].update({"encoding": "base64", "text": "AR=="})
(root / "invalid-private-parent.har").write_text(json.dumps(invalid_har))
strings = first.get_state()
for state in [strings, strings["backup"]]:
state["request"]["path"] = "https://native-user:string-password@example.test/string-path"
state["request"]["authority"] = "authority-user:authority-password@example.test"
state["request"]["headers"] = [(b"Referer", "https://referer-user:referer-password@example.test/from"), (b"Origin", "//origin-user:origin-password@example.test")]
state["response"]["headers"] = [(b"Location", "https://location-user:location-password@example.test/to")]
state["metadata"]["_markers"] = dict(markers)
state["metadata"]["_private_properties"] = {"private-property/~": {"number": 123, "bytes": b"private-property"}, "kept": 7}
with (root / "string-urls.mitm").open("wb") as handle:
tnetstring.dump(strings, handle)
strings["metadata"]["_private_properties"]["private-property/~"] = {1: b"unsupported-key"}
with (root / "invalid-private-key.mitm").open("wb") as handle:
tnetstring.dump(strings, handle)
extension = "leaf"
for _ in range(66):
extension = {"child": extension}
har["_extension"] = extension
(root / "deep.har").write_text(json.dumps(har))
deep = first.get_state()
deep["_extension"] = extension
with (root / "deep.mitm").open("wb") as handle:
tnetstring.dump(deep, handle)
for name, value in [("invalid-credential-key", {1: b"bad-key"}), ("deep-credential", extension)]:
malformed = first.get_state()
malformed["request"]["headers"] = [(b"Authorization", value)]
with (root / (name + ".mitm")).open("wb") as handle:
tnetstring.dump(malformed, handle)
for prefix, name in [(" \t", "ows"), ("\r\n \v\f\x00\x1f", "controls")]:
url = prefix + "HTTPS://ows-user:ows-secret@example.test/path?token=ordinary#fragment\t "
relative = prefix + "//ows-user:ows-secret@example.test/path?token=ordinary#fragment\t "
whitespace_har = json.loads((root / "producer.har").read_text())
entry = whitespace_har["log"]["entries"][0]
for side, header in [("response", "Location"), ("request", "Referer"), ("request", "Origin")]:
entry[side]["headers"] = [{"name": header, "value": url}] + entry[side]["headers"]
(root / (name + ".har")).write_text(json.dumps(whitespace_har))
whitespace_native = first.get_state()
for state in [whitespace_native, whitespace_native["backup"]]:
state["request"]["headers"] = [(b"Referer", url.encode("utf-8")), (b"Origin", relative)]
state["response"]["headers"] = [(b"Location", url)]
with (root / (name + ".mitm")).open("wb") as handle:
tnetstring.dump(whitespace_native, handle)
non_utf8_native = first.get_state()
for state in [non_utf8_native, non_utf8_native["backup"]]:
state["request"]["path"] = b"https://byte-user:byte-secret@example.test/path\xff"
state["request"]["authority"] = b"byte-user:byte-secret@example.test\xff"
state["request"]["headers"] = [(b"Referer", b" https://byte-user:byte-secret@example.test/from\xff"), (b"Origin", b"//byte-user:byte-secret@example.test\xff")]
state["response"]["headers"] = [(b"Location", b"https://byte-user:byte-secret@example.test/to\xff")]
with (root / "non-utf8-urls.mitm").open("wb") as handle:
tnetstring.dump(non_utf8_native, handle)
(root / "oracle.json").write_text(json.dumps({"records": 2, "request_base64": "AP9ib2R5", "response_base64": "AP5hbnN3ZXI=", "websocket_base64": "AP1tZXNzYWdl", "id": first.id}))
ctx.master.shutdown()
addons = [Generator()]
+24
View File
@@ -0,0 +1,24 @@
#include <stdint.h>
#include <pthread.h>
#include <stdatomic.h>
#include <unistd.h>
static atomic_int ready;
static void *recorded_thread(void *unused) {
(void)unused;
__asm__ volatile("movabs $0xfedcba9876543210, %%r12" ::: "r12");
atomic_store(&ready, 1);
for (;;) pause();
return 0;
}
__attribute__((noinline)) static void fixture_crash(uint64_t value) {
volatile uint64_t *pointer = (volatile uint64_t *)(uintptr_t)0x123;
*pointer = value;
}
int main(void) {
pthread_t thread;
pthread_attr_t attr;
if (pthread_attr_init(&attr) || pthread_attr_setstacksize(&attr, 65536) || pthread_create(&thread, &attr, recorded_thread, 0)) return 1;
while (!atomic_load(&ready)) usleep(1000);
fixture_crash(UINT64_C(0x1122334455667788));
return 0;
}
+1 -1
View File
@@ -30,7 +30,7 @@ const { stdout } = await exec(
},
);
const verifierOutput = JSON.parse(stdout);
// Managed conformance claims are host-independent. Keep the checked-in
// Managed conformance claims are host-independent. Keep the generated
// commitment reproducible across macOS, Linux, and Windows rather than
// embedding the machine that happened to generate it.
const portableCompletionReport = {
+3 -2
View File
@@ -17,8 +17,9 @@ for (const argument of arguments_)
throw new Error(`Unknown MCP tool catalog option: ${argument}`);
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
// The checked-in catalog is a portable package artifact. Keep its canonical
// provider metadata independent of the machine running the generator.
// This build-generated source supports schema and provider conformance tests;
// production MCP tools are registered from their canonical contracts directly.
// Keep the test metadata portable and regenerate it with `npm run build`.
const CATALOG_PLATFORM = "darwin";
const sessionToolNames = new Set([
...TOOL_CONTRACTS.filter(({ kind }) => kind === "session").map(
+1 -1
View File
@@ -17,7 +17,7 @@ const lock = JSON.parse(
await readFile(join(root, "package-lock.json"), "utf8"),
);
const skill = await readFile(
join(root, "skills/reverse-engineer-anything/SKILL.md"),
join(root, "skill-src/reverse-engineer-anything/SKILL.md"),
"utf8",
);
const skillVersion = /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(skill)?.[1];
+4 -1
View File
@@ -1,3 +1,4 @@
import { mkdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
@@ -14,8 +15,10 @@ for (const argument of arguments_)
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const catalog = await createProductCatalog(root);
const path = join(root, "docs/public/product-catalog.json");
if (!arguments_.has("--check")) await mkdir(dirname(path), { recursive: true });
await ensureGeneratedFile({
path: join(root, "docs/product-catalog.json"),
path,
source: await serializeProductCatalog(catalog),
check: arguments_.has("--check"),
generateCommand: "npm run docs:generate",
+62 -19
View File
@@ -1,4 +1,4 @@
import { readFile } from "node:fs/promises";
import { mkdir, readFile, readdir, rm } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
@@ -10,26 +10,69 @@ for (const argument of arguments_)
throw new Error(`Unknown skill metadata option: ${argument}`);
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const path = join(root, "skills/reverse-engineer-anything/SKILL.md");
const current = await readFile(path, "utf8");
const sourceRoot = join(root, "skill-src/reverse-engineer-anything");
const outputRoot = join(root, "skills/reverse-engineer-anything");
const check = arguments_.has("--check");
const current = (
await readFile(join(sourceRoot, "SKILL.md"), "utf8")
).replaceAll("\r\n", "\n");
const { CATALOG_IDENTITY } = await import(
`${pathToFileURL(join(root, "dist/catalogIdentity.js")).href}?${String(Date.now())}`
);
const withCount = current.replace(
/^\s{2}tool_count:\s*\d+\s*$/mu,
` tool_count: ${String(CATALOG_IDENTITY.counts.mcp_tools)}`,
if (/^\s{2}(?:tool_count|catalog_digest):/mu.test(current))
throw new Error(
"Catalog metadata belongs in the generated skill, not skill-src",
);
const versionLine = /^ {2}version: "[^"\r\n]+"$/mu;
if (!versionLine.test(current))
throw new Error("Missing authored skill version");
const source = current.replace(
versionLine,
`$&\n tool_count: ${String(CATALOG_IDENTITY.counts.mcp_tools)}`,
);
const digestLine = ` catalog_digest: "${CATALOG_IDENTITY.digests.combined_sha256}"`;
const source = /^\s{2}catalog_digest:/mu.test(withCount)
? withCount.replace(
/^\s{2}catalog_digest:\s*"[a-f0-9]{64}"\s*$/mu,
digestLine,
)
: withCount.replace(/^(\s{2}tool_count:\s*\d+\s*)$/mu, `$1\n${digestLine}`);
const paths = await filePaths(sourceRoot);
// Rebuild this owned output directory so removed references cannot survive a build.
if (!check) await rm(outputRoot, { recursive: true, force: true });
for (const relativePath of paths) {
const path = join(outputRoot, relativePath);
if (!check) await mkdir(dirname(path), { recursive: true });
await ensureGeneratedFile({
path,
source:
relativePath === "SKILL.md"
? source
: (await readFile(join(sourceRoot, relativePath), "utf8")).replaceAll(
"\r\n",
"\n",
),
check,
generateCommand: "npm run docs:generate",
});
}
if (
check &&
JSON.stringify(await filePaths(outputRoot)) !== JSON.stringify(paths)
)
throw new Error(
"Generated skill file inventory drifted; run npm run docs:generate",
);
await ensureGeneratedFile({
path,
source,
check: arguments_.has("--check"),
generateCommand: "npm run docs:generate",
});
async function filePaths(directory, prefix = "") {
const paths = [];
for (const entry of (await readdir(directory, { withFileTypes: true })).sort(
(left, right) =>
left.name < right.name ? -1 : left.name > right.name ? 1 : 0,
)) {
const relativePath = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
if (entry.isDirectory())
paths.push(
...(await filePaths(join(directory, entry.name), relativePath)),
);
else if (entry.isFile()) paths.push(relativePath);
else
throw new Error(
`Skill bundle requires regular files or directories: ${relativePath}`,
);
}
return paths;
}
+14 -4
View File
@@ -14,13 +14,23 @@ const environment = () => ({
});
/** Execute the compiled CLI with production providers and no test adapters. */
export async function artifactCli(command, target, arguments_ = []) {
return (await artifactCliEvidence(command, target, arguments_))
export async function artifactCli(
command,
target,
arguments_ = [],
options = {},
) {
return (await artifactCliEvidence(command, target, arguments_, options))
.normalized_result;
}
/** Execute the compiled CLI and return its complete Evidence record. */
export async function artifactCliEvidence(command, target, arguments_ = []) {
export async function artifactCliEvidence(
command,
target,
arguments_ = [],
{ timeoutMs = 60000 } = {},
) {
const { stdout } = await promisify(execFile)(
process.execPath,
[
@@ -30,7 +40,7 @@ export async function artifactCliEvidence(command, target, arguments_ = []) {
...arguments_,
"--json",
],
{ env: environment(), timeout: 60000, maxBuffer: 16 * 1024 * 1024 },
{ env: environment(), timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 },
);
const evidence = JSON.parse(stdout);
assert.equal(evidence.error, undefined, stdout);
@@ -0,0 +1,238 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { TraceMap, eachMapping } from "@jridgewell/trace-mapping";
import { CdpBrowserProvider } from "../../dist/browser/CdpBrowserProvider.js";
import { CdpConnection } from "../../dist/browser/CdpConnection.js";
import { waitForBrowserDevtoolsPort } from "../../dist/browser/BrowserProcessStartup.js";
import { parseEvidence } from "../../dist/domain/evidence.js";
import { traceSourceMap } from "../../dist/javascript/sourceMaps/TraceSourceMap.js";
import { startBrowserSourceMapSite } from "../fixtures/browser-source-map-site.mjs";
import {
startRuntimeFixtureBrowser,
closeRuntimeFixtureResources,
} from "./browser-runtime-fixture-lifecycle.mjs";
const cases = [
{ name: "same-line nested column control", prefixes: ["\n\n ", " "] },
{ name: "zero outer column control", prefixes: ["\n\n", "\n "] },
{ name: "nested line resets outer column", prefixes: ["\n\n ", "\n"] },
{
name: "nested line retains its own column",
prefixes: ["\n\n ", "\n "],
},
];
/** Verify compiler-produced nested maps through an actual owned Chrome and public CLI. */
export async function verifyBrowserNestedSourceMaps(
executable,
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
) {
const results = [];
for (const scenario of cases) {
try {
const proof = await verifyCase(executable, entrypoint, scenario);
results.push({ name: scenario.name, status: "pass", ...proof });
} catch (cause) {
results.push({
name: scenario.name,
status: "fail",
phase: cause.qualificationPhase ?? "native-producer-admission",
proof: cause.qualificationReceipt ?? null,
error: String(cause),
});
}
}
// Preserve every affected owner and control even when BEFORE fails.
process.stdout.write(
`${JSON.stringify({ nested_source_map_cases: results })}\n`,
);
assert.equal(results.filter(({ status }) => status === "fail").length, 0);
return { cases: results, actual_chrome: true, public_cli: true };
}
async function verifyCase(executable, entrypoint, scenario) {
const site = await startBrowserSourceMapSite({
prefixes: scenario.prefixes,
sourceMapAnnotation: true,
sourcefile: "../src/nested-fixture.ts",
});
let fixture;
let connection;
let primaryError;
let stderr = "";
let phase = "native-map-admission";
let browserVersion;
let expected;
let observed;
try {
const mapUrl = `${site.origin}/maps/app.js.map`;
// This independent existing decoder already composes valid nested offsets.
// It is a before-product admission control, not the expected browser oracle.
const point = traceSourceMap(site.map, mapUrl, site.position);
assert.equal(point.matches.length, 1);
assert.equal(
point.matches[0].original_position.line,
site.originalPosition.line,
);
assert.equal(
point.matches[0].original_position.column,
site.originalPosition.column,
);
expected = compilerMappingsAtPhysicalPositions(site, mapUrl);
assert.ok(
expected.length > 0,
"Native compiler leaf must carry point mappings",
);
phase = "owned-browser-startup";
fixture = await startRuntimeFixtureBrowser(executable, site.origin);
fixture.browser.stderr.on("data", (chunk) => {
if (stderr.length < 65536) stderr += chunk;
});
const port = await waitForBrowserDevtoolsPort({
child: fixture.browser,
executable,
activePortPath: join(fixture.profile, "DevToolsActivePort"),
stderr: () => stderr,
timeoutMs: 20_000,
});
const endpoint = `http://127.0.0.1:${port}`;
const provider = new CdpBrowserProvider();
const targets = await provider.listTargets({
cdp_endpoint: endpoint,
allowed_origins: [site.origin],
});
if (!targets.ok) throw targets.error;
const target = targets.value.targets[0];
assert.ok(target, "Owned fixture target missing");
const pages = await (await fetch(`${endpoint}/json/list`)).json();
const page = pages.find(({ id }) => id === target.target_id);
assert.ok(page, "Owned CDP fixture page missing");
connection = await CdpConnection.connect(
page.webSocketDebuggerUrl,
"inspect_web_page",
);
await ready(connection);
browserVersion = await connection.send("Browser.getVersion");
phase = "public-cli";
const { stdout } = await promisify(execFile)(
process.execPath,
[
entrypoint,
"analyze-web-bundle",
endpoint,
target.target_id,
"--allowed-origins",
site.origin,
"--fetch-source-maps",
"--observation-ms",
"0",
"--json",
],
{
env: { ...process.env, REA_LOG_LEVEL: "silent" },
timeout: 45_000,
maxBuffer: 16 * 1024 * 1024,
},
);
const evidence = parseEvidence(JSON.parse(stdout));
assert.equal(evidence.operation, "analyze_web_bundle");
const maps = evidence.normalized_result.observations.source_maps;
const item = maps.items.find(({ artifact }) => artifact?.text === site.map);
assert.ok(
item,
"Public browser analysis omitted the exact compiler-derived map",
);
phase = "public-map-assertions";
observed = item?.mappings;
assert.equal(item.status, "included");
assert.deepEqual(item.mappings, expected);
assert.equal(item.artifact.sha256, sha(site.map));
assert.equal(item.artifact.bytes, Buffer.byteLength(site.map));
assert.equal(item.original_sources[0].artifact.sha256, sha(site.original));
assert.ok(
site.mapRequests() > 0,
"Actual browser workflow did not fetch the declared map",
);
return {
compiler: site.compiler,
browser: browserVersion.product,
generated_sha256: sha(site.generated),
source_map_sha256: sha(site.map),
original_sha256: sha(site.original),
mappings: expected.length,
native_decoder_control: true,
};
} catch (cause) {
const failure = new Error(`Nested source-map ${phase}: ${String(cause)}`, {
cause,
});
failure.qualificationPhase = phase;
failure.qualificationReceipt = {
compiler: site.compiler,
browser: browserVersion?.product ?? null,
generated_sha256: sha(site.generated),
leaf_map_sha256: sha(site.leafMap),
source_map_sha256: sha(site.map),
original_sha256: sha(site.original),
expected_first_mapping: expected?.[0] ?? null,
observed_first_mapping: observed?.[0] ?? null,
};
primaryError = failure;
throw failure;
} finally {
await closeRuntimeFixtureResources(
[() => connection?.close(), () => fixture?.close(), () => site.close()],
primaryError,
);
}
}
function compilerMappingsAtPhysicalPositions(site, mapUrl) {
const expected = [];
const prefixLength = site.generated.indexOf(site.leafCode);
assert.ok(prefixLength >= 0);
eachMapping(new TraceMap(site.leafMap, mapUrl), (mapping) => {
if (
mapping.source === null ||
mapping.originalLine === null ||
mapping.originalColumn === null
)
return;
const leafLines = site.leafCode.split("\n");
const before = leafLines.slice(0, mapping.generatedLine - 1).join("\n");
const leafOffset =
before.length +
(mapping.generatedLine > 1 ? 1 : 0) +
mapping.generatedColumn;
const physical = site.generated
.slice(0, prefixLength + leafOffset)
.split("\n");
expected.push({
generated_line: physical.length,
generated_column: physical.at(-1).length,
source: mapping.source,
original_line: mapping.originalLine,
original_column: mapping.originalColumn,
name: mapping.name ?? null,
});
});
return expected;
}
async function ready(connection) {
for (let attempt = 0; attempt < 100; attempt += 1) {
const result = await connection.send("Runtime.evaluate", {
expression: 'document.body.dataset.ready === "source-map proof"',
returnByValue: true,
});
if (result.result.value === true) return;
await new Promise((resolve) => setTimeout(resolve, 20));
}
throw new Error("Owned compiler fixture did not execute in actual Chrome");
}
const sha = (text) => createHash("sha256").update(text).digest("hex");
+86 -2
View File
@@ -1,5 +1,8 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { chromium } from "playwright-core";
@@ -62,13 +65,21 @@ export async function verifyLargeScreenshotE2e(endpoint, origin) {
);
assert.equal(bytes.readUInt32BE(16), 2048);
assert.equal(bytes.readUInt32BE(20), 1536);
await verifyDefaultScreenshotDelivery({
entrypoint,
environment: env,
endpoint,
targetId: target.id,
screenshot,
});
const maxBufferSize = 64 * 1024 * 1024;
transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
env: { ...env, REA_MCP_MAX_RESPONSE_BYTES: String(maxBufferSize) },
stderr: "pipe",
// The SDK defaults to a 10 MiB receive buffer; inline PNG output is larger.
maxBufferSize: 64 * 1024 * 1024,
maxBufferSize,
});
client = new Client({ name: "browser-screenshot-real-e2e", version: "1" });
await client.connect(transport);
@@ -102,6 +113,9 @@ export async function verifyLargeScreenshotE2e(endpoint, origin) {
mocked: false,
cli: true,
stdio_mcp: true,
default_buffer_recovery: true,
complete_export: true,
matched_large_buffer: true,
png_bytes: bytes.byteLength,
};
} finally {
@@ -117,3 +131,73 @@ export async function verifyLargeScreenshotE2e(endpoint, origin) {
}
}
}
const verifyDefaultScreenshotDelivery = async ({
entrypoint,
environment,
endpoint,
targetId,
screenshot,
}) => {
const workspace = await mkdtemp(join(tmpdir(), "rea-screenshot-delivery-"));
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env: environment,
stderr: "pipe",
});
const client = new Client({
name: "browser-screenshot-default-e2e",
version: "1",
});
try {
await client.connect(transport);
const constrained = await client.callTool(
{
name: "capture_web_screenshot",
arguments: { cdp_endpoint: endpoint, target_id: targetId },
},
{ timeout: 60000 },
);
assert.equal(constrained.isError, true);
assert.equal(
constrained.structuredContent?.error?.details?.resource,
"transport",
);
const reference =
constrained.structuredContent?.error?.details?.reported_limits
?.evidence_reference;
assert.equal(reference?.kind, "retained-evidence");
await client.ping();
const exported = await client.callTool({
name: "export_evidence_bundle",
arguments: { path: join(workspace, "screenshot-evidence.json") },
});
assert.notEqual(exported.isError, true, JSON.stringify(exported));
const bundle = JSON.parse(
await readFile(join(workspace, "screenshot-evidence.json"), "utf8"),
);
const record = bundle.records.find(
(record) => record.evidence_id === reference.evidence_id,
);
assert.ok(
record,
"Full screenshot Evidence must remain exportable after the delivery constraint",
);
const retained = webScreenshotSchema.parse(
parseEvidence(record).normalized_result,
);
assert.deepEqual(retained.artifact, screenshot.artifact);
await client.ping();
} finally {
try {
await client.close();
} finally {
try {
await transport.close();
} finally {
await rm(workspace, { recursive: true, force: true });
}
}
}
};
+30 -2
View File
@@ -16,12 +16,18 @@ export const toolFamilyCatalog = (sources) => {
{
id: "native",
surface: "native-provider",
contracts: sources.nativeContracts.NATIVE_TOOL_CONTRACTS,
contracts: [
...sources.nativeContracts.NATIVE_TOOL_CONTRACTS,
...sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS,
],
},
{
id: "artifact",
surface: "artifact-provider",
contracts: sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS,
contracts: [
...sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS,
...sources.evmContracts.EVM_TOOL_CONTRACTS,
],
},
{
id: "managed",
@@ -48,6 +54,8 @@ export const toolFamilyCatalog = (sources) => {
...sources.browserContracts.BROWSER_TOOL_CONTRACTS,
...sources.webRuntimeContracts.WEB_RUNTIME_TOOL_CONTRACTS,
...sources.browserScenarioContracts.BROWSER_SCENARIO_TOOL_CONTRACTS,
...sources.webNetworkCaptureContracts
.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS,
],
},
{
@@ -141,6 +149,15 @@ export const providerCatalog = (sources) => {
identity: sources.nativeProvider.NATIVE_MACOS_PROVIDER_IDENTITY,
contracts: sources.nativeContracts.NATIVE_TOOL_CONTRACTS,
},
{
identity: sources.pwntoolsProvider.PWNTOOLS_PROVIDER_IDENTITY,
contracts:
sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS,
},
{
identity: sources.evmProvider.EVMOLE_PROVIDER_IDENTITY,
contracts: sources.evmContracts.EVM_TOOL_CONTRACTS,
},
{
identity: sources.artifactProviders.ARTIFACT_GRAPH_PROVIDER,
contracts: sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS,
@@ -170,6 +187,17 @@ export const providerCatalog = (sources) => {
identity: sources.androidProvider.JADX_PROVIDER_IDENTITY,
contracts: sources.androidContracts.ANDROID_TOOL_CONTRACTS,
},
{
identity: sources.webNetworkCaptureProvider.HAR_CAPTURE_PROVIDER_IDENTITY,
contracts:
sources.webNetworkCaptureContracts.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS,
},
{
identity:
sources.webNetworkCaptureProvider.MITMPROXY_CAPTURE_PROVIDER_IDENTITY,
contracts:
sources.webNetworkCaptureContracts.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS,
},
{
identity: sources.browserProvider.CDP_BROWSER_PROVIDER_IDENTITY,
contracts: [
+9 -1
View File
@@ -40,7 +40,12 @@ const SOURCE_PATHS = {
officialContracts: "dist/contracts/officialToolContracts.js",
enhancedContracts: "dist/contracts/enhancedToolContracts.js",
sessionContracts: "dist/contracts/sessionToolContracts.js",
binaryDiagnosticsContracts:
"dist/contracts/native/binaryDiagnosticsToolContracts.js",
pwntoolsProvider: "dist/native/pwntools/PwntoolsRelease.js",
nativeContracts: "dist/contracts/native/nativeToolContracts.js",
evmContracts: "dist/contracts/evm/evmToolContracts.js",
evmProvider: "dist/evm/EvmoleRelease.js",
artifactContracts: "dist/contracts/artifactToolContracts.js",
managedContracts: "dist/contracts/managed/managedToolContracts.js",
firmwareContracts: "dist/contracts/firmware/firmwareToolContracts.js",
@@ -51,6 +56,9 @@ const SOURCE_PATHS = {
"dist/contracts/managed/managedWorkflowToolContracts.js",
browserContracts: "dist/contracts/browserToolContracts.js",
webRuntimeContracts: "dist/contracts/webRuntimeToolContracts.js",
webNetworkCaptureContracts:
"dist/contracts/webNetworkCaptureToolContracts.js",
webNetworkCaptureProvider: "dist/browser/history/CaptureRelease.js",
browserScenarioContracts: "dist/contracts/browserScenarioToolContracts.js",
electronContracts: "dist/contracts/javascript/electronToolContracts.js",
javascriptRuntimeObservationContracts:
@@ -75,7 +83,7 @@ const SOURCE_PATHS = {
evidenceBundle: "dist/domain/evidenceBundle.js",
evidenceCompletion: "dist/domain/evidenceCompletionLedger.js",
completionGeneration: "dist/domain/completionLedgerGeneration.js",
processCapture: "dist/domain/processCapture.js",
processCapture: "dist/domain/process/processCapture.js",
analysisSnapshot: "dist/domain/analysisSnapshot.js",
artifactGraph: "dist/domain/artifactGraph.js",
browserObservation: "dist/domain/browserObservation.js",
+43 -30
View File
@@ -8,21 +8,14 @@ const README_PATHS = [
"README_ko.md",
"README_ar.md",
];
const tableCounts = (content, path, expectedCounts) => {
const lines = content.split(/\r?\n/u);
for (const [index, line] of lines.entries()) {
if (!/^\|\s*-/u.test(line)) continue;
const counts = [];
for (const row of lines.slice(index + 1)) {
if (!row.trim().startsWith("|")) break;
const count = Number(row.split("|")[2]?.trim());
if (!Number.isInteger(count)) break;
counts.push(count);
}
if (counts.length === expectedCounts.length) return counts;
}
throw new Error(`Missing tool-family inventory table in ${path}`);
};
const readmeLinkTargets = (content) =>
[...content.matchAll(/\]\(([^)\s]+)\)/gu)]
.map((match) => match[1] ?? "")
.filter(
(target) =>
!target.startsWith("#") && !/^README(?:_[a-z]+)?\.md$/u.test(target),
)
.sort();
const requireText = (issues, path, content, expected) => {
if (!content.includes(expected)) issues.push(`${path}: missing ${expected}`);
@@ -71,32 +64,52 @@ export const documentationFactIssues = async (root, catalog) => {
const issues = await skillReferenceIssues(
join(root, "skills/reverse-engineer-anything"),
);
const expectedCounts = catalog.tools.families.map(({ count }) => count);
const readmes = new Map();
for (const path of README_PATHS) {
const content = await readFile(join(root, path), "utf8");
readmes.set(path, content);
requireText(issues, path, content, "MCP-tool_catalog");
for (const client of catalog.setup_clients)
requireText(issues, path, content, client.display_name);
try {
const actualCounts = tableCounts(content, path, expectedCounts);
if (JSON.stringify(actualCounts) !== JSON.stringify(expectedCounts))
issues.push(
`${path}: tool family counts ${JSON.stringify(actualCounts)} do not match ${JSON.stringify(expectedCounts)}`,
);
} catch (cause) {
issues.push(cause instanceof Error ? cause.message : String(cause));
if (path === "README.md") {
requireText(
issues,
path,
content,
"(docs/installation.md#supported-agents)",
);
requireText(issues, path, content, "(docs/installation.md#mcp-registry)");
requireText(
issues,
path,
content,
"(docs/mcp-contracts.md#generated-catalog)",
);
}
}
const canonicalLinks = readmeLinkTargets(readmes.get("README.md") ?? "");
for (const path of README_PATHS.slice(1))
if (
JSON.stringify(readmeLinkTargets(readmes.get(path) ?? "")) !==
JSON.stringify(canonicalLinks)
)
issues.push(`${path}: documentation links differ from README.md`);
const installationPath = "docs/installation.md";
const installation = await readFile(join(root, installationPath), "utf8");
for (const client of catalog.setup_clients)
requireText(issues, installationPath, installation, client.display_name);
const agents = await readFile(join(root, "AGENTS.md"), "utf8");
requireText(issues, "AGENTS.md", agents, "docs/product-catalog.json");
requireText(issues, "AGENTS.md", agents, "docs/public/product-catalog.json");
const templatePath = ".github/pull_request_template.md";
const template = await readFile(join(root, templatePath), "utf8");
requireText(issues, templatePath, template, "docs/product-catalog.json");
requireText(
issues,
templatePath,
template,
"docs/public/product-catalog.json",
);
const english = await readFile(join(root, "README.md"), "utf8");
requireText(issues, "README.md", english, "docs/product-catalog.json");
return issues;
};
+88
View File
@@ -0,0 +1,88 @@
import assert from "node:assert/strict";
/** Source-owned ELF64 mutations for declared, absent and escaped name-table references. */
export const sectionNameFixtures = (original, report) => {
// ELF gABI: https://gabi.xinuos.com/elf/02-eheader.html#e-shstrndx
// SHN_XINDEX must carry an actual index >= SHN_LORESERVE, not a small index.
const sourceTable = report.sections[original.readUInt16LE(62)];
assert.equal(sourceTable.type, "SHT_STRTAB");
const sourceHeader = Number(BigInt(sourceTable.header_location.offset));
const fixtures = [];
for (const problem of [
"out-of-range",
"wrong-type",
"undeclared-header",
"extended-undeclared-header",
"extended-without-sections",
"extended-absent",
"extended-small",
]) {
let bytes = Buffer.from(original);
let selected = report.sections.length + 100;
if (problem === "wrong-type")
selected = report.sections.find(
(section) => section.type === "SHT_PROGBITS",
).index;
else if (problem.includes("undeclared-header")) {
const end =
Number(bytes.readBigUInt64LE(40)) +
report.sections.length * bytes.readUInt16LE(58);
const extended = Buffer.alloc(Math.max(bytes.length, end + 64));
bytes.copy(extended);
bytes.copy(extended, end, sourceHeader, sourceHeader + 64);
bytes = extended;
selected = report.sections.length;
}
if (problem === "extended-without-sections") {
bytes.writeBigUInt64LE(0n, 40);
bytes.writeUInt16LE(0, 60);
bytes.writeUInt16LE(0xffff, 62);
} else if (problem.startsWith("extended-")) {
bytes.writeUInt16LE(0xffff, 62);
if (problem === "extended-absent") selected = 0;
if (problem === "extended-small") selected = sourceTable.index;
bytes.writeUInt32LE(selected, Number(bytes.readBigUInt64LE(40)) + 40);
} else bytes.writeUInt16LE(selected, 62);
fixtures.push({ name: problem, bytes, expectation: "invalid_input" });
}
const absent = Buffer.from(original);
absent.writeUInt16LE(0, 62);
fixtures.push({ name: "absent", bytes: absent, expectation: "absent" });
// A genuine escape requires a large table. Relocate the original table, keep
// its referenced indices and move only the name table to index SHN_LORESERVE.
const selected = 0xff00,
count = selected + 1;
const start = Math.ceil(original.length / 8) * 8;
const large = Buffer.alloc(start + count * 64);
original.copy(large);
const oldStart = Number(original.readBigUInt64LE(40));
original.copy(large, start, oldStart, oldStart + report.sections.length * 64);
large.fill(
0,
start + sourceTable.index * 64,
start + (sourceTable.index + 1) * 64,
);
original.copy(large, start + selected * 64, sourceHeader, sourceHeader + 64);
large.writeBigUInt64LE(BigInt(start), 40);
large.writeUInt16LE(0, 60);
large.writeUInt16LE(0xffff, 62);
large.writeBigUInt64LE(BigInt(count), start + 32);
large.writeUInt32LE(selected, start + 40);
fixtures.push({
name: "extended-large",
bytes: large,
expectation: "resolved",
tableIndex: selected,
sectionCount: count,
});
const reserved = Buffer.from(large);
reserved.writeUInt16LE(selected, 62);
reserved.writeUInt32LE(0, start + 40);
fixtures.push({
name: "direct-reserved",
bytes: reserved,
expectation: "invalid_input",
});
return fixtures;
};
+1 -1
View File
@@ -33,7 +33,7 @@ export const ensureGeneratedFile = async ({
return { changed: false };
if (check)
throw new Error(
`${path} is missing or stale. Run \`${generateCommand}\` and commit the result.`,
`${path} is missing or stale. Run \`${generateCommand}\` to regenerate it; commit the result if the file is tracked.`,
);
await writeFile(path, preserveLineEndings(source, existing), "utf8");
return { changed: true };
@@ -0,0 +1,184 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { randomUUID } from "node:crypto";
import { writeFile, rm } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
import { createAnalysisProfile } from "../../dist/domain/analysisProfile.js";
import {
createAnalysisSnapshotEntry,
parseAnalysisSnapshot,
serializeAnalysisSnapshot,
snapshotBinding,
snapshotTarget,
} from "../../dist/domain/analysisSnapshot.js";
import { createEvidence } from "../../dist/domain/evidence.js";
import { createEvidenceBundle } from "../../dist/domain/evidenceBundle.js";
import { requireMcpResult } from "./mcp-verifier-results.mjs";
/** Reject a valid older snapshot whose instruction-only collector omitted a real edge. */
export async function verifyLegacyGhidraReferenceSnapshot(
client,
{ target, procedure, omittedEdge, entrypoint, env },
) {
const bundle = requireMcpResult(
await client.callTool({
name: "get_evidence_bundle",
arguments: {},
}),
"get_evidence_bundle",
);
const observed = bundle.records.find(
(record) =>
record.operation === "analyze_function" &&
record.parameters.procedure === procedure,
);
assert.ok(observed);
const { function_references: coverage, ...legacyParameters } =
observed.analysis_profile.parameters;
assert.equal(
typeof coverage,
"string",
"Reference semantics must participate in snapshot identity",
);
const legacyProfile = createAnalysisProfile(
observed.provider,
legacyParameters,
);
assert.notEqual(legacyProfile.digest, observed.analysis_profile.digest);
const parsed = await parseBinaryTarget(
target.path,
process.cwd(),
process.arch,
);
if (!parsed.ok) throw parsed.error;
const nativeTarget = parsed.value;
const result = structuredClone(observed.normalized_result);
// Reconstruct the observed pre-fix omission; all other data
// comes from the real provider and the file has a fully valid Evidence binding.
result.incoming_references = result.incoming_references.filter(
(edge) =>
edge.source_address !== omittedEdge.source_address ||
edge.target_address !== omittedEdge.target_address,
);
assert.equal(
result.incoming_references.length,
observed.normalized_result.incoming_references.length - 1,
);
const evidence = createEvidence(nativeTarget, observed.provider, {
operation: observed.operation,
parameters: observed.parameters,
result,
rawResult: result,
analysisProfile: legacyProfile,
limitations: observed.limitations,
locations: observed.locations,
});
const binding = snapshotBinding(legacyProfile);
const identity = snapshotTarget(nativeTarget);
const snapshot = parseAnalysisSnapshot({
target: identity,
binding,
entries: [
createAnalysisSnapshotEntry({
target: identity,
binding,
operation: observed.operation,
parameters: observed.parameters,
execution: {
result,
rawResult: result,
provider: observed.provider,
analysisProfile: legacyProfile,
limitations: observed.limitations,
locations: observed.locations,
subject: nativeTarget,
},
}),
],
evidence_bundle: createEvidenceBundle([evidence]),
});
assert.equal(
snapshot.entries.length,
1,
"The legacy query must remain valid and replayable under its own profile",
);
const path = join(env.TMPDIR, `legacy-reference-${randomUUID()}.json`);
await writeFile(path, serializeAnalysisSnapshot(snapshot), { flag: "wx" });
try {
const session = requireMcpResult(
await client.callTool({ name: "binary_session", arguments: {} }),
"binary_session",
);
const reply = await client.callTool({
name: "open_binary",
arguments: {
path: target.path,
provider_id: "ghidra",
snapshot_path: path,
},
});
assert.equal(reply.isError, true);
const mcpRejection = reply.structuredContent.error;
assert.equal(mcpRejection.code, "evidence_integrity_mismatch");
assert.match(mcpRejection.details.reason, /profile_mismatch/u);
assert.match(
mcpRejection.remediation.action,
/without this snapshot.*fresh snapshot/u,
);
assert.deepEqual(
requireMcpResult(
await client.callTool({ name: "binary_session", arguments: {} }),
"binary_session",
),
session,
"A rejected snapshot must preserve the selected session",
);
const retained = requireMcpResult(
await client.callTool({
name: "analyze_function",
arguments: { procedure },
}),
"analyze_function",
);
assert.ok(
retained.incoming_references.some(
(edge) =>
edge.source_address === omittedEdge.source_address &&
edge.target_address === omittedEdge.target_address,
),
"A rejected legacy snapshot must not replace complete live analysis",
);
await assert.rejects(
promisify(execFile)(
process.execPath,
[
entrypoint,
"function",
target.path,
procedure,
"--provider",
"ghidra",
"--snapshot",
path,
"--json",
],
{ env, timeout: 240000, maxBuffer: 72 * 1024 * 1024 },
),
(error) => {
assert.equal(error.code, 1);
const rejection = JSON.parse(error.stdout);
assert.equal(rejection.code, "evidence_integrity_mismatch");
assert.equal(rejection.details.reason, mcpRejection.details.reason);
assert.equal(
rejection.remediation.action,
mcpRejection.remediation.action,
);
return true;
},
);
} finally {
await rm(path);
}
}
+30
View File
@@ -0,0 +1,30 @@
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
/** Own one SDK client with an explicit receive budget for a large complete result. */
export const withLargeResultMcp = async (
{ entrypoint, environment, maxBufferSize },
useClient,
) => {
const client = new Client({ name: "large-result-verifier", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env: {
...environment,
REA_MCP_MAX_RESPONSE_BYTES: String(maxBufferSize),
},
stderr: "inherit",
maxBufferSize,
});
try {
await client.connect(transport);
return await useClient(client);
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
};
+1
View File
@@ -6,6 +6,7 @@ export interface ModuleBoundaryViolation {
readonly target: string;
readonly boundary:
| "artifact-acquisition"
| "process-capture"
| "pure-layer"
| "application-composition"
| "provider-construction"

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