Compare commits

...
Author SHA1 Message Date
Ysopwnable d8288ef1c8 fix(linux): align Hopper defaults and support docs 2026-10-07 22:20:30 +08:00
Namnarak 9fe37142ee chore: retrigger CI after PR title fix 2026-10-07 20:46:34 +07:00
Namnarak 16777fe7b0 fix(hopper): harden Linux demo dialog handling 2026-10-07 20:14:40 +07:00
Namnarak f5fbbc3fdf fix(config): reuse user-local Hopper launcher 2026-10-07 20:14:39 +07:00
Namnarak f7a5b66df2 fix(linux): support CachyOS Hopper hosts 2026-10-07 20:14:38 +07:00
Kaoru0822andGitHub a65755728f Merge pull request #953 from morluto/feat/macho-chained-fixups
feat(native): decode chained fixups, binds, ObjC categories and properties
2026-10-07 05:43:23 -07:00
Kaoru0822andGitHub 7acf4e741a Merge pull request #931 from morluto/fix/apple-bridge-roots
fix(apple): pair bridge hypotheses within one application root
2026-10-07 05:41:08 -07:00
morlutoandGitHub c7530f9a81 perf(javascript): reduce analysis and source-map work (#955)
* perf(javascript): reduce semantic traversal overhead

* perf(source-maps): reuse validated regular map rows
2026-10-07 20:33:35 +08:00
Kaoru0822andClaude Opus 5.5 6d74992efe docs(apple): describe directory-based framework plist ambiguity
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:21:41 +00:00
Kaoru0822andClaude Opus 5.5 3e24a72465 feat(native): decode chained fixups, binds, ObjC categories and properties
inspect_native_dispatch_metadata read pointer fields as stored bytes, so
chained-fixup images (the default since Xcode 13) decoded no classes, and
superclasses from other images were always unresolved.

- Decode LC_DYLD_CHAINED_FIXUPS pointer formats 64, 64_OFFSET, ARM64E,
  ARM64E_USERLAND and ARM64E_USERLAND24 with import formats 1-3, and
  interpret LC_DYLD_INFO bind, weak-bind and lazy-bind opcode streams.
- Name external superclasses from their _OBJC_CLASS_$_ binds.
- Decode class_ro_t baseProperties and __objc_catlist category_t records
  (instance/class methods, protocols, properties) into objc_categories;
  category methods carry their category name.
- Report the fixup encoding in a pointer_fixups coverage facet.
- verify:apple-dispatch builds the fixture with dyld-info and chained
  fixups, plus arm64e on Apple silicon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:18:24 +00:00
Kaoru0822andGitHub dda8e6a728 fix(setup): accept the pinned Hopper RPM launcher (#933) 2026-10-07 20:04:57 +08:00
morlutoandGitHub 2366bda37b fix(artifacts): harden reader lifecycle boundaries (#951)
* fix(artifacts): retain ASAR filesystem failure reasons

* fix(artifacts): avoid blocking on replaced FIFO entries

* test(artifacts): type the ASAR reader iterator

* fix(artifacts): classify filesystem-shaped ASAR errors

* test: account for POSIX FIFO fixture utility
2026-10-07 20:00:24 +08:00
morlutoandGitHub e0d76ae80d fix(mcp): advertise reserved process environment key (#949) 2026-10-07 19:58:51 +08:00
morlutoandGitHub f14b6a8430 fix(ida): share concurrent session cleanup (#950) 2026-10-07 19:58:43 +08:00
morlutoandGitHub 118d43e787 perf(setup): reuse initial diagnostics during planning (#940)
* perf(setup): reuse initial diagnostics during planning

* test(setup): verify post-configuration doctor refresh

* style(setup): remove extra test spacing
2026-10-07 19:52:43 +08:00
morlutoandGitHub c92a344d47 perf(javascript): index semantic fingerprint relations (#942) 2026-10-07 19:52:29 +08:00
morlutoandGitHub ac3bae3cb9 fix(native-ui): handle incomplete accessibility child collections (#944)
* fix(native-ui): align verifier with scenario contract

* perf(native-ui): reuse accessibility child counts

* fix(native-ui): handle incomplete child collections
2026-10-07 19:52:16 +08:00
morlutoandGitHub 348c65c7ec fix(javascript): handle deep static member paths (#939) 2026-10-07 19:51:43 +08:00
morlutoandGitHub 9e39795895 fix(android): bound fragmented JADX MCP frame memory (#937)
* perf(android): avoid repeated MCP frame copies

* fix(android): bound fragmented MCP frame memory

* style(android): format JADX transport changes
2026-10-07 19:51:08 +08:00
Kaoru0822andClaude Opus 5.5 2e81e867b2 fix(apple): count only real framework version directories
A file such as Versions/.DS_Store beside the only real version made the
framework look multi-versioned, so its Info.plist became unknown with a
misleading limitation. Version directories are now limited to entries
observed as directories or implied by deeper paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:46:53 +00:00
ysopwnableandGitHub 810fe53e79 ci: consolidate macOS native baseline checks (#935) 2026-10-07 19:44:03 +08:00
ysopwnableandGitHub 28713f9b4d refactor(managed): group capability workflows and verification (#926) 2026-10-07 19:22:04 +08:00
ysopwnableandGitHub 2ccc86cd13 docs: add website and showcase links to READMEs (#930)
docs: add prominent website and showcase links to READMEs
2026-10-07 19:21:50 +08:00
morlutoandGitHub dcde958450 perf(javascript): analyze member chains without recursive copying (#932)
* fix(javascript): traverse deep semantic member chains iteratively

* fix(javascript): preserve deep initializer and pattern analysis
2026-10-07 19:09:25 +08:00
ysopwnableandGitHub e6ba9c3af3 fix(javascript): retain recovered call argument positions (#929) 2026-10-07 19:08:45 +08:00
37d2ebac4c fix(artifacts): accept the bundle subject when extracting an app bundle (#891)
* fix(artifacts): accept the bundle subject when extracting an app bundle

For a .app target the session target is the bundle's executable, while the
artifact provider reports the bundle itself as the subject. The subject
check exempted inventory_artifact and inspect_artifact but not
extract_artifact, so extracting any .app failed with execution_failure after
the output tree had already been written, and the output path was never
reported.

* refactor(binary): rename artifact bundle subject helper

isArtifactInventorySubject also covers extract_artifact for .app
bundles, so name it for the shared bundle-subject exemption.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-07 19:04:55 +08:00
45fcd28b40 fix(javascript): resolve bare source-map and worker URLs next to the script (#890)
* fix(javascript): resolve bare source-map and worker URLs next to the script

A sourceMappingURL like `main.js.map` (the default of webpack, esbuild,
rollup, Vite and rolldown) and `new Worker("worker.js")` were resolved as
module specifiers, so the bare name was looked up as an npm package and the
map or worker script was never linked. Resolve them as URLs relative to the
declaring script.

* fix(javascript): tighten url-reference resolution edge cases

Use looksExternal for non-module contexts so protocol-relative URLs
cannot be misread as root-absolute paths, cover url-reference unit
cases, and assert bare service-worker script resolution.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-07 19:04:15 +08:00
Kaoru0822andClaude Opus 5.5 9226ade511 fix(apple): pair bridge hypotheses within one application root
A ZIP or DMG holding two applications produced bridge candidates from
scripts in one app to native code in the other. Candidates now pair only
components owned by the same application root; components outside every
root pair only with each other.

A versioned framework with several real version directories now keeps
its Info.plist unknown even when only one version has a plist, because
Versions/Current, which is not inventoried, decides which applies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:02:47 +00:00
Kaoru0822andGitHub aa8082ea6c Merge pull request #913 from morluto/fix/uikit-runtime-connections
fix(artifacts): project UIKit runtime outlet and event connections
2026-10-07 04:00:25 -07:00
Kaoru0822andGitHub b3f013fa18 Merge pull request #899 from morluto/fix/interface-builder-plist-data
fix(artifacts): decode flat keyed-archive NIBs with data and shared references
2026-10-07 03:59:54 -07:00
Kaoru0822andGitHub 2ed89e6529 Merge pull request #910 from morluto/feat/ios-app-bundles
feat(native): open iOS-style and iOS-on-Mac wrapper app bundles
2026-10-07 03:59:41 -07:00
Kaoru0822andGitHub 88c940b165 Merge pull request #922 from morluto/feat/macos-bundle-anatomy
feat(apple): project macOS bundle anatomy in application graphs
2026-10-07 03:59:24 -07:00
Kaoru0822andGitHub d93ebec9df Merge pull request #921 from morluto/fix/dmg-apfs-detach
fix(artifacts): accept APFS image disks ejected with their container
2026-10-07 03:59:12 -07:00
morluto fd552a8c4b chore: merge main and regenerate combined catalog metadata 2026-10-07 18:44:34 +08:00
morluto be121d87eb chore: merge main and preserve keyed archive regressions 2026-10-07 18:44:01 +08:00
ysopwnableandGitHub 4f25ee33dc docs(website): add worked guides and keep Pages publishing manual (#925)
* fix(pages): reserve publishing for the REA website

* docs(website): add worked guides and agent prompt examples

* ci(website): check relevant PRs and publish only on demand

* docs(website): clarify IDA coverage and analysis session lifetime
2026-10-07 18:40:11 +08:00
morlutoandGitHub b9292516f4 fix(process): retain provider output until streams close (#928) 2026-10-07 18:39:10 +08:00
morlutoandGitHub f826aa893f fix(browser): preserve evidence through deep AST analysis (#924) 2026-10-07 18:36:18 +08:00
Kaoru0822andClaude Opus 5.5 55249704c5 fix(apple): attribute only application-root components in macOS inventories
Directory, ZIP, and DMG inventories can hold installers, disk-image
extras, and AppleDouble sidecars beside the app. Those components were
classified as the application's frameworks, libraries, JavaScript, and
metadata, and fed runtime families and bridge candidates. For these
formats, components outside every application root are now left out,
with a limitation that counts them. IPA projection is unchanged.

A selected directory whose name ends in .app is now its own root by
identity, so an empty or partial inventory still reports it. iOS is
claimed only for shallow roots that have an Info.plist, so an empty root
reports no platform.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:30:47 +00:00
Kaoru0822 b956bd589f Merge branch 'fix/dmg-apfs-detach' into feat/macos-bundle-anatomy 2026-10-07 10:28:20 +00:00
Kaoru0822andClaude Opus 5.5 62634e4c01 fix(artifacts): keep the post-snapshot detach check out of provenance
The inventory snapshot captures reader provenance before close() runs,
so a `hdiutil info -plist` entry recorded during cleanup never reached
CLI or MCP results. Detach entries already share that timing. Record
neither instead of implying the query is observable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:28:03 +00:00
Kaoru0822andGitHub 28fd49f0f6 Merge pull request #889 from morluto/fix/managed-comparison-undecoded-signatures
fix(dotnet): compare members with undecoded signatures by exact identity
2026-10-07 03:22:54 -07:00
Kaoru0822andGitHub 4f5110306d Merge pull request #895 from morluto/fix/javascript-indexeddb-open-version
fix(javascript): keep storage open versions out of XHR endpoints
2026-10-07 03:22:12 -07:00
morlutoandGitHub 9a15525298 perf(cli): defer unused catalog work during command startup (#920)
perf(cli): defer catalog digests and use canonical cache scope
2026-10-07 18:16:10 +08:00
27c272bd9a fix(artifacts): read AppKit connectors from flat keyed-archive NIBs (#900)
* fix(artifacts): read AppKit connectors from flat keyed-archive NIBs

Flat AppKit NIBs archive NSNibOutletConnector and NSNibControlConnector
endpoints and the outlet or selector name under NSSource, NSDestination,
and NSLabel. The keyed-archive parser read only source, destination,
and label, so it skipped every such connector: XLD.app's 64 decoded NIBs
had no outlet or target-action edges. The NS-prefixed keys now follow the
same source and destination convention as compiled NIBArchive documents.

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

* fix(artifacts): read AppKit control connectors from the control to its target

NSNibControlConnector archives NSSource as the sending control and
NSDestination as the action target. The previous commit inverted them
like the unprefixed fields, so every flat AppKit action ran from the
target to the control. The action trace selects a control and follows
control -> action, and the graph keys the selector by its target, so
neither could resolve these actions. The NS-prefixed keys are now read
as source and destination for outlets and actions alike.

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

* fix(artifacts): key AppKit connector endpoints by their node identity

An NSSource or NSDestination object with an identifier field was
referenced by that identifier, but its node is keyed by its authored
object ID or archive UID. The connector then pointed at a synthetic
unknown node. NS-prefixed endpoints now use the referenced object's
node identity.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:08:02 +08:00
Kaoru0822andClaude Opus 5.5 dd3e7aef0d feat(apple): project macOS bundle anatomy in application graphs
project_apple_application_graph accepted only IPA inventories. It now
also projects macOS .app directories and ZIP or DMG inventories that
contain applications. It reports application roots, platforms, and
every nested bundle with its path-convention role, layout, parent,
Info.plist path, executable candidates, and signing paths:
- roles: app extensions, XPC services (including inside frameworks),
  frameworks, login items, system and driver extensions, plug-ins,
  resource bundles, and helper or nested applications;
- privileged helpers in Contents/Library/LaunchServices and launchd
  plists in Contents/Library/LaunchAgents or LaunchDaemons;
- direct files of Contents/Helpers;
- symlinks by path. Their targets are not inventoried, so versioned
  frameworks are described through their real Versions/<version>/
  directories.

AppleDouble sidecars (._* and __MACOSX/) are excluded from roles. A DMG
whose contents could not be inventoried is reported as partial. Other
inventory formats remain input errors. The projection still reads no
plist or CMS semantics; it points callers at inspect_plist.

The result schema is additive. projection_id now covers the new fields,
so IPA projection IDs differ from earlier versions.

verify:macos-bundle compiles a source-owned app with clang (CLT only).
It packs it as a directory, a ditto ZIP, and an APFS DMG, and checks
identical anatomy through the CLI with stdio MCP parity. It runs in the
macOS CI job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:06:48 +00:00
Kaoru0822andClaude Opus 5.5 b72c079a9a fix(artifacts): accept APFS image disks ejected with their container
An APFS disk image attaches as an image disk plus a synthesized APFS
container, and hdiutil reports both as whole disks. Detaching the
container also ejects the image disk, so the reverse-order detach then
failed with "No such file or directory" and every APFS DMG inventory
ended in "DMG detach or mount-root cleanup failed". APFS is the default
format of `hdiutil create` on current macOS.

When a detach fails, ask `hdiutil info -plist` whether the device is
still attached. A device that is gone counts as detached; one that is
still listed, or whose state cannot be read, remains a cleanup failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:06:37 +00:00
morluto 1d769cd261 chore: merge main and preserve UIKit and AppKit connections 2026-10-07 18:02:05 +08:00
morluto 071cfd1bb6 chore: merge main and preserve plist and AppKit regressions 2026-10-07 18:02:05 +08:00
ysopwnableandGitHub f0a68dce60 refactor(artifacts): isolate JavaScript file acquisition (#916) 2026-10-07 17:57:33 +08:00
Kaoru0822andGitHub 2db668f6ca fix(cli): emit empty structured filter projections (#914) 2026-10-07 17:51:10 +08:00
morlutoandGitHub 7a9f587c3a perf(dotnet): index declaring-type ranges once per metadata table (#918)
perf(dotnet): index ordered declaring-type ranges once per table
2026-10-07 17:48:39 +08:00
900d7c8606 fix(artifacts): project compiled AppKit actions from the control to its target (#905)
* fix(artifacts): project compiled AppKit actions from the control to its target

NSNibControlConnector archives the sending control as NSSource and the
action target as NSDestination; a nil target is the first responder. The
NIBArchive projection swapped them for control connectors, so every
compiled AppKit action ran from its target to the control. The action
trace, which follows control -> action, could not find them, and the
selector node was keyed by the control instead of the target class. When
the target was nil, the swapped source was null and the action was
dropped: Chess lost 29 menu actions, KeepingYouAwake 7.

Connectors now keep NSSource -> NSDestination. The NIBArchive test
encoder moves to a fixture module so a bundle test can build compiled
NIBs.

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

* test(artifacts): report observed action routes when a compiled route is missing

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

* test(artifacts): report compiled connector records when a storyboard route is missing

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

* test(artifacts): write the storyboard fixture's action in Cocoa syntax

MacFixture.storyboard declared its button action with the iOS attribute,
<action selector="buildTapped:" destination="vc1">. Cocoa documents
name an action's receiver with target=. ibtool compiled the iOS spelling
into an NSNibControlConnector whose NSSource is the view controller
placeholder and whose NSDestination is the button. That is the reverse
of what Xcode-authored AppKit NIBs contain (Music, Chess,
KeepingYouAwake, and Transmission all archive NSSource as the control),
and it is what the old inversion had matched. With target= the compiled
fixture archives the button as NSSource. The temporary connector dump is
removed; the observed routes remain in the assertion message.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:40:18 +08:00
morlutoandGitHub 62b37111e7 fix(javascript): handle deep AST traversal and addition without stack overflow (#915)
* fix(javascript): avoid recursion and eager ancestry copies in deep ASTs

* fix(javascript): traverse static analysis and bundler ASTs iteratively
2026-10-07 17:40:05 +08:00
morluto 5574287101 chore: merge main and resolve PR #889 conflicts 2026-10-07 17:28:56 +08:00
morluto 5b5b76a583 chore: merge main and resolve PR #895 conflicts 2026-10-07 17:28:56 +08:00
morluto 4d2faef37b chore: merge main and resolve PR #910 conflicts 2026-10-07 17:28:55 +08:00
morluto 96a9f32dc1 chore: merge main and resolve PR #899 conflicts 2026-10-07 17:28:55 +08:00
ysopwnableandGitHub f6a58f1062 refactor(javascript): group domain semantics and contracts (#907) 2026-10-07 17:16:58 +08:00
Kaoru0822andGitHub d5f28fb29c Merge pull request #887 from morluto/fix/managed-native-boundaries-non-managed
fix(dotnet): inspect native boundaries of non-managed and malformed PEs
2026-10-07 02:12:48 -07:00
Kaoru0822andGitHub 15ab5dbcaf Merge pull request #894 from morluto/fix/plist-prototype-key-reporting
fix(plist): report omitted __proto__ entries in signatures and Interface Builder
2026-10-07 02:12:34 -07:00
ysopwnableandGitHub 4e3953cc45 feat(website): introduce REA and the DX-Ball reconstruction (#906)
* feat(website): add local introduction and DX-Ball showcase

* feat(website): illustrate REA and DX-Ball reconstruction

Add two explanatory figures and an interactive comparison of recorded DX-Ball x86 instructions with maintained C. Preserve the full instruction listing, caller excerpt, constant reads, figure prompts and evidence provenance.

Validation: Chrome preview at local root and /rea/; five comparison steps; 22 clipboard checks; keyboard navigation; responsive widths 1440, 768, 390 and 320. Verify assembly against saved REA Evidence and C against the pinned public source. Existing execution and compiler results are cited, not rerun.

* docs(website): show the REA queries behind source recovery

Present compact function, caller and byte-read request excerpts alongside recorded evidence. Keep the overview and case narrative focused on REA, with provider provenance in contributor notes. Replace raster references with maintained SVG source diagrams while retaining the established layout and image-generation prompts.

Validation: local and /rea/ Chrome previews, five comparison steps, 22 clipboard checks, responsive widths and keyboard navigation passed. SVG XML and all 41 local links/assets passed checks.
2026-10-07 17:11:06 +08:00
Kaoru0822andClaude Opus 5.5 8b243877c2 fix(artifacts): project UIKit runtime outlet and event connections
Compiled UIKit NIBs and storyboards archive outlets and control actions
as UIRuntimeOutletConnection and UIRuntimeEventConnection with
UISource, UIDestination, and UILabel. The NIBArchive projection
recognized only AppKit *Connector classes with NS-prefixed keys, so an
iOS app produced no outlet or action edges: Stash's 64 decoded documents
yielded none. These connections are now projected with the same
source -> destination roles, an event connection carries its
UIEventMask as ui_event_mask, UIClassSwapper reports its UIClassName,
UIProxyObject is a placeholder, and connection records are no longer
emitted as unknown objects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:10:53 +00:00
Kaoru0822andClaude Opus 5.5 418a5562f6 fix(native): keep the resolved bundle plist and permission failures
- Carry the Info.plist that declared the program file on the target as
  bundleInfoPlist, so inspect_plist defaults to it even when the program
  file is a symlink into another bundle directory.
- Accept a readable Info.plist symlink, as reading the plist directly did.
- Treat only a missing path as an absent layout or wrapper; a permission
  denial now reaches the resolver's permission-denied classification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:10:29 +00:00
Kaoru0822andClaude Opus 5.5 7714e763d9 fix(dotnet): name the shared key in exact-signature ambiguities
An ambiguous exact-signature group was described as sharing a
non-name identity key, but that key includes the declared type and name,
so names cannot separate it. Ambiguity limitations now name the key the
group actually shares.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:08:32 +00:00
morlutoandGitHub df7151ba30 perf(providers): assemble fragmented responses once per line (#911) 2026-10-07 17:07:00 +08:00
Kaoru0822andClaude Opus 5.5 53bfa7e3f7 test(artifacts): write the storyboard fixture's action in Cocoa syntax
MacFixture.storyboard declared its button action with the iOS attribute,
<action selector="buildTapped:" destination="vc1">. Cocoa documents
name an action's receiver with target=. ibtool compiled the iOS spelling
into an NSNibControlConnector whose NSSource is the view controller
placeholder and whose NSDestination is the button. That is the reverse
of what Xcode-authored AppKit NIBs contain (Music, Chess,
KeepingYouAwake, and Transmission all archive NSSource as the control),
and it is what the old inversion had matched. With target= the compiled
fixture archives the button as NSSource. The temporary connector dump is
removed; the observed routes remain in the assertion message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:06:36 +00:00
morlutoandGitHub e52fe9719f perf(call-path): prune dead ends and enumerate without recursion (#909) 2026-10-07 17:03:44 +08:00
Kaoru0822andClaude Opus 5.5 d421d3f286 test(artifacts): report compiled connector records when a storyboard route is missing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:00:41 +00:00
Kaoru0822andClaude Opus 5.5 aeebba391e feat(native): open iOS-style and iOS-on-Mac wrapper app bundles
Every native command rejected an iOS app installed on an Apple silicon
Mac, such as /Applications/Stash.app, with "app has no readable
CFBundleExecutable": the resolver understood only the macOS layout,
Contents/Info.plist with Contents/MacOS/<executable>. iOS-style bundles,
including those extracted from an IPA, keep Info.plist and the program
file at the bundle root, and an iOS-on-Mac wrapper holds that bundle in
Wrapper/<Name>.app.

Bundle resolution moves to AppBundleExecutable and accepts the macOS
layout, then the flat layout, then a wrapper's single real Wrapper/*.app
directory. The WrappedBundle symlink is never followed, and the program
file must stay inside its bundle. inspect_plist defaults to the Info.plist
beside the program file when the bundle has no Contents/Info.plist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:59:28 +00:00
morluto c78fe6990b docs: publish VitePress documentation on GitHub Pages 2026-10-07 16:58:04 +08:00
morlutoandGitHub eab34a9d8c fix(android)!: preserve JVM settings and reuse metadata sessions (#908)
* perf(graph): avoid repeated traversal edge scans and copies

* fix(android)!: preserve JVM settings and reuse metadata sessions

Read class metadata without generating application source, retain immutable serialized APK sessions, and join owned cleanup on cancellation and shutdown.

BREAKING CHANGE: Android analysis now requires a full JDK 17 or newer with its compiler module for the packaged Java source bridge.
2026-10-07 16:50:41 +08:00
Kaoru0822andClaude Opus 5.5 73370e1c06 fix(dotnet): treat ambiguous undecoded members as possible counterparts
An undecoded member in an ambiguous exact-signature group stays unpaired,
like a one-sided member, but the counterpart check considered only
one-sided members. A decoded member with the same declared type and name
on the other side was therefore reported as removed or added. Ambiguous
candidates now count as possible changed counterparts too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:48:01 +00:00
Kaoru0822andClaude Opus 5.5 c60b737f7d test(artifacts): report observed action routes when a compiled route is missing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:46:57 +00:00
Kaoru0822andClaude Opus 5.5 b08228d19f fix(artifacts): project compiled AppKit actions from the control to its target
NSNibControlConnector archives the sending control as NSSource and the
action target as NSDestination; a nil target is the first responder. The
NIBArchive projection swapped them for control connectors, so every
compiled AppKit action ran from its target to the control. The action
trace, which follows control -> action, could not find them, and the
selector node was keyed by the control instead of the target class. When
the target was nil, the swapped source was null and the action was
dropped: Chess lost 29 menu actions, KeepingYouAwake 7.

Connectors now keep NSSource -> NSDestination. The NIBArchive test
encoder moves to a fixture module so a bundle test can build compiled
NIBs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:39:27 +00:00
Kaoru0822 767c6c9a95 Merge remote-tracking branch 'origin/main' into fix/managed-comparison-undecoded-signatures
# Conflicts:
#	docs/product-catalog.json
#	docs/verification/managed-conformance-manifest.json
#	skills/reverse-engineer-anything/SKILL.md
#	src/generatedMcpToolCatalog.ts
2026-10-07 08:36:16 +00:00
ysopwnableandGitHub f430c416c7 feat(browser): trace captured website source-map locations (#897)
* refactor(browser): share captured-script acquisition boundaries

Reuse stable capture/source acquisition and identity validation for module
and source-map analysis. Preserve host I/O, malformed input, unavailable
source and digest mismatch diagnostics.

Refs #851

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

* feat(browser): trace captured website source-map locations

Refs #851. Preserve selected capture/map byte identity, duplicate mappings and embedded source evidence through the pinned upstream codec.

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

* test(browser): verify source-map artifacts and synchronize generated contracts

Cover byte-preserving map reads and actionable filesystem constraints. Refresh generated public schemas and metadata on current main.

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

* fix(browser): preserve inert source-map URL context

The selected URL is local codec context and grants no transport authority. Preserve its reported text and verify normal/userinfo contexts through real public interfaces, alongside independent manifest byte identity.

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-07 16:21:18 +08:00
ysopwnableandGitHub 4801a24089 ci: verify native arm64 and Intel macOS workflows (#898) 2026-10-07 16:16:55 +08:00
Kaoru0822andClaude Opus 5.5 5afd8a850b fix(plist): keep character references beyond Unicode literal
Reserving the __proto__ placeholder decodes the whole document, so
<![CDATA[&#x110000;]]> or a comment with an out-of-range reference
threw a RangeError, and every XML plist reader failed on a valid
document. References beyond U+10FFFF now stay as written. The XML
decoder still rejects them outside comments and CDATA.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:12:59 +00:00
Kaoru0822andClaude Opus 5.5 9f48a76e11 fix(javascript): keep literal HTTP methods on storage-named receivers
A receiver spelled indexedDB or caches does not prove the binding, so
indexedDB.open("GET", "/api") keeps its endpoint. Such receivers
still need a standard HTTP method literal: a computed first argument or
an uppercase database name such as "APP" reads as storage, not XHR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:12:14 +00:00
Kaoru0822andClaude Opus 5.5 2a4667bb79 fix(artifacts): decode flat keyed-archive NIBs with data and shared references
decode_interface_builder failed on every flat (keyed-archive) NIB that
holds plist data or dates. Archived colors keep their components as
data, so this covers nearly every AppKit NIB: XLD.app decoded 0 of 64.
The decoded Uint8Array and Date values failed JSON validation. They now
use the shared plist projection, which keeps them as explicit
$plist_type values.

Decoding those archives exposed a second failure. The keyed-archive
parser inlined every UID reference up to 16 levels for every object, so
an archive whose views reference their window, superview, and next key
view expanded exponentially. A 9 KB NIB took 3 s, and a 158 KB
MainMenu.nib did not finish. A reference now resolves to its string, or
to a stub naming the referenced object's archive UID, authored ID, and
class. The hierarchy walk visits each object once. The parser also reads
XML CF$UID references and treats UID 0 as nil.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:10:25 +00:00
ysopwnableandGitHub 0804722cdf refactor(javascript): group application workflows and verification (#896) 2026-10-07 16:03:37 +08:00
Kaoru0822andClaude Opus 5.5 898b0f62d7 fix(javascript): keep storage open versions out of XHR endpoints
#877 reads a computed-method `.open(method, url)` as XMLHttpRequest when
the second literal could be a URL. IndexedDB uses the same callee name
with a database name and version, so `indexedDB.open(name, "2")` was
published as the network endpoint "2". Recognized storage `open` calls
(IndexedDB and Cache Storage) are no longer read as XHR. Both the bundle
and static analyzers still report them as storage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 07:54:09 +00:00
Kaoru0822andClaude Opus 5.5 6729ee7636 fix(plist): report omitted __proto__ entries in signatures and Interface Builder
#885 decodes XML plists that contain a __proto__ key by omitting those
entries, but signature entitlements and Interface Builder plists dropped
the omission count. inspect_signature returned an entitlement set that
looked complete, and archive_decode stayed complete. Both now report the
omission: a signature limitation, and a partial archive_decode facet with
reason dictionary_entries_omitted and a per-document limitation.

The placeholder that stands in for __proto__ was reserved against the raw
XML text, so an entity-encoded key such as &#95;&#95;rea_prototype_key__
decoded to the placeholder and was omitted as if it were __proto__. The
placeholder is now reserved against the decoded text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 07:51:37 +00:00
Kaoru0822andClaude Opus 5.5 e5ba6934e4 fix(dotnet): keep admitted CLI header facts when metadata is unreadable
When the CLI header parsed but its metadata could not be admitted, the
native-boundary fallback reported hard-coded false flags and zero header
values. A corrupt-metadata ReadyToRun image therefore appeared IL-free
and non-native. The fallback now reports the parsed CLI header and
derives the summary from it as the full inspection does. When the CLI
header itself is unreadable, a limitation says the values are defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 07:47:44 +00:00
Kaoru0822andGitHub faaee693d5 Merge pull request #885 from morluto/fix/plist-prototype-keys
fix(plist): read XML plists containing a __proto__ key
2026-10-07 00:46:50 -07:00
Kaoru0822andGitHub 05f43ed388 Merge pull request #877 from morluto/fix/javascript-computed-xhr-method
fix(javascript): keep XHR and client endpoints that receiver spellings hid
2026-10-07 00:46:23 -07:00
Kaoru0822andClaude Opus 5.5 76b70203d9 fix(dotnet): keep a decoded member unknown beside an undecoded counterpart
A field that changed from a decodable to an undecoded signature was
reported as removed on the decoded side and unknown on the other. The
undecoded member with the same declared type and name may be that field
with a changed signature, so its decoded counterpart is now unknown too,
with a counterpart-signature-not-decoded limitation.

The compare_managed_members description now names the exact declared
type, name, and raw signature tier and the undecoded handling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 07:45:16 +00:00
ysopwnableandGitHub b808e2d6bb refactor(provider): declare deep capabilities from source (#892) 2026-10-07 15:41:50 +08:00
ysopwnableandGitHub 4037c6545d docs: add concise lawful-use disclaimer (#888) 2026-10-07 15:18:24 +08:00
Kaoru0822andClaude Opus 5.5 d52af3b321 fix(dotnet): compare members with undecoded signatures by exact identity
`compare_managed_members` gave undecoded (unsupported or malformed)
signatures no match key at all. In complete inventories every such member
was then reported as removed on one side and added on the other, even
for byte-identical assemblies: comparing PSReadLine.dll with itself
reported 457 added and 457 removed members, mostly generic
instantiations the signature decoder does not support.

The raw signature blob is the exact signature, so methods and fields now
pair by exact declared type, name, and raw signature without decoding.
An undecoded member that still has no counterpart is reported as unknown
with a `signature-not-decoded` limitation, never added or removed, because
no shape round could look for a changed counterpart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:15:13 -07:00
Kaoru0822andClaude Opus 5.5 d98dcfa8c2 fix(dotnet): inspect native boundaries of non-managed and malformed PEs
`inspect_managed_native_boundaries` wrote the PE classification
(`not-managed` or `malformed`) into `coverage.state`, which accepts only
`complete`, `partial`, or `unavailable`. Its own result validation then
threw, so every native DLL and every PE with unreadable CLI metadata
failed as a generic execution failure advising a retry. The sibling
artifact and member inspectors succeed on the same files.

Report these files with `unavailable` coverage, as the member inspector
does, keeping the metadata status and parse issues.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:05:58 -07:00
ysopwnableandGitHub dd114d3980 refactor(binary): group session selection and snapshot ownership (#886) 2026-10-07 15:05:29 +08:00
Kaoru0822andGitHub ab985c6a10 Merge pull request #880 from morluto/fix/native-macho-echoed-paths
fix(native): keep echoed Mach-O paths out of load commands and symbols
2026-10-07 00:02:14 -07:00
Kaoru0822andClaude Opus 5.5 e32028d1b5 fix(javascript): keep bare relative URLs for computed-method XHR
`xhr.open(method, "api/users")` is a same-origin request, but the
computed-method fallback required an absolute URL or a path prefix.
Keep the literal unless it is provably not a URL: a Node fs open flag,
a reserved browsing-context name, or a call on a receiver spelled as a
browsing context or document.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:01:16 -07:00
ysopwnableandGitHub 01be082e9e feat(browser): trace captured website module imports (#879)
* feat(browser): trace captured website module imports

Compose verified exported scripts, inert native import syntax and a narrow
Chromium resolution port. Preserve query/fragment identity, import-map context,
all reported capture candidates and explicit unknown execution. Share named
CLI/MCP contracts and retain native reports, diagnostics and cleanup failures.

Add focused boundary regressions and a real module-loading oracle to the
existing conditional Chrome lane. No captured application code is executed
by module tracing, and no selected website asset is refetched.

Refs #851, #774

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

* fix(browser): preserve module contexts and source bindings

Keep capture completeness inline, validate actual source text against its
digest, and separate unsupported URL contexts from malformed input.
Use the importer origin for native module resolution and the selected map
base independently, including mixed HTTP/HTTPS and escaped HTML attributes.

Refs #851, #774

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-07 14:59:52 +08:00
Iwakurakaoru0822andClaude Opus 5.5 a08be51e9f fix(plist): read XML plists containing a __proto__ key
The `plist` XML decoder throws on the legal dictionary key `__proto__`
(its CVE-2022-22912 guard). An app whose XML Info.plist held such an
entry could not be opened at all ("app has no readable
CFBundleExecutable"), and entitlements, XML keyed archives, and
Interface Builder plists failed the same way. On the plutil JSON route,
validation silently dropped the key from `inspect_plist` results.

Decode XML plists through a shared reader that substitutes such keys
before the decoder runs, then omits and counts those entries.
`inspect_plist` and `inspect_keyed_archive` report the omission as a
limitation instead of failing or dropping it silently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:58:59 -07:00
ysopwnableandGitHub 507e8b3c7b refactor(firmware): group layered source and verification (#878) 2026-10-07 14:44:52 +08:00
Iwakurakaoru0822andClaude Opus 5.5 e3e7664665 fix(javascript): keep literal-method and URL-literal requests on aliased receivers
Merge main (#874) and resolve the endpoint heuristics together. Receiver
spellings do not prove bindings: `parent.open("GET", "/api")` may be a
local XHR, `new Headers()` may be an imported client, and
`api.headers.get("/v1/x")` may be a nested client.

A literal HTTP method is read as XHR on any receiver; the browsing-
context exclusion now applies only to computed methods. A get/delete
literal that is a URL or path stays an endpoint even when the receiver
is spelled as a keyed collection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:44:34 -07:00
Kaoru0822andGitHub c371d6e1e9 Merge pull request #871 from morluto/fix/javascript-template-literal-static-strings
fix(javascript): read substitution-free templates as exact strings
2026-10-06 23:42:52 -07:00
Kaoru0822andGitHub 14f9b3d931 Merge pull request #875 from morluto/fix/native-plist-selection-failures
fix(native): report why a selected plist cannot be inspected
2026-10-06 23:42:14 -07:00
Iwakurakaoru0822andClaude Opus 5.5 37fe105ba1 fix(native): keep echoed Mach-O paths out of load commands and symbols
otool and dyld_info print the operand path before a slice's records,
and the Mach-O parsers read that header as output. A file name holding
`Load command` blocks or `(from lib)` lines therefore added a forged
LC_MAIN entrypoint, corrupted the Mach header fields, and injected
imports and exports into `inspect_macho`.

Strip the exact echoed operand header (`<path>:`, `<path> (architecture
<arch>):`, `<path> [<arch>]:`) before parsing. Output without that
header, such as replayed captures, is parsed unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:40:35 -07:00
Kaoru0822andGitHub 9075ce71ad Merge pull request #874 from morluto/fix/javascript-http-method-endpoints
fix(javascript): keep keyed-collection reads out of endpoints
2026-10-06 23:40:24 -07:00
Iwakurakaoru0822andClaude Opus 5.5 00f707b250 fix(javascript): filter keyed-collection receivers only for get/delete
The collection APIs share only `get` and `delete` with HTTP clients, so
`api.headers.post("/v1/headers")` and `api.searchParams.request("/s")`
keep their endpoints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:34:47 -07:00
Iwakurakaoru0822andClaude Opus 5.5 ae849a7281 fix(javascript): never read window or document open as XHR
`window.open(url, "/preview")` has a computed first argument and a
path-shaped browsing-context name, so the computed-method XHR fallback
reported the target name as an endpoint. Browsing-context and document
`open` calls never take an XHR method and request URL; skip them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:32:50 -07:00
Iwakurakaoru0822andClaude Opus 5.5 5738a0cf8c fix(native): report plist permission denials as access failures
A regular plist that the host refuses to open made both plutil
conversions fail, which was reported as an undecodable property list.
Open the file before running plutil and report EACCES/EPERM as a
target access failure naming the path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:31:14 -07:00
ysopwnableandGitHub 895079dd09 docs: add DX-Ball reconstruction showcase (#876)
* docs: add DX-Ball reconstruction showcase

* docs: highlight DX-Ball reconstruction results
2026-10-07 14:28:21 +08:00
Iwakurakaoru0822andClaude Opus 5.5 eb68761eb0 fix(javascript): keep keyed-collection reads out of endpoints
Static endpoint extraction read the first literal argument of any call
named get, post, put, patch, delete, or request as an HTTP request URL.
Reads of a provable keyed collection also became network endpoints in
application and web bundle analysis: `new URLSearchParams(...).get("q")`,
`response.headers.get("content-type")`, `new Map().get("key")`, and
`url.searchParams.get("page")`.

Skip these calls only when the receiver is a constructed Map, WeakMap,
Set, WeakSet, Headers, FormData, or URLSearchParams, or a platform
`headers`/`searchParams` property. Any other receiver may still be an
HTTP client with a base URL, so `api.get("users")` keeps its endpoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:28:19 -07:00
Iwakurakaoru0822andClaude Opus 5.5 602d0021d4 fix(javascript): keep computed-method XHR request URLs
A computed XHR method, as in `xhr.open(method, "/api")`, no longer
discarded an endpoint that main reports. Keep the URL when it is an
HTTP(S)/WebSocket URL or a path; fs flags and window targets such as
`r` and `_blank` still produce no endpoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:25:48 -07:00
Iwakurakaoru0822andClaude Opus 5.5 0674facd46 fix(native): report why a selected plist cannot be inspected
`inspect_plist` turned a missing selected plist, a directory, or a file
that is not a property list into a generic execution failure that
advised retrying and running `rea doctor`. A bare Mach-O without
Contents/Info.plist failed the same way.

Check the resolved path before running plutil and keep plutil's decode
diagnostic. A caller-selected path now fails as invalid input naming
the path and the reason; a missing default Info.plist is unavailable for
the target. The CLI no longer sends Contents/Info.plist as an explicit
selection, so its default matches the MCP default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:16:13 -07:00
ysopwnableandGitHub 8e8921d40c refactor(android): group layered source and verification (#873) 2026-10-07 14:09:57 +08:00
Kaoru0822andGitHub 63e0822cc7 Merge pull request #872 from morluto/fix/javascript-xhr-open-endpoints
fix(javascript): read open() endpoints only from XHR method/URL pairs
2026-10-06 23:09:13 -07:00
Kaoru0822andGitHub cfd2b0c75d Merge pull request #870 from morluto/fix/keyed-archive-uid-named-keys
fix(artifacts): read keyed archive dictionaries keyed UID as dictionaries
2026-10-06 23:03:09 -07:00
Iwakurakaoru0822andClaude Opus 5.5 1be370543f fix(javascript): read open() endpoints only from XHR method/URL pairs
Static endpoint extraction treated any `.open(_, "literal")` call as
`XMLHttpRequest.open(method, url)`. In application and web bundle
analysis, `fs.open("/tmp/data.txt", "r")` became the network endpoint
`r` and `window.open(url, "_blank")` became the endpoint `_blank`.

Read the URL only when the first argument is a literal HTTP method: a
standard method in any case, or an uppercase extension token such as
WebDAV `PROPFIND`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:02:58 -07:00
Kaoru0822andGitHub dd90965058 Merge pull request #868 from morluto/fix/swift-demangle-arguments
fix(native): pass Swift symbols to swift-demangle as operands
2026-10-06 23:00:45 -07:00
Iwakurakaoru0822andClaude Opus 5.5 77cf757103 fix(javascript): read substitution-free templates as exact strings
Static application analysis read call arguments only from quoted string
literals. A template literal without substitutions denotes the same
exact string, yet `fetch(`https://x`)`, `localStorage.setItem(`k`)`,
`new Worker(`./w.js`)`, and `require(`./dep.js`)` produced no endpoint,
an unnamed storage node, or an unresolved reference expression instead
of the facts their quoted spellings produce. Electron IPC and data-effect
readers already accepted these templates.

Read substitution-free templates in the shared static string and
argument readers so both spellings produce the same graph.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:58:43 -07:00
ysopwnableandGitHub 021ba690b0 refactor: give Inspector its own capability directory (#869) 2026-10-07 13:53:25 +08:00
Iwakurakaoru0822 01779a6f61 Merge remote-tracking branch 'origin/main' into fix/swift-demangle-arguments
# Conflicts:
#	docs/product-catalog.json
#	docs/verification/managed-conformance-manifest.json
#	skills/reverse-engineer-anything/SKILL.md
#	src/generatedMcpToolCatalog.ts
2026-10-06 22:47:23 -07:00
Iwakurakaoru0822andClaude Opus 5.5 35c82b93eb fix(artifacts): read keyed archive dictionaries keyed UID as dictionaries
Foundation encodes `encode(_:forKey: "UID")` as a `$top` dictionary
whose single key is `UID`. The keyed archive projection treated every
one-key `UID`/`CF$UID` dictionary as a reference marker, so that root
became a malformed reference and the real link to its object was lost.

Treat a one-key dictionary as a UID only when its value is a number,
matching the binary decoder's UID shape and Foundation's XML `CF$UID`
rule; otherwise descend into it as an ordinary dictionary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:46:16 -07:00
Iwakurakaoru0822andClaude Opus 5.5 6c88094da4 fix(native): pass Swift symbols to swift-demangle as operands
`swift-demangle` parsed a symbol beginning with `-` as an option, so
`-help` replaced the results with usage text and the call failed as
unreadable output. A symbol containing a line feed split into extra
output records, and the CRLF-tolerant parser stripped a carriage return
that belonged to the final symbol.

Pass the symbols after `--`, split records on the LF the demangler
emits, and reject multi-line symbols in the shared CLI/MCP input schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:40:19 -07:00
e0fd8207a1 feat(application): wire Android and Apple inventory graph tools (#852)
* feat(application): wire Android and Apple inventory graph tools

Expose project_android_application_graph and project_apple_application_graph
through CLI and MCP so authenticated inventory Evidence can be projected
without a provider launch.

* fix(application): use producer root occurrence IDs in graph examples

Catalog examples constructed root occurrence IDs from artifact_id +
logical_path, but inventory validation authenticates "." with
{ root: root_artifact_id }. Align the examples and regenerate catalogs
after rebasing onto main (application family count 11).

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-07 13:34:48 +08:00
da0970672d refactor(domain): split Interface Builder keyed-archive parse from graph builder (#859)
* refactor(domain): split Interface Builder keyed-archive parse from graph builder

* fix(domain): drop unused Interface Builder type re-exports

Knip failed Static checks because the split re-exported object and
connection types that no caller imported.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-07 13:30:25 +08:00
676959ecb5 refactor(native): split Apple dispatch metadata decode facets (#855)
Extract Mach-O/FAT selection, Objective-C class/method/ivar decode, and
Swift conformance/witness facets so AppleDispatchMetadata stays a thin
orchestrator without changing decode semantics.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-07 13:22:31 +08:00
b7df5b7805 refactor(ghidra): extract createClient and load-image attestation (#856)
Move AnalysisClient construction into GhidraProviderClient and the DOS
load-image attestation path into GhidraLoadImageAttest so GhidraProvider
stays a thin candidate adapter without behavior changes.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-07 13:22:23 +08:00
4aa2b60c1d fix(native): keep echoed codesign paths out of signature fields (#865)
codesign echoes the executable path and signing identifier verbatim,
and an ad hoc identifier derives from the file name. A file name with
line breaks added forged Authority lines, a signed file named after
codesign's unsigned message was reported unsigned, and path text could
supply entitlements. Parse display fields after the exact echoed path
and identifier, classify unsigned output by exit status, and read
requirements and entitlements from stdout.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:20:12 +08:00
4e17be5705 fix(setup): read an OpenCode V2 server named type as a table entry (#866)
Only a string type is the V1 entry discriminator; a native mcp.servers
table may hold a server object named type.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:20:02 +08:00
ysopwnableandGitHub bb19c9477e refactor: colocate investigation record implementation (#867) 2026-10-07 13:18:39 +08:00
Kaoru0822andGitHub f608127635 Merge pull request #843 from morluto/fix/exact-empty-graph-identities
fix(javascript): keep exact empty values in graph identities
2026-10-06 22:11:07 -07:00
Kaoru0822andGitHub 957069d14d Merge pull request #864 from morluto/fix/opencode-v2-servers
fix(setup): register OpenCode V2 configurations in mcp.servers
2026-10-06 22:07:29 -07:00
Kaoru0822andGitHub 93091abb4d Merge pull request #863 from kaoru0822-kitauji/fix/smol-toml-advisory
fix(deps): remediate smol-toml advisories
2026-10-06 22:04:59 -07:00
Kaoru0822andGitHub c57f1b74e0 Merge pull request #857 from morluto/fix/app-xml-plist-executable
fix(target): decode app Info.plist executables with an XML parser
2026-10-06 22:04:47 -07:00
N0zoM1z0andGitHub 3427593daf build: verify resolved module boundaries (#862) 2026-10-07 13:02:28 +08:00
Iwakurakaoru0822andClaude Opus 5.5 bddeb634b5 fix(errors): report union input issues at the union's path
Zod reports union branch issues relative to the union, so a missing
union-typed argument such as trace_application_feature's seed was
reported at the request root, once per branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:02:25 -07:00
3b92a2bced fix(native): keep integral plist reals beyond the JSON range as reals (#860)
plutil's JSON spells an integral <real> such as 9007199254740992 like an
<integer>, so the JSON path labeled it an exact integer. When the JSON
holds an unsafe integral number, read element types from plutil's XML
conversion; numbers whose origin stays ambiguous are reported as decoded
with an explicit limitation.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:01:26 +08:00
8b9940a7b5 refactor(cli): split core analysis command registrar (#858)
Extract binary overview/procedure and native load-image/annotation
registrars so coreAnalysisCommands stays a thin wiring entry point.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-07 13:00:38 +08:00
Iwakurakaoru0822 b6d9acca63 Merge remote-tracking branch 'origin/main' into fix/exact-empty-graph-identities
# Conflicts:
#	docs/product-catalog.json
#	docs/verification/managed-conformance-manifest.json
#	skills/reverse-engineer-anything/SKILL.md
#	src/generatedMcpToolCatalog.ts
2026-10-06 22:00:37 -07:00
2d4a910fcd fix: preserve native UI failure reasons and dedupe version literals (#854)
Keep adapter/OS helper failures in diagnostics instead of replacing them
with a canned remediation sentence. Share browserVersionSchema, Node
support prose, and JADX heap/remediation constants so release pins stay
authoritative.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-07 13:00:24 +08:00
Iwakurakaoru0822andClaude Opus 5.5 35e5508fd0 fix(javascript): advertise the exact empty seed rule and match empty API keys
Express the empty-seed rule as a JSON-Schema-representable union, so
the advertised trace_application_feature schema rejects what the
server rejects, and match context-bridge seeds on the exact api_key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:00:14 -07:00
Iwakurakaoru0822andClaude Opus 5.5 69d8afefc1 fix(setup): register OpenCode V2 configurations in mcp.servers
OpenCode V2 reads native server entries from mcp.servers, and a native
entry takes precedence over a V1 mcp.<name> entry. After OpenCode
migrated a configuration, setup wrote an ignored V1 entry, status
reported REA as missing, and uninstall left the native entry in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:58:20 -07:00
Kaoru0822 c55ae3b9ec fix(deps): remediate smol-toml advisories 2026-10-06 21:55:36 -07:00
N0zoM1z0andGitHub 166dcd21d0 feat(javascript): recover readable modules with Wakaru (#848)
* feat(javascript): recover readable modules with Wakaru

Integrate pinned upstream source behind a typed optional recovery port,
preserve source identity and producer provenance, and publish verified
artifacts for existing JavaScript analysis through CLI and MCP.

Add focused real-tool CI with genuine bundler fixtures and bounded
resource, cancellation, cleanup, schema and packaged-artifact checks.

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

* fix(javascript): align recovery catalog and browser verification

Share canonical Wakaru provider identity, describe CLI mode options, and
format the new workflow test. Disconnect the real page transport after
its observation readiness progress instead of a fixed delay.

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

* refactor(javascript): consume the investigation evidence writer

Verify that the SDK recovery handler records complete Evidence through the newly shared investigation owner.

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)

* chore(test): account for isolated recovery fixture compilers

* fix(javascript): validate recovery paths before acquiring the engine

Return field-specific invalid_format diagnostics for relative paths and verify compiled CLI/MCP parity even when the optional engine is unavailable.

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-07 12:46:13 +08:00
Iwakurakaoru0822andClaude Opus 5.5 c182f72ad3 fix(target): decode app Info.plist executables with an XML parser
The pattern-based reader decoded entities twice, ignored numeric
character references and CDATA, and could match a commented or nested
CFBundleExecutable key, so valid app bundles reported a missing program.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:40:32 -07:00
Iwakurakaoru0822 6cc45e8dc9 Merge remote-tracking branch 'origin/main' into fix/exact-empty-graph-identities
# Conflicts:
#	docs/product-catalog.json
#	docs/verification/managed-conformance-manifest.json
#	skills/reverse-engineer-anything/SKILL.md
#	src/generatedMcpToolCatalog.ts
2026-10-06 21:36:34 -07:00
Iwakurakaoru0822andClaude Opus 5.5 e20da1f205 fix(javascript): match exact empty literal channels and seeds
Pair the same empty IPC channel, context-bridge key, or endpoint value
across application versions by its exact value, and accept an exact
empty feature seed while rejecting an empty containment seed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:35:59 -07:00
N0zoM1z0andGitHub 4a6c17ce83 refactor: declare auxiliary capabilities from source (#853) 2026-10-07 12:32:34 +08:00
Kaoru0822andGitHub c9f49db907 Merge pull request #846 from morluto/fix/plist-exact-large-integers
fix(native): report exact plist integers beyond the JSON number range
2026-10-06 21:31:49 -07:00
morluto b912b117f0 fix(test): disconnect Hopper fixture IPC on SIGTERM 2026-10-07 12:28:43 +08:00
morlutoandGitHub f72931d9dd test: eliminate fixture races and prune redundant scaffolding (#849)
* test: eliminate fixture races and prune redundant scaffolding

* fix(test): publish launcher capture readiness without renaming
2026-10-07 12:25:37 +08:00
5cbf06c7ad fix(ghidra): admit the 12.1 line and the install's JDK range (#840)
* fix(ghidra): admit the 12.1 line and the install's JDK range

The provider treated the verified Ghidra 12.1.4 build and JDK 21 as the only
legal installation, so a working 12.1.x package or a newer JDK was rejected
before analysis started.

Admission now follows the 12.1 release line and application.java.min/max.
A compatible build other than 12.1.4 keeps its observed version and records
that the bridge was verified against 12.1.4.

Fixes #821

* fix(ghidra): stop pinning the NativeAOT build to one version string

The build script still rejected every Ghidra release except the literal
12.1.4 and every javac except major 21. It now accepts the 12.1 line and
javac 21 or newer, compiles with --release 21, and records the observed
Ghidra version. The JADX engine version uses the audited release constant.

* fix(ghidra): report unverified builds on analysis results

Session open uses health only as a startup gate and discards that
execution, so the unverified-build limitation never reached CLI or MCP
results. Attach it to every Ghidra execution for that install.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-07 12:24:36 +08:00
N0zoM1z0andGitHub 11f27deddf docs: add README star history chart (#850) 2026-10-07 12:22:51 +08:00
bef4321afb fix(firmware): accept the verified Binwalk and Unblob release lines (#844)
* fix(firmware): accept the verified Binwalk and Unblob release lines

Exact --version equality rejected every build except Binwalk 3.1.0 and
Unblob 26.6.4, then stored that constant as the observed engine version.
Admit the 3.1 and 26.6 lines, keep the audited build as verified, and
report the banner that actually ran.

* chore(docs): regenerate skill metadata after firmware contract text

docs:check failed because SKILL.md lagged the Binwalk/Unblob release-line
tool descriptions.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-07 12:19:28 +08:00
3f1a77f68e fix(artifacts): report which path constraint failed (#847)
Artifact path failures told callers to "inspect the reported path" but
reported none: `ArtifactReaderFailure` messages such as
`Artifact path collision: ...` were dropped when converted to
`ArtifactOperationError`, leaving only `{ operation, reason: "path" }`.

Carry the failure message into `details.detail` for artifact tools and
JavaScript application analysis. Name both spellings in case-fold
collisions and include the JSON-escaped input in unsafe and
non-normalized path messages.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:17:16 +08:00
Kaoru0822andGitHub 880cca1bca Merge pull request #842 from morluto/docs/task-scoped-readiness
docs: show task-scoped readiness and provider preference in the quick start
2026-10-06 21:16:03 -07:00
Iwakurakaoru0822andClaude Opus 5.5 f85056d83e fix(native): keep plist XML element types for large numbers
The XML fallback inferred an integer from `Number.isInteger`, so an
integral `<real>` beyond 2^53 was misreported as an unknown integer.
Read the `<integer>` and `<real>` literals from plutil's XML text instead:
an unsafe integral number that matches one `<integer>` literal and no
`<real>` gets its exact decimal; a number that could be either stays as
decoded with a limitation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:13:11 -07:00
Iwakurakaoru0822andClaude Opus 5.5 527b76ef78 fix(javascript): leave empty application values unlabeled
A synthetic `""` label for an empty application-graph value could still
be matched by a literal `""` feature seed, which also matches the label of
a real two-quote value, so `trace_application_feature` combined
`fetch('')` with `fetch('""')`. Emit no label (`null`) for empty values;
the exact value stays in the identity and properties.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:11:51 -07:00
N0zoM1z0andGitHub ce73c3fed2 refactor: compose investigation record ownership (#845) 2026-10-07 12:10:45 +08:00
Iwakurakaoru0822andClaude Opus 5.5 430c297ca4 fix(native): report exact plist integers beyond the JSON number range
`inspect_plist` parsed plutil JSON with `JSON.parse`, silently rounding
integers outside ±(2^53 - 1): `18446744073709551615` became
`18446744073709552000` with no limitation. Keep the exact decimal text
from the JSON source as `{ "$plist_type": "integer", "decimal": "..." }`
and add a limitation. On the XML fallback path, the decoder has already
rounded such values, so they become `decimal: null` instead of a wrong
number.

Also restore the `` escape in the BOM regex, which an earlier
rewrite had turned into a raw invisible character.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:04:47 -07:00
Iwakurakaoru0822andClaude Opus 5.5 c3f2b8e774 docs: separate ambiguous and unavailable provider recovery
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:01:43 -07:00
Kaoru0822andGitHub bc2cd8b874 Merge pull request #841 from morluto/fix/plist-non-json-values
fix(native): inspect plists containing data, dates, and non-finite reals
2026-10-06 21:01:10 -07:00
Iwakurakaoru0822andClaude Opus 5.5 63012eeb60 fix(javascript): keep exact empty values in graph identities
#839 displayed empty artifact-local keys and source-map original names as
`""` inside node identities, so `fetch("")` and `fetch('""')` (or
`sources: ["", "\"\""]`) produced the same node ID and merged two
distinct facts. Function nodes likewise exposed only a display label, so
`['']()` and `['""']()` were indistinguishable.

Admit the exact empty string in `artifact-local-key.key` and
`source-map-original.original_source`, keep the `""` substitution for
labels only, and record each function node's exact `name` in its
properties. Regenerate the catalog digests for the relaxed output schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:00:57 -07:00
Iwakurakaoru0822andClaude Opus 5.5 5fd0db37d9 docs: show task-scoped readiness and provider preference in the quick start
Explain that bare `rea doctor` audits every integration, map common tasks
to `--provider`, `--client`, and `--skill` scopes, and describe how
`scope_checks`, `informational_checks`, `healthy`, and
`environment_healthy` relate. Document the typed ambiguous-provider
error and how to choose an engine once without implying a fallback.

Fixes #726

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:57:39 -07:00
Kaoru0822andGitHub e7cbfe04fd Merge pull request #839 from morluto/fix/empty-literal-graph-labels
fix(javascript): analyze sources containing empty string literals
2026-10-06 20:55:11 -07:00
Kaoru0822andGitHub 522c2d0ecb Merge pull request #836 from morluto/fix/empty-static-key-consumers
fix(javascript): keep exact empty env keys and method names
2026-10-06 20:54:50 -07:00
Iwakurakaoru0822andClaude Opus 5.5 5ead2cfbc4 fix(native): inspect plists containing data, dates, and non-finite reals
`inspect_plist` converted every plist with `plutil -convert json`, which
rejects plists containing `<data>`, `<date>`, or non-finite `<real>`
values. Such valid plists (including Stocks.app's Info.plist) failed with
a cause-free `execution_failure`.

When the JSON conversion exits nonzero, decode `plutil -convert xml1`
instead. Data and dates use the `$plist_type` objects the keyed-archive
reader already emits; non-finite reals become an explicit unknown because
the XML decoder reports NaN and infinity alike. Provenance records both
conversions and limitations explain the representation. Plists that JSON
can express keep the unchanged single-conversion path.

The projection moves to `src/domain/plistValue.ts` and is shared with the
keyed-archive reader, which keeps rejecting non-finite reals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:54:23 -07:00
Kaoru0822andGitHub 3a4a63f70a Merge pull request #835 from kaoru0822-kitauji/fix/electron-computed-member-keys
fix(javascript): preserve computed Electron member uncertainty
2026-10-06 20:51:31 -07:00
Iwakurakaoru0822andClaude Opus 5.5 58ef8ac3aa fix(javascript): analyze source maps with empty source names
A source map whose `sources` contains `""` (a legal relative reference,
emitted for virtual or generated modules) made the whole application
analysis fail: the empty name became the `source-map-original` identity's
`original_source`, which must be nonempty. Display it as `""` at the same
accumulator seam; the observation's `properties.source` keeps the exact
empty name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:47:27 -07:00
N0zoM1z0andGitHub be3cfbffdf fix(mcp): isolate optional observation adapter failures (#838) 2026-10-07 11:45:17 +08:00
Iwakurakaoru0822andClaude Opus 5.5 11fc3b9815 fix(javascript): analyze sources containing empty string literals
A single legal empty literal such as `fetch('')`, `require('')`,
`bus.on('', f)`, `localStorage.getItem('')`, or `ipcMain.handle('', f)`
made the whole application analysis fail with `unreadable_output`: the
literal became an application-graph observation label, an artifact-local
identity key, or a semantic node label, all of which must be nonempty.

Normalize display at the two graph construction seams instead of at each
producer: `JavaScriptArtifactGraphAccumulator.addNode` and
`constructSemanticGraphNode` show the empty value as `""` via a shared
`javascriptDisplayText` helper. Observation and node properties keep the
exact empty value. This replaces the per-site `semanticNameLabel` calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:43:08 -07:00
morlutoandGitHub 78f4848d70 refactor(domain): map validation taxonomies and add inventory Result seam (#833)
Document the three disjoint failure taxonomies (filesystem reader
codes, session reference reasons, inventory TypeErrors) with a test
pinning their disjointness. Export ParsedInventorySet and add
tryAssembleInventorySet covering the empty-pages leaf as Result;
the throwing entry contract is unchanged until callers migrate.
2026-10-07 11:40:34 +08:00
morlutoandGitHub c13c9d41f8 refactor(domain): converge comparison mechanics on shared vocabulary (#824)
Delete the private canonicalJson duplicate in
functionComparisonNormalization in favor of comparisonSemantics
(same canonicalize behavior via the context parameter).
Add comparisonStatus as the single owner of the canonical
unchanged/changed/unknown triple and conclusion vocabulary, with
mappers pinning every pipeline-native status; adopt the shared
triple schema where wire-identical (bundle result, scenario
overall/step). Native detail enums and collation choices stay
untouched.
2026-10-07 11:40:24 +08:00
Iwakurakaoru0822andClaude Opus 5.5 ad1f6b508f fix(javascript): keep exact empty require paths and request fields
Migrate the remaining `""`-sentinel consumers from #783 to the nullable
`semanticStaticPropertyKey`: CommonJS module origins
(`require("m")['']`), require destructuring (`const {'': a} =
require("m")`), and request body fields (`{ '': value }`). Dynamic keys
still produce no link or field. API selectors that compare against known
names are unchanged.

Fixes #783

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:38:11 -07:00
6aa6350c80 docs: note Cloud Agent Node.js toolchain (#837)
Record the pinned Node 24.18.0 setup and that Hopper, Ghidra, and IDA stay optional for JavaScript analysis and deterministic tests.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-07 11:37:11 +08:00
Iwakurakaoru0822andClaude Opus 5.5 f7954a8fd6 fix(javascript): keep exact empty env keys and method names
`process.env['']` now projects an exact empty environment key with
complete resolution instead of the same null/partial result as a dynamic
`process.env[key]`. `{ ['']() {} }` keeps its exact empty method name in
the IR instead of a generated `[computed@N]` name.

Semantic graph labels stay nonempty: a shared `semanticNameLabel` helper
displays the empty key as `""` for property slots, config sources, and
function nodes, while `properties.key` keeps the exact value.

Refs #783

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:35:02 -07:00
morlutoandGitHub de9593ad48 refactor(browser): align inspector exclusions with canonical vocabulary (#834)
Report canonical BrowserExclusionReasons from runtime location
authorization (unsupported_url, not_approved) and collapse them
onto the fixed V8 wire bucket via inspectorExclusionKey, keeping
describeInspectorTargetLimitations text stable. Document the
provider authority split (attach-only CDP/V8 vs owned-runtime
Playwright) so the shared vocabulary is not re-split.
2026-10-07 11:34:57 +08:00
Kaoru0822 1260cacc67 fix(javascript): preserve computed Electron member uncertainty 2026-10-06 20:33:55 -07:00
856 changed files with 36725 additions and 6665 deletions
+57 -5
View File
@@ -239,20 +239,46 @@ jobs:
if-no-files-found: error
package-e2e:
name: Package/install integration (${{ matrix.os }})
name: Native package baseline (${{ matrix.platform }} ${{ matrix.architecture }})
needs: [changes, windows-native-build]
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
strategy:
fail-fast: false
max-parallel: 2
matrix:
os: [ubuntu-latest, macos-14]
include:
- os: ubuntu-latest
platform: linux
architecture: x64
- os: ubuntu-24.04-arm
platform: linux
architecture: arm64
- os: macos-15
platform: darwin
architecture: arm64
- os: macos-15-intel
platform: darwin
architecture: x64
runs-on: ${{ matrix.os }}
timeout-minutes: 20
env:
NODE_OPTIONS: "--max-old-space-size=1024"
UV_THREADPOOL_SIZE: "2"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- name: Verify native runtime identity
env:
EXPECTED_CI_PLATFORM: ${{ matrix.platform }}
EXPECTED_CI_ARCHITECTURE: ${{ matrix.architecture }}
run: >-
node -e "const assert = require('node:assert/strict');
assert.equal(process.platform, process.env.EXPECTED_CI_PLATFORM);
assert.equal(process.arch, process.env.EXPECTED_CI_ARCHITECTURE);
console.log(JSON.stringify({ platform: process.platform, architecture: process.arch, node: process.version }));"
- run: npm ci
- uses: actions/download-artifact@v4
with:
@@ -260,12 +286,15 @@ jobs:
path: native/windows/build/
- run: npm run verify:windows-native:artifact
- run: npm run verify:package
- name: Verify real Node Inspector discovery and observation
if: matrix.platform == 'darwin'
run: npm run verify:inspector
apple-artifacts:
name: Real Apple artifact CLI/MCP E2E
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
runs-on: macos-14
runs-on: macos-15
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
@@ -276,8 +305,9 @@ jobs:
- run: npm ci
- 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 -- src/application/NibNativeHierarchy.test.ts
run: npm run test:focused -- 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
@@ -318,16 +348,38 @@ jobs:
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
strategy:
fail-fast: false
max-parallel: 2
matrix:
os: [ubuntu-latest, windows-latest]
include:
- os: ubuntu-latest
platform: linux
architecture: x64
- os: ubuntu-24.04-arm
platform: linux
architecture: arm64
- os: windows-latest
platform: win32
architecture: x64
runs-on: ${{ matrix.os }}
timeout-minutes: 10
env:
NODE_OPTIONS: "--max-old-space-size=1024"
UV_THREADPOOL_SIZE: "2"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- name: Verify native runtime identity
env:
EXPECTED_CI_PLATFORM: ${{ matrix.platform }}
EXPECTED_CI_ARCHITECTURE: ${{ matrix.architecture }}
run: >-
node -e "const assert = require('node:assert/strict');
assert.equal(process.platform, process.env.EXPECTED_CI_PLATFORM);
assert.equal(process.arch, process.env.EXPECTED_CI_ARCHITECTURE);
console.log(JSON.stringify({ platform: process.platform, architecture: process.arch, node: process.version }));"
- run: npm ci
- name: Verify real Node Inspector discovery and observation
run: npm run verify:inspector
+28
View File
@@ -0,0 +1,28 @@
name: Documentation build
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: documentation-build-${{ github.ref }}
cancel-in-progress: true
env:
HUSKY: "0"
jobs:
build:
name: Build documentation
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run docs:build
+14
View File
@@ -7,17 +7,31 @@ on:
- "scripts/verify-real-browser.mjs"
- "scripts/verify-browser-network.mjs"
- "scripts/verify-browser-scripts.mjs"
- "scripts/verify-browser-modules.mjs"
- "scripts/verify-browser-dom-destinations.mjs"
- "scripts/lib/browser-*.mjs"
- "scripts/fixtures/browser-network-site.mjs"
- "scripts/fixtures/browser-script-site.mjs"
- "scripts/fixtures/browser-module-site.mjs"
- "scripts/fixtures/browser-verifier-site.mjs"
- "scripts/fixtures/page-cdp-proxy.mjs"
- "src/browser/**"
- "src/domain/browser*.ts"
- "src/domain/webScript*.ts"
- "src/domain/webModule*.ts"
- "src/application/WebModule*.ts"
- "src/composition/webModules.ts"
- "src/artifacts/readStableArtifact.ts"
- "src/artifacts/ArtifactPaths.ts"
- "src/application/WebScriptExportService.ts"
- "src/contracts/webScriptToolContracts.ts"
- "src/server/registerWebScriptTool.ts"
- "src/server/registerWebModuleTool.ts"
- "src/server/createServer.ts"
- "src/cli/webModuleCommands.ts"
- "src/cli.ts"
- "src/cliCommandNames.ts"
- "src/contracts/toolEffects.ts"
- "src/cli/webScriptCommands.ts"
- "package.json"
- "package-lock.json"
@@ -0,0 +1,68 @@
name: Real JavaScript recovery
on:
pull_request:
paths:
- ".github/workflows/real-javascript-recovery.yml"
- "src/javascript/recovery/**"
- "src/domain/javascript/**"
- "src/application/javascript/JavaScriptRecovery*.ts"
- "src/composition/javascriptRecovery.ts"
- "src/contracts/javascript/javascriptRecoveryToolContracts.ts"
- "src/cli/javascriptRecoveryCommands.ts"
- "src/cli/javascriptApplicationAnalysis.ts"
- "src/server/registerJavaScriptRecoveryTool.ts"
- "src/server/createServer.ts"
- "src/server/sessionAvailabilityPolicy.ts"
- "src/application/CapabilityInventory.ts"
- "src/cli.ts"
- "src/cliCommandNames.ts"
- "src/contracts/toolContracts.ts"
- "src/contracts/toolEffects.ts"
- "scripts/rea.mjs"
- "src/process/**"
- "src/artifacts/SafeOutputTree.ts"
- "scripts/verify/javascript/recovery.mjs"
- "scripts/fixtures/javascript-recovery/**"
- "third_party/wakaru"
- "package.json"
- "package-lock.json"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: javascript-recovery-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
HUSKY: "0"
NODE_OPTIONS: "--max-old-space-size=768"
RAYON_NUM_THREADS: "1"
jobs:
real-javascript-recovery:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- name: Fetch pinned Wakaru release into the job workspace
shell: bash
run: |
mkdir -p "$RUNNER_TEMP/rea-wakaru"
curl --fail --location --retry 3 https://github.com/pionxzh/wakaru/releases/download/v1.13.0/wakaru-linux-x64.tar.gz -o "$RUNNER_TEMP/rea-wakaru/release.tar.gz"
tar -xzf "$RUNNER_TEMP/rea-wakaru/release.tar.gz" -C "$RUNNER_TEMP/rea-wakaru" wakaru
printf '%s %s\n' e8c7ca052974604197389f6ace7464e4dd24dce64e2730971521bd6606ec9c9e "$RUNNER_TEMP/rea-wakaru/wakaru" | sha256sum --check
- name: Install isolated fixture compilers
run: npm install --prefix "$RUNNER_TEMP/rea-fixture-compilers" --ignore-scripts --no-audit --no-fund esbuild@0.25.10 webpack@5.101.3
- run: npm run build:cached
- run: npm run verify:javascript:recovery
env:
REA_WAKARU_COMMAND: ${{ runner.temp }}/rea-wakaru/wakaru
REA_JAVASCRIPT_FIXTURE_TOOLS: ${{ runner.temp }}/rea-fixture-compilers
+50
View File
@@ -0,0 +1,50 @@
name: Real website source-map verification
on:
pull_request:
paths:
- ".github/workflows/real-web-source-map.yml"
- "scripts/verify-browser-source-maps.mjs"
- "scripts/lib/browser-source-map-e2e.mjs"
- "scripts/fixtures/browser-source-map-site.mjs"
- "src/javascript/sourceMaps/**"
- "src/browser/assets/**"
- "src/domain/webSource*.ts"
- "src/domain/webScript*.ts"
- "src/application/WebSourceLocation*.ts"
- "src/application/ValidateSelectedWebScript.ts"
- "src/composition/webSourceLocations.ts"
- "src/server/registerWebSourceLocationTool.ts"
- "src/server/createServer.ts"
- "src/contracts/webScriptToolContracts.ts"
- "src/process/**"
- "src/cli/webSourceLocationCommands.ts"
- "src/cli.ts"
- "src/artifacts/readStableArtifact.ts"
- "package.json"
- "package-lock.json"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: real-web-source-map-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
HUSKY: "0"
jobs:
real-web-source-map:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- name: Install the pinned compiler into an isolated fixture directory
run: npm install --prefix "$RUNNER_TEMP/rea-web-source-map-compiler" --no-audit --no-fund --package-lock=false esbuild@0.25.10
- run: npm run verify:browser:source-maps
env:
REA_BROWSER_EXECUTABLE: /usr/bin/google-chrome
REA_BROWSER_NO_SANDBOX: "true"
REA_WEB_SOURCE_MAP_COMPILER: ${{ runner.temp }}/rea-web-source-map-compiler/node_modules/esbuild/lib/main.js
+28
View File
@@ -0,0 +1,28 @@
name: Website checks
on:
pull_request:
branches: [main]
paths:
- "website/**"
- "scripts/verify-website.py"
- ".github/workflows/**"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: website-check-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
check:
name: Website checks
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check out the repository
uses: actions/checkout@v4
- name: Check website links and publishing configuration
run: python3 scripts/verify-website.py
+46
View File
@@ -0,0 +1,46 @@
name: Publish REA website
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: website-pages
cancel-in-progress: false
jobs:
prepare:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pages: read
steps:
- name: Check out the repository
uses: actions/checkout@v4
- name: Check website links and publishing configuration
run: python3 scripts/verify-website.py
- name: Configure Pages
uses: actions/configure-pages@v5
- name: Upload the website
uses: actions/upload-pages-artifact@v4
with:
path: website/public
deploy:
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Publish to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+1
View File
@@ -37,6 +37,7 @@ coverage/
test-results.xml
.turbo/
.cache/
docs/.vitepress/cache/
# OS
.DS_Store
+3
View File
@@ -10,3 +10,6 @@
[submodule "third_party/ghidra-nativeaot"]
path = third_party/ghidra-nativeaot
url = https://github.com/Washi1337/ghidra-nativeaot.git
[submodule "third_party/wakaru"]
path = third_party/wakaru
url = https://github.com/pionxzh/wakaru.git
+2 -117
View File
@@ -28,98 +28,13 @@
{
"files": [
"scripts/electron-active-hook*.cjs",
"tests/conformance/readiness/electron/*.js"
"tests/conformance/readiness/electron/*.js",
"website/public/examples/notes-electron/*.js"
],
"rules": {
"no-var-requires": "off"
}
},
{
"files": [
"src/{contracts,domain,server}/**/*.ts",
"src/application/**/*.ts"
],
"excludeFiles": ["src/application/runtime.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": [
"**/ghidra/**",
"**/hopper/**",
"**/ida/**",
"**/android/**",
"**/firmware/**"
],
"message": "Provider imports belong behind typed ports or in production composition."
}
]
}
]
}
},
{
"files": ["src/{contracts,domain}/**/*.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": [
"**/application/**",
"**/server/**",
"**/cli/**",
"**/main/**",
"**/cli.*",
"**/main.*",
"**/android/**",
"**/artifacts/**",
"**/browser/**",
"**/dotnet/**",
"**/firmware/**",
"**/ghidra/**",
"**/hopper/**",
"**/ida/**",
"**/native/**",
"**/process/**",
"**/reference/**",
"**/windows/**",
"**/composition/**"
],
"message": "Domain and contracts depend inward; use typed ports for application, caller and provider boundaries."
}
]
}
]
}
},
{
"files": ["src/application/**/*.ts"],
"excludeFiles": ["src/application/runtime.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": [
"**/ghidra/**",
"**/hopper/**",
"**/ida/**",
"**/android/**",
"**/firmware/**",
"**/composition/**"
],
"message": "Application workflows use typed ports; production construction belongs in composition."
}
]
}
]
}
},
{
"files": ["tests/**/*.ts", "src/**/*.test.ts", "src/**/*.fixture.ts"],
"rules": {
@@ -134,36 +49,6 @@
]
}
},
{
"files": ["src/domain/**/*.{fixture,test}.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": [
"**/application/**",
"**/server/**",
"**/process/**",
"**/browser/**",
"**/ghidra/**",
"**/hopper/**",
"**/ida/**",
"**/replay/**",
"**/cli.*",
"**/main.*",
"**/composition/**",
"**/android/**",
"**/firmware/**"
],
"message": "Domain tests may import only their owning domain and inward dependencies."
}
]
}
]
}
},
{
"files": ["src/application/**/*.test.ts"],
"rules": {
+8 -2
View File
@@ -20,10 +20,10 @@ REA is a local-only tool. Preserve caller-selected inputs, captured output, URLs
REA is a layered ESM TypeScript application. Dependencies flow inward from pure domain logic through contracts, providers, application workflows, and CLI/MCP adapters. See [docs/architecture.mermaid](docs/architecture.mermaid) for the component map.
- `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/native/`, `src/artifacts/`, and `src/dotnet/` own provider-specific boundaries. Keep provider protocols out of domain and application code.
- `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.
- `tests/` contains unit, composition, boundary, acceptance, and conformance tests. `scripts/verify-*` contains real-toolchain checks.
- `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.
## Build, Test, and Development Commands
@@ -91,6 +91,12 @@ Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rath
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.
## Cursor Cloud specific instructions
Development requires Node.js 24.18.0 and npm 11.16.0 (`.nvmrc` and `packageManager`). The Cloud Agent image places an older `node` on `PATH` ahead of a normal install. Environment setup installs the pinned toolchain under `/usr/local` and prepends `/usr/local/bin` for login shells. Confirm `node -v` is `v24.18.0` before installing dependencies.
`npm ci` installs locked dependencies. `npm run build:cached` produces the CLI and MCP server. `npm run check:fast` is the pre-push typecheck and lint. Hopper, Ghidra, and IDA are optional bring-your-own providers. JavaScript analysis and the deterministic Vitest suites do not need them. `rea doctor` reports those engines as missing until they are configured.
## 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/`).
+17 -1
View File
@@ -10,7 +10,7 @@ When adding or changing an MCP tool, follow the [tool design guide](docs/tool-de
## Development setup
REA development requires Node.js 24.18.x and npm 11.16.x (pinned toolchain via `nvm use`; the supported runtime range is Node.js ^22.19 || ^24.11 || >=26, as the README badge states). Real-Hopper verification additionally requires either macOS 12+ or an officially supported Linux host (Ubuntu 24.04+, Fedora 41+, or 64-bit Arch) and an installed Hopper application. Linux demo verification uses its own private Xvfb display and does not require a desktop session. Run `nvm use` before installing dependencies.
REA development requires Node.js 24.18.x and npm 11.16.x (pinned toolchain via `nvm use`; the supported runtime range is Node.js ^22.19 || ^24.11 || >=26, as the README badge states). Real-Hopper verification additionally requires either macOS 12+ or an officially supported Linux host (Ubuntu 24.04+, Fedora 41+, 64-bit Arch, or CachyOS) and an installed Hopper application. Linux demo verification uses its own private Xvfb display and does not require a desktop session. Run `nvm use` before installing dependencies.
```bash
npm ci
@@ -26,6 +26,22 @@ worktrees. After a package, lockfile, or managed-skill version change, run
Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Keep tool discovery complete and report capability- and session-scoped availability through `binary_session`.
## 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/`.
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.
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.
## Development feedback and PR verification
For an ordinary edit, run the relevant regression and cached static checks:
+100 -21
View File
@@ -17,6 +17,8 @@
<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/) · [DX-Ball showcase](https://morluto.github.io/rea/showcase/dx-ball/)**
[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)
<code>npx rea-agents setup</code>
@@ -95,6 +97,40 @@ MCP setup, Hopper, Ghidra, or executing the application. For a native app, confi
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:
@@ -121,7 +157,7 @@ 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+, or 64-bit Arch Linux
- 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
@@ -129,7 +165,7 @@ Deep native binary analysis requires [Hopper](https://www.hopperapp.com/), [Ghid
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 Java, with no emulator or APK execution. See [Android analysis](docs/android-analysis.md) for setup, CLI/MCP operations, coverage and public test fixtures.
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.
@@ -147,7 +183,7 @@ On macOS, setup can install Hopper in `~/Applications` after approval. It verifi
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.
The normal Linux launcher is `/opt/hopper/bin/Hopper`. If Hopper was installed elsewhere:
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
@@ -157,16 +193,16 @@ rea doctor --json
If doctor reports a missing analysis engine even though the file exists, inspect shared-library resolution with:
```bash
ldd /opt/hopper/bin/Hopper | grep 'not found'
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 defaults `HOPPER_LAUNCHER_PATH` to `/Applications/Hopper Disassembler.app/Contents/MacOS/hopper` on macOS and `/opt/hopper/bin/Hopper` on Linux. Explicit configuration always takes precedence.
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 requires **Ghidra 12.1.4** and a **64-bit JDK 21**. On macOS, your Ghidra installation must also include the native decompiler for your architecture.
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:
@@ -335,6 +371,21 @@ REA gives the agent a clear path from that request to working code:
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.
@@ -350,20 +401,20 @@ See [native investigation](docs/native-investigation.md) for keyed archives, ins
## 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 | 9 | 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 | 8 | captured website script export; 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 |
| 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 | 9 | 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.
@@ -371,7 +422,7 @@ The public interface describes what the agent is trying to learn. Providers deci
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 is verified on Linux with headless JADX; see [Android analysis](docs/android-analysis.md) for its separate prerequisites and coverage.
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.
@@ -383,6 +434,20 @@ Static Android APK inspection is verified on Linux with headless JADX; see [Andr
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).
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).
@@ -742,3 +807,17 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and contribution ch
## License
[MIT](LICENSE)
## Star history
<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 history" src="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date" />
</picture>
</a>
## 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.
+7 -5
View File
@@ -17,6 +17,8 @@
<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/)**
[البدء السريع](#البدء-السريع) · [حالة الدعم](#حالة-الدعم) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [منصة أدوات التحقيق](#منصة-أدوات-التحقيق) · [خطة العمل](#خطة-العمل) · [كيف يعمل؟](#كيف-يعمل)
<table aria-label="REA community">
@@ -136,12 +138,12 @@ rea setup
### المتطلبات
- macOS 12 أو أحدث
- Ubuntu 24.04+ أو Fedora 41+ أو Arch Linux بنواة 64 بت
- Ubuntu 24.04+ أو Fedora 41+ أو Arch Linux بنواة 64 بت أو 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.4 وJDK 21 الكامل بنواة 64 بت بصورة منفصلة، ثم اضبط REA لاستخدامهما. يحتاج macOS أيضًا إلى أداة فك الترجمة الأصلية المطابقة لمعمارية الجهاز.
يدعم 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.
@@ -151,7 +153,7 @@ rea setup
يشخّص `npx -y rea-agents@latest doctor` الجهاز والاعتماديات وأدوات التحليل وإعدادات الوكيل من دون تغييرها. أضف `--json` للحصول على تشخيص منظم.
مسار Hopper الافتراضي على Linux هو `/opt/hopper/bin/Hopper`. استخدم `HOPPER_LAUNCHER_PATH` للمسارات الأخرى. إذا كان الملف موجودًا لكن محرك التحليل لا يظهر، فشغّل `ldd /opt/hopper/bin/Hopper | grep 'not found'` للتحقق من المكتبات الناقصة. راجع [دليل Hopper](docs/installation.md#hopper).
على 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).
### التحديث والإزالة
@@ -223,7 +225,7 @@ rea setup
| مراقبة المتصفح | 9 | بنية الصفحات وبيانات الشبكة والبرامج النصية وخرائط المصدر واكتشاف WebMCP والصور ومقارنة الالتقاط |
| تحليل Electron | 5 | مراقبة العارضين وخريطة التطبيق الثابتة والمواءمة بين نتائج الثبات والتشغيل |
| وقت تشغيل JavaScript | 2 | اكتشاف أهداف Node/Electron Inspector ومواضع البرامج وأحداث سياقات التنفيذ |
| سير عمل التطبيقات | 8 | تصدير النصوص البرمجية الملتقطة، تتبع الميزات عبر الطبقات ومقارنة الإصدارات ومواءمة المصدر التاريخي ومقارنة أشكال القيم المعادة والتحقق من إعادة الإنشاء |
| سير عمل التطبيقات | 13 | تصدير النصوص البرمجية الملتقطة، تتبع الميزات عبر الطبقات ومقارنة الإصدارات ومواءمة المصدر التاريخي ومقارنة أشكال القيم المعادة والتحقق من إعادة الإنشاء |
| مساحة العمل والملاحظة | 21 | الجلسات وحزم الأدلة وسياق التنقل ومقارنة العمليات والمخرجات والدوال وتسجيل الأسئلة غير المحسومة |
## خطة العمل
@@ -311,7 +313,7 @@ npx -y rea-agents@latest compare /absolute/path/to/left-evidence.json /absolute/
<details>
<summary><strong>هل يعمل REA على Linux أو Windows؟</strong></summary>
يدعم REA نظام macOS 12+ وUbuntu 24.04+ وFedora 41+ وArch Linux بنواة 64 بت. يعمل تحليل Ghidra للقراءة فقط على Linux x64 وmacOS x64/arm64 مع Ghidra 12.1.4 وJDK 21 الكامل، إضافة إلى حد Windows x64 P0 التجريبي لتطبيقات PE الأصلية x86-64 على NTFS محلي؛ راجع [دليل Windows Ghidra P0](docs/windows-ghidra-p0.md) للنطاق الموثق.
يدعم 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) للنطاق الموثق.
</details>
+6 -4
View File
@@ -17,6 +17,8 @@
<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/)**
[クイックスタート](#クイックスタート) · [現在の対応状況](#現在の対応状況) · [バイナリから動作へ](#バイナリから動作へ) · [調査ツールカタログ](#調査ツールカタログ) · [ロードマップ](#ロードマップ) · [仕組み](#仕組み)
<table aria-label="REA community">
@@ -136,12 +138,12 @@ rea setup
### 要件
- macOS 12 以降
- Ubuntu 24.04+、Fedora 41+、または 64 ビット Arch Linux
- 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.4 と完全な 64 ビット JDK 21 を別途インストールし、REA で使うように設定してください。macOS ではホストのアーキテクチャに合うネイティブデコンパイラーも必要です。
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 のインストールや更新は行いません。
@@ -151,7 +153,7 @@ Setup はインストールを確認し、パスを保存できます。Ghidra
`npx -y rea-agents@latest doctor` はホスト、依存関係、解析ツール、エージェント設定を変更せずに確認します。構造化された診断には `--json` を追加してください。
Linux の既定ランチャーは `/opt/hopper/bin/Hopper` です。別の場所には `HOPPER_LAUNCHER_PATH` を設定します。ファイルがあるのに解析エンジンが見つからない場合は、`ldd /opt/hopper/bin/Hopper | grep 'not found'` で不足ライブラリを確認してください。詳しくは [Hopper ガイド](docs/installation.md#hopper)を参照してください。
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)を参照してください。
### 更新とアンインストール
@@ -212,7 +214,7 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
| ブラウザー観察 | 9 | ページ構成、ネットワークメタデータ、スクリプト、ソースマップ、WebMCP 検出、スクリーンショット、キャプチャ比較 |
| Electron 解析 | 5 | レンダラー観察、静的アプリマッピング、静的/実行時結果の対応付け |
| JavaScript 実行時 | 2 | Node/Electron Inspector 対象の検出、スクリプト位置、実行コンテキストイベント |
| アプリワークフロー | 8 | 取得した Web スクリプトのエクスポート、層をまたぐ機能トレース、ビルド比較、履歴ソース対応、静的戻り値形状の比較、再実装検証 |
| アプリワークフロー | 13 | 取得した Web スクリプトのエクスポート、層をまたぐ機能トレース、ビルド比較、履歴ソース対応、静的戻り値形状の比較、再実装検証 |
| ワークスペースと観察 | 21 | セッション、証跡バンドル、ナビゲーション文脈、プロセス・成果物・関数比較、未解決事項の記録 |
## ロードマップ
+6 -4
View File
@@ -17,6 +17,8 @@
<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/)**
[빠른 시작](#빠른-시작) · [현재 지원 범위](#현재-지원-범위) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [조사 도구 카탈로그](#조사-도구-카탈로그) · [로드맵](#로드맵) · [작동 방식](#작동-방식)
<table aria-label="REA community">
@@ -137,12 +139,12 @@ rea setup
### 요구 사항
- macOS 12 이상
- Ubuntu 24.04+, Fedora 41+ 또는 64비트 Arch Linux
- 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.4와 완전한 64비트 JDK 21을 별도로 설치한 뒤 REA가 사용하도록 설정하세요. macOS에서는 호스트 아키텍처에 맞는 네이티브 디컴파일러도 필요합니다.
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를 설치하거나 업데이트하지 않습니다.
@@ -152,7 +154,7 @@ Setup은 설치를 확인하고 경로를 저장할 수 있습니다. Ghidra, Ja
`npx -y rea-agents@latest doctor`는 호스트, 의존성, 분석 도구, 에이전트 설정을 변경 없이 확인합니다. 구조화된 진단에는 `--json`을 추가하세요.
Linux의 기본 Hopper 실행 파일은 `/opt/hopper/bin/Hopper`입니다. 다른 위치에는 `HOPPER_LAUNCHER_PATH`를 설정하세요. 파일이 있는데도 분석 엔진이 없다고 하면 `ldd /opt/hopper/bin/Hopper | grep 'not found'`로 누락된 라이브러리를 확인하세요. 자세한 내용은 [Hopper 안내](docs/installation.md#hopper)를 참고하세요.
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)를 참고하세요.
### 업데이트와 제거
@@ -213,7 +215,7 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
| 브라우저 관찰 | 9 | 페이지 구조, 네트워크 메타데이터, 스크립트, 소스 맵, WebMCP 탐색, 스크린샷, 캡처 비교 |
| Electron 분석 | 5 | 렌더러 관찰, 정적 앱 매핑, 정적·런타임 결과 연결 |
| JavaScript 런타임 | 2 | Node/Electron Inspector 대상 탐색, 스크립트 위치, 실행 컨텍스트 이벤트 |
| 앱 워크플로 | 8 | 캡처한 웹 스크립트 내보내기, 계층 간 기능 추적, 빌드 비교, 히스토리 소스 매핑, 정적 반환 구조 비교, 재구현 검증 |
| 앱 워크플로 | 13 | 캡처한 웹 스크립트 내보내기, 계층 간 기능 추적, 빌드 비교, 히스토리 소스 매핑, 정적 반환 구조 비교, 재구현 검증 |
| 작업 공간과 관찰 | 21 | 세션, 근거 번들, 탐색 컨텍스트, 프로세스·아티팩트·함수 비교, 미해결 항목 기록 |
## 로드맵
+6 -4
View File
@@ -17,6 +17,8 @@
<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/)**
[快速开始](#快速开始) · [当前状态](#当前状态) · [从二进制到行为](#从二进制到行为) · [调查工具目录](#调查工具目录) · [路线图](#路线图) · [工作原理](#工作原理)
<table aria-label="REA community">
@@ -136,12 +138,12 @@ rea setup
### 系统要求
- macOS 12 或更高版本
- Ubuntu 24.04+、Fedora 41+ 或 64 位 Arch Linux
- 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.4 和完整的 64 位 JDK 21,然后配置 REA 使用它们。macOS 还需要对应架构的原生反编译器。
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。
@@ -151,7 +153,7 @@ Setup 可以验证安装并保存路径,不会安装或升级 Ghidra、Java、
运行 `npx -y rea-agents@latest doctor`,检查主机、依赖、分析工具和智能体配置。该命令不会修改文件。添加 `--json` 可获取结构化诊断。
Linux 上的默认 Hopper 启动器为 `/opt/hopper/bin/Hopper`。其他路径可通过 `HOPPER_LAUNCHER_PATH` 指定。如果文件存在但仍报告缺少分析引擎,运行 `ldd /opt/hopper/bin/Hopper | grep 'not found'` 检查缺少的系统库。安装详情见 [Hopper 指南](docs/installation.md#hopper)。
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)。
### 更新与卸载
@@ -212,7 +214,7 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
| 浏览器观察 | 9 | 页面结构、网络元数据、脚本、来源映射、WebMCP 发现、截图与捕获比较 |
| Electron 分析 | 5 | 渲染进程观察、静态应用映射、静态/运行时结果关联 |
| JavaScript 运行时 | 2 | Node/Electron Inspector 目标发现、脚本位置与执行上下文事件 |
| 应用工作流 | 8 | 捕获的网站脚本导出、跨层功能追踪、构建比较、历史源码映射、静态返回结构比较与重建验证 |
| 应用工作流 | 13 | 捕获的网站脚本导出、跨层功能追踪、构建比较、历史源码映射、静态返回结构比较与重建验证 |
| 工作区与观察 | 21 | 会话、证据包、导航上下文、进程/产物/函数比较与待解决问题跟踪 |
## 路线图
+247
View File
@@ -0,0 +1,247 @@
import com.atxx.jhmcp.*;
import com.google.gson.*;
import io.modelcontextprotocol.kotlin.sdk.server.*;
import io.modelcontextprotocol.kotlin.sdk.types.*;
import jadx.api.*;
import jadx.core.dex.nodes.*;
import java.io.PrintStream;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
import kotlin.Unit;
import kotlin.coroutines.EmptyCoroutineContext;
import kotlinx.coroutines.BuildersKt;
import kotlinx.io.CoreKt;
import kotlinx.io.JvmCoreKt;
import kotlinx.serialization.json.JsonObject;
/** REA-owned metadata boundary for the unmodified jadx-headless-mcp 0.7.1 JAR. */
class ReaJadxBridge {
private static final Gson JSON = new Gson();
private final SessionHolder holder;
private JadxSession indexedSession;
private Map<String, List<ClassSnapshot>> classes = Map.of();
private record MethodSnapshot(String name, String shortId, Map<String, Object> summary) {}
private record ClassSnapshot(JavaClass node, List<MethodSnapshot> methods, Map<String, Object> summary) {}
private ReaJadxBridge(SessionHolder holder) { this.holder = holder; }
private static CallToolResult text(String value, boolean failed) {
return new CallToolResult(List.of(new TextContent(value, null, null)), failed, null, null);
}
private static CallToolResult json(Object value) { return text(JSON.toJson(value), false); }
private static String string(JsonObject args, String name) {
JsonElement value = JsonParser.parseString(args.toString()).getAsJsonObject().get(name);
if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString())
throw new IllegalArgumentException(name + " must be a string");
return value.getAsString();
}
private static int integer(JsonObject args, String name, int fallback) {
JsonElement value = JsonParser.parseString(args.toString()).getAsJsonObject().get(name);
if (value == null) return fallback;
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isNumber())
throw new IllegalArgumentException(name + " must be a nonnegative integer");
try {
int result = value.getAsBigDecimal().intValueExact();
if (result < 0) throw new ArithmeticException();
return result;
} catch (ArithmeticException error) {
throw new IllegalArgumentException(name + " must be a nonnegative integer", error);
}
}
private JadxSession session() {
JadxSession current = holder.current();
if (current == null) throw new IllegalArgumentException("No APK loaded");
return current;
}
private ClassSnapshot selected(JsonObject args) {
JadxSession current = session();
if (indexedSession != current) {
Map<String, List<ClassSnapshot>> index = new LinkedHashMap<>();
if (!current.getClasses().isEmpty()) {
JadxDecompiler decompiler = current.getClasses().get(0).getClassNode().root().getDecompiler();
// JavaClass.getMethods/getFields/getInnerClasses all load code. Raw nodes do not.
// Snapshot every identity before code generation can rewrite aliases or members.
for (JavaClass cls : decompiler.getClassesWithInners()) {
ClassNode node = cls.getClassNode();
List<MethodNode> ordered = new ArrayList<>(node.getMethods());
ordered.sort(Comparator.comparing(MethodNode::getAlias));
List<MethodSnapshot> methods = ordered.stream().map(method -> {
String name = method.getAlias();
String signature = name + "(" + method.getArgTypes().stream()
.map(Object::toString).collect(Collectors.joining(", "))
+ "): " + method.getReturnType().toString();
return new MethodSnapshot(name, method.getMethodInfo().getShortId(), Map.of(
"name", name, "signature", signature,
"is_constructor", method.isConstructor(), "def_pos", 0));
}).toList();
List<Map<String, Object>> fields = node.getFields().stream()
.sorted(Comparator.comparing(FieldNode::getAlias))
.map(field -> Map.<String, Object>of("name", field.getAlias(),
"type", field.getType().toString(), "def_pos", 0)).toList();
List<String> inner = node.getInnerClasses().stream().map(ClassNode::getFullName).toList();
Map<String, Object> summary = Map.of(
"full_name", cls.getFullName(), "name", cls.getName(),
"method_count", methods.size(), "field_count", fields.size(),
"inner_class_count", inner.size(), "inner_classes", inner,
"methods", methods.stream().map(MethodSnapshot::summary).toList(), "fields", fields);
index.computeIfAbsent(cls.getFullName(), ignored -> new ArrayList<>())
.add(new ClassSnapshot(cls, methods, summary));
}
}
classes = Collections.unmodifiableMap(index);
indexedSession = current;
}
String name = string(args, "class_name");
List<ClassSnapshot> candidates = classes.get(name);
if (candidates == null) throw new IllegalArgumentException("Exact class not found: " + name);
if (candidates.size() != 1) throw new IllegalArgumentException("Ambiguous metadata class identity: " + name
+ "; raw identities: " + candidates.stream().map(candidate -> candidate.node().getRawName()).toList());
return candidates.get(0);
}
private List<MethodSnapshot> methods(ClassSnapshot cls, JsonObject args) {
String name = string(args, "method_name");
List<MethodSnapshot> result = cls.methods().stream().filter(method -> method.name().equals(name)).toList();
if (result.isEmpty()) throw new IllegalArgumentException("Method not found: " + name);
return result;
}
private JavaMethod method(ClassSnapshot cls, MethodSnapshot selected) {
JavaMethod result = cls.node().searchMethodByShortId(selected.shortId());
if (result == null) throw new IllegalArgumentException("Method identity no longer available: " + selected.shortId());
return result;
}
private CallToolResult handle(String name, JsonObject args) {
if (name.equals("rea_jvm_status")) return json(Map.of(
"engine_reported_version", BuildInfo.VERSION,
"max_heap_bytes", Runtime.getRuntime().maxMemory(),
"available_processors", Runtime.getRuntime().availableProcessors(),
"java_version", System.getProperty("java.version"),
"metadata_scope", "parsed_members"));
JadxSession current = session();
if (name.equals("list_classes")) {
List<String> names = current.getClassFqns();
int offset = integer(args, "offset", 0);
int limit = integer(args, "limit", 200);
int start = Math.min(offset, names.size());
int end = (int) Math.min((long) start + limit, names.size());
return json(Map.of("total", names.size(), "offset", offset, "limit", limit,
"items", names.subList(start, end)));
}
ClassSnapshot cls = selected(args);
if (name.equals("get_class_summary")) return json(cls.summary());
if (name.equals("get_method_body")) {
List<MethodSnapshot> overloads = methods(cls, args);
int index = integer(args, "overload_index", 0);
if (index >= overloads.size()) throw new IllegalArgumentException("overload_index out of range: " + index);
JavaMethod method = method(cls, overloads.get(index));
JadxSession.SmartCode code = current.getMethodBodySmart(method, current.getMaxSourceBytes(), true);
return json(Map.of("class_name", cls.summary().get("full_name"),
"method_name", overloads.get(index).name(), "full_name", method.getFullName(),
"overload_index", index, "overload_count", overloads.size(),
"mode", code.getKind(), "fell_back", code.getFellBack(),
"markers", code.getMarkers(), "body", code.getText()));
}
List<JavaNode> uses;
String target = string(args, "class_name");
if (name.equals("get_xrefs_to_class")) uses = cls.node().getUseIn();
else {
List<MethodSnapshot> overloads = methods(cls, args);
if (overloads.size() != 1) throw new IllegalArgumentException("Method references require a unique overload");
target += "." + overloads.get(0).name();
uses = method(cls, overloads.get(0)).getUseIn();
}
// REA consumes all references without materializing source line information.
List<Map<String, Object>> items = uses.stream().map(node -> current.describeUsage(node, false)).toList();
return json(Map.of("target", target, "total", uses.size(), "count", items.size(),
"resolve_line", false, "items", items));
}
public static void main(String[] args) throws Exception {
PrintStream protocolOut = System.out;
System.setOut(System.err);
SessionHolder holder = new SessionHolder(new SessionConfig(
1048576, 0, 90000L, 1, List.of(), List.of(), 64, ResourceMode.FULL));
ServerCapabilities capabilities = new ServerCapabilities(
new ServerCapabilities.Tools(null), null, null, null, null, null, null, Map.of());
Server server = new Server(new Implementation("rea-jadx-bridge", "1", null, null, null),
new ServerOptions(capabilities), "", ignored -> Unit.INSTANCE);
SessionToolsKt.registerSessionTools(server, holder);
ManifestToolsKt.registerManifestTools(server, holder);
ClassToolsKt.registerClassTools(server, holder);
MethodToolsKt.registerMethodTools(server, holder);
XrefToolsKt.registerXrefTools(server, holder);
ReaJadxBridge bridge = new ReaJadxBridge(holder);
Set<String> consumed = Set.of("load_apk", "get_app_info", "get_android_manifest",
"list_classes", "get_class_summary", "get_method_body",
"get_xrefs_to_class", "get_xrefs_to_method");
for (String name : new ArrayList<>(server.getTools().keySet()))
if (!consumed.contains(name)) server.removeTool(name);
Tool diagnostics = new Tool("rea_jvm_status",
new ToolSchema(null, new JsonObject(Map.of()), List.of(), null),
"Observed worker JVM and metadata scope", null, null, null, null, null, null);
server.addTool(diagnostics, (connection, request, continuation) -> bridge.handle("rea_jvm_status", request.getArguments()));
for (String name : List.of("list_classes", "get_class_summary",
"get_method_body", "get_xrefs_to_class", "get_xrefs_to_method")) {
Tool tool = server.getTools().get(name).getTool();
server.removeTool(name);
List<String> fields = switch (name) {
case "list_classes" -> List.of("offset", "limit");
case "get_method_body" -> List.of("class_name", "method_name", "overload_index");
case "get_xrefs_to_method" -> List.of("class_name", "method_name");
default -> List.of("class_name");
};
Map<String, kotlinx.serialization.json.JsonElement> properties = new LinkedHashMap<>();
for (String field : fields) properties.put(field, tool.getInputSchema().getProperties().get(field));
String description = switch (name) {
case "list_classes" -> "Indexed pages of all top-level class names, with offset and limit.";
case "get_class_summary" -> "Parsed member metadata for one exact class name; no source generation.";
case "get_method_body" -> "Source for an exact class and stable same-name overload index, with smali fallback.";
default -> "Complete static references to an exact class or unique method; source lines are unresolved.";
};
Tool narrowed = new Tool(name, new ToolSchema(null, new JsonObject(properties),
tool.getInputSchema().getRequired(), null), description, null, null, null, null, null, null);
server.addTool(narrowed, (connection, request, continuation) -> {
try { return bridge.handle(name, request.getArguments()); }
catch (IllegalArgumentException error) { return text(error.getMessage(), true); }
});
}
CountDownLatch done = new CountDownLatch(1);
server.onClose(() -> { done.countDown(); return Unit.INSTANCE; });
StdioServerTransport transport = new StdioServerTransport(
CoreKt.buffered(JvmCoreKt.asSource(System.in)),
CoreKt.buffered(JvmCoreKt.asSink(protocolOut)));
transport.onClose(() -> { done.countDown(); return Unit.INSTANCE; });
ServerSession connection = null;
try {
connection = BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
(scope, continuation) -> server.createSession(transport, continuation));
done.await();
} finally {
try {
if (connection != null) {
ServerSession connected = connection;
// Unsubscribe the connection before the SDK joins its notification service.
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
(scope, continuation) -> connected.close(continuation));
}
} finally {
try {
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
(scope, continuation) -> server.close(continuation));
} finally {
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
(scope, continuation) -> holder.unload(continuation));
}
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
struct ChildBatch<Element> {
let values: [Element]
let complete: Bool
}
func captureChildValues<Element>(requestedCount: Int, copy: () -> [Element]?) -> ChildBatch<Element> {
guard requestedCount > 0 else { return ChildBatch(values: [], complete: true) }
let returned = copy() ?? []
let values = Array(returned.prefix(requestedCount))
return ChildBatch(values: values, complete: values.count == requestedCount)
}
+11 -7
View File
@@ -26,11 +26,12 @@ func childCount(_ element: AXUIElement) -> Int {
var count = 0
return AXUIElementGetAttributeValueCount(element, kAXChildrenAttribute as CFString, &count) == .success ? count : 0
}
func children(_ element: AXUIElement, limit: Int) -> [AXUIElement] {
var value: CFArray?
let count = min(limit, childCount(element))
if count == 0 { return [] }
return AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, 0, count, &value) == .success ? value as? [AXUIElement] ?? [] : []
func children(_ element: AXUIElement, count: Int) -> ChildBatch<AXUIElement> {
captureChildValues(requestedCount: count) {
var value: CFArray?
guard AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, 0, count, &value) == .success else { return nil }
return value as? [AXUIElement]
}
}
func text(_ element: AXUIElement, _ key: String) -> Any { (attribute(element, key) as? String) ?? NSNull() as Any }
func windowBounds(_ element: AXUIElement) -> CGRect? {
@@ -115,9 +116,12 @@ func observe(_ request: Request) async throws -> [String: Any] {
if path.count >= 32 { if totalChildren > 0 { truncated = true }; continue }
let available = max(0, request.max_nodes - nodes.count - pending.count)
let count = min(available, totalChildren)
let items = children(element, limit: count)
let batch = children(element, count: count)
if !batch.complete { truncated = true }
if totalChildren > count { truncated = true }
for index in (0..<count).reversed() { pending.append((items[index], path + [index])) }
for (index, item) in batch.values.enumerated().reversed() {
pending.append((item, path + [index]))
}
}
}
var screenshot: Any = NSNull()
+83
View File
@@ -0,0 +1,83 @@
import { defineConfig } from "vitepress";
export default defineConfig({
title: "REA",
description:
"Reverse engineer binaries, applications, and runtime behavior with one CLI and MCP server.",
lang: "en-US",
base: "/rea/",
themeConfig: {
nav: [
{ text: "Get started", link: "/installation" },
{ text: "Reference", link: "/mcp-contracts" },
{ text: "Roadmap", link: "/roadmap" },
],
sidebar: [
{
text: "Getting started",
items: [
{ text: "Installation and setup", link: "/installation" },
{ text: "Agent prompts", link: "/mcp-prompts" },
{ text: "Roadmap", link: "/roadmap" },
],
},
{
text: "Investigation guides",
items: [
{ text: "Native binaries", link: "/native-investigation" },
{
text: "JavaScript and Electron",
link: "/javascript-artifact-reconstruction",
},
{
text: "JavaScript workflows",
link: "/javascript-application-workflows",
},
{ text: "Browser observation", link: "/browser-observation" },
{ text: "Electron observation", link: "/electron-observation" },
{ text: "Process capture", link: "/process-capture" },
{ text: ".NET assemblies", link: "/managed-code-analysis" },
{ text: "Android applications", link: "/android-analysis" },
{ text: "Apple applications", link: "/apple-application-analysis" },
{ text: "Firmware", link: "/firmware-analysis" },
{
text: "Reconstruction readiness",
link: "/reconstruction-readiness",
},
],
},
{
text: "Provider guides",
collapsed: true,
items: [
{ text: "IDA", link: "/ida-provider" },
{ text: "Ghidra on Windows", link: "/windows-ghidra-p0" },
{ text: "Ghidra DOS analysis", link: "/ghidra-dos" },
{ text: "Ghidra NativeAOT", link: "/ghidra-nativeaot" },
{ text: "Provider evaluation", link: "/provider-evaluation" },
],
},
{
text: "Reference and development",
collapsed: true,
items: [
{ text: "MCP contracts", link: "/mcp-contracts" },
{ text: "Tool design", link: "/tool-design" },
{ text: "Testing", link: "/testing" },
{ text: "Architecture decisions", link: "/adr/README" },
],
},
],
search: { provider: "local" },
socialLinks: [
{ icon: "github", link: "https://github.com/morluto/rea" },
{ icon: "discord", link: "https://discord.gg/GkcryMnJDM" },
],
editLink: {
pattern: "https://github.com/morluto/rea/edit/main/docs/:path",
},
footer: {
message: "Released under the MIT License.",
},
},
});
@@ -29,12 +29,12 @@ same bytes could therefore disagree because of language, compiler, loader,
analyzer, or provider-version differences while appearing target-compatible.
The decision addressed the earlier representations of
[`CompositeProvider`](../../src/application/CompositeProvider.ts),
[runtime composition](../../src/application/runtime.ts),
[`BinarySession`](../../src/application/BinarySession.ts),
[`BinaryTarget`](../../src/domain/binaryTarget.ts),
[analysis snapshot](../../src/domain/analysisSnapshot.ts), and
[session lifecycle inputs](../../src/contracts/sessionLifecycleInputs.ts).
[`CompositeProvider`](https://github.com/morluto/rea/blob/main/src/application/binary/CompositeProvider.ts),
[runtime composition](https://github.com/morluto/rea/blob/main/src/application/runtime.ts),
[`BinarySession`](https://github.com/morluto/rea/blob/main/src/application/binary/BinarySession.ts),
[`BinaryTarget`](https://github.com/morluto/rea/blob/main/src/domain/binaryTarget.ts),
[analysis snapshot](https://github.com/morluto/rea/blob/main/src/domain/analysisSnapshot.ts), and
[session lifecycle inputs](https://github.com/morluto/rea/blob/main/src/contracts/sessionLifecycleInputs.ts).
The next deep provider must not weaken the existing product invariants:
+60 -13
View File
@@ -2,7 +2,7 @@
REA wraps an existing headless JADX engine through `src/android/`. Its source is
kept as an unmodified, commit-pinned Git submodule; see
[upstream provenance](../third_party/README.md). CLI and MCP use the same
[upstream provenance](https://github.com/morluto/rea/blob/main/third_party/README.md). CLI and MCP use the same
application workflow and return artifact-bound Evidence inline.
This family is present on repository main and in npm 4.1.0. Check the
@@ -11,7 +11,9 @@ connected server's tool list before selecting it.
## Supply tools
Use **Java 17 or newer** and **jadx-headless-mcp 0.7.1**. REA does not install Java,
Use a **full JDK 17 or newer** (including its compiler module) and
**jadx-headless-mcp 0.7.1**. REA runs its packaged Java metadata bridge in
source-file mode against the unmodified engine JAR. REA does not install Java,
an Android SDK, JADX, a device service or an emulator. Set the path in the shell
or MCP server environment:
@@ -21,8 +23,9 @@ export REA_JADX_MCP_JAR=/absolute/path/jadx-headless-mcp-0.7.1-all.jar
export JAVA_HOME=/absolute/path/existing-jdk
```
Linux is verified with the public Appium ApiDemos fixture. The POSIX adapter can
run on macOS, but that host has not undergone real Android verification.
The current metadata bridge is verified on macOS arm64 with OpenJDK 21 and the
public Appium ApiDemos fixture. The POSIX adapter also supports Linux; the new
bridge has not yet undergone real verification on Linux/JDK 25.
Windows is unsupported for this provider's owned stdio process boundary.
## Answer an analyst question
@@ -72,7 +75,10 @@ selected overload. Native/abstract methods with no decompiled body report
Signature verification is explicitly `not_performed`.
- Class search consumes all pages and checks their counts. An empty `query`
inventories all class names; class names are decompiler representations.
- Class inventories return display signatures, fields and inner class names.
- Class inventories read parsed metadata without generating source and return
full reported display types, fields and inner class names. Synthetic members
that code generation hides can appear in the inventory; it stays consistent
after method decompilation within the retained session.
Exact DEX descriptors are unknown. Overload indices are tied to this artifact
and engine, not stable identities across different builds.
- Method inspection reports Java or smali, fallback markers and complete/partial
@@ -88,28 +94,69 @@ selected overload. Native/abstract methods with no decompiled body report
These operations do not execute the APK or provide split APK/AAB handling,
signature validation, full resource-table semantics, native-library analysis or
Android runtime capture. Existing artifact inventory/extraction tools can supply
archive evidence; `project_android_application_graph` remains a separate,
execution-free inventory projection.
archive evidence. Project that Evidence with the execution-free inventory tool:
```sh
rea project-android-application-graph '{"inventory_evidence":[<inventory_artifact Evidence>]}'
```
```json
{
"name": "project_android_application_graph",
"arguments": {
"inventory_evidence": ["<inventory_artifact Evidence>"]
}
}
```
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.
## Resource and lifecycle limits
Each request creates private, immutable APK and JAR copies. Their workspace and
owned process group are cleaned on success, failure, timeout and cancellation.
REA retains one serialized session with private, immutable APK, engine JAR and
metadata-bridge copies. Matching requests reuse the loaded engine. APK/engine
digests and JVM configuration are checked before reuse; changed inputs retire
the previous session. Sessions are cleaned after 60 seconds of inactivity, on
target/configuration changes, failure, timeout, cancellation, MCP disconnect or
server shutdown. One-shot CLI commands always join cleanup before returning.
REA never writes to the original APK or launches target code.
If process cleanup cannot be confirmed, REA retains the workspace and reports its
location. That provider instance blocks subsequent engine launches until the
caller resolves the reported resources and starts a fresh instance.
The provider serializes requests within each REA instance, uses one worker and
sets JVM maximum heap to **512 MiB**. Heap is not a total RSS cap: the JVM also
The provider uses one JADX analysis worker independently of JVM processor and
garbage-collector settings. REA supplies no default heap or visible-CPU override;
Java uses its normal ergonomics and inherits caller JVM settings. Optional
provider settings add explicit JVM arguments:
```sh
export REA_JADX_HEAP_MIB=8192
# Optional, independent of the single JADX worker:
export REA_JADX_ACTIVE_PROCESSOR_COUNT=4
# Or select settings through Java's standard environment options:
export JAVA_TOOL_OPTIONS=-Xmx8g
```
Both REA settings must be positive safe integers; processor counts must also fit
the JVM's signed 32-bit range. REA preserves
`JAVA_TOOL_OPTIONS`, `JDK_JAVA_OPTIONS` and `_JAVA_OPTIONS` rather than replacing
them. Java's standard precedence applies: explicit command-line heap settings
override `JAVA_TOOL_OPTIONS`, while `_JAVA_OPTIONS` can override command-line
settings. Evidence reports the worker's observed `Runtime.maxMemory()` rounded
down to MiB and retains exact bytes, Java version and visible processors in its
raw runtime response. The observed allocatable heap can differ from configured
`-Xmx`, especially with Serial GC. Heap is not a total RSS cap: the JVM also
uses native memory. Independent REA instances have independent queues. Whole
operations have a **120-second** execution deadline after reaching the queue's
front; upstream decompilation is limited to **90 seconds**.
Decoded manifest and method text have a **1 MiB** upstream byte budget and report
truncation. A protocol frame above **8 MiB**, or cumulative stdout/stderr above
**32 MiB**, fails with a diagnostic instead of returning an incomplete inventory
truncation. Class-name pages use indexed slices of up to 4,096 names without changing the
complete, case-sensitive search contract. A protocol frame above **8 MiB**, or combined stdout/stderr above
**32 MiB per operation**, fails with a diagnostic instead of returning an incomplete inventory
as complete. These are memory-safety boundaries, not undocumented result caps.
## Real verification
+98 -4
View File
@@ -1,10 +1,24 @@
# Apple Application Inventory Projection
REA can project one or more authenticated `inventory_artifact` Evidence records
for the same IPA into an Apple application inventory. The projection is
execution-free: it reports exact paths, content hashes, detected formats,
runtime-family hints, and path-based bridge hypotheses. It does not parse plist
or CMS semantics or claim observed runtime calls.
for the same IPA, macOS `.app` directory, ZIP, or DMG into an Apple application
inventory. The projection is execution-free: it reports exact paths, content
hashes, detected formats, bundle anatomy, runtime-family hints, and path-based
bridge hypotheses. It does not parse plist or CMS semantics or claim observed
runtime calls.
```sh
rea project-apple-application-graph '{"inventory_evidence":[<inventory_artifact Evidence>]}'
```
```json
{
"name": "project_apple_application_graph",
"arguments": {
"inventory_evidence": ["<inventory_artifact Evidence>"]
}
}
```
The result includes every component in each inventory category and every
JavaScript-to-native bridge candidate pair. It has no caller-selected component
@@ -16,3 +30,83 @@ 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.
## Application roots
| Inventory root | Application root |
| -------------------------------------- | --------------------------------------------------- |
| IPA | Each `Payload/<Name>.app` |
| `.app` directory | `.`, the inventoried bundle itself |
| Directory, ZIP, or DMG containing apps | Each outermost `<Name>.app` followed by `Contents/` |
ZIP archives made with `ditto -c -k --keepParent` report `<Name>.app`. A DMG
reports `<image>.dmg/<Volume>/<Name>.app`; DMG child inventory needs the native
macOS mounting adapter. When a DMG has only its root identity, the projection is
`partial` and says that bundle absence is unknown. Other inventory formats, such
as APK or ASAR, are rejected as input errors.
An inventoried directory whose name ends in `.app` is its own root even when its
inventory is partial or lacks `Contents/`. For directory, ZIP, and DMG
inventories, components outside every application root are not attributed to
the application. This covers installers, disk-image extras, and AppleDouble
sidecars, which therefore stay out of runtime families and bridge candidates. A
limitation reports how many inventoried entries were left out. IPA projection
still lists every archive component, including `SwiftSupport/`.
`platforms` lists `macos` for application roots with `Contents/`. It lists `ios`
for shallow application roots that have an `Info.plist`. An application root
with neither, such as an empty inventory, has no platform.
## macOS bundle anatomy
`bundles` lists each application root and every nested bundle under it. Each
bundle reports its `parent_path`, `layout`, `info_plist_path`,
`executable_candidates`, and `signing_paths`.
| Layout | Meaning | Content directory |
| --------------------- | --------------- | --------------------------------------------- |
| `macos-deep` | Has `Contents/` | `Contents/`, executables in `Contents/MacOS/` |
| `versioned-framework` | Has `Versions/` | Each real `Versions/<version>/` |
| `shallow` | Neither | The bundle directory itself |
Roles are path conventions (`role_basis: "path-convention"`):
| Role | Path |
| -------------------- | --------------------------------------------------------------- |
| `app-extension` | `*.appex` |
| `xpc-service` | `*.xpc`, including XPC services inside frameworks |
| `framework` | `*.framework` |
| `system-extension` | `*.systemextension` |
| `driver-extension` | `*.dext` |
| `plug-in` | `PlugIns/*.bundle`, `*.plugin`, `*.qlgenerator`, `*.mdimporter` |
| `resource-bundle` | `Resources/*.bundle`, such as SwiftPM resource bundles |
| `bundle` | Any other `*.bundle` |
| `login-item` | `Contents/Library/LoginItems/*.app` |
| `helper-application` | `Contents/Helpers/*.app` |
| `nested-application` | Any other nested `*.app` |
The components also list:
- `privileged_helpers`: files directly in `Contents/Library/LaunchServices/`,
where SMJobBless helpers live;
- `launchd_plists`: `.plist` files directly in `Contents/Library/LaunchAgents/`
(`domain: "agent"`) or `Contents/Library/LaunchDaemons/` (`domain: "daemon"`),
as used by SMAppService;
- `helpers`: files directly in `Contents/Helpers/`.
Symlinks are reported by path in `symlinks`. Their targets are not inventoried,
so a versioned framework is described through its real `Versions/<version>/`
directories rather than `Versions/Current`. A framework's `info_plist_path` is
`null`, with a limitation explaining why, when it has more than one real
`Versions/<version>/` directory (even if only one of them holds a plist,
because `Versions/Current` decides which one applies) or when its single
version directory has no `Resources/Info.plist`.
AppleDouble sidecar files (`._*` and `__MACOSX/`) are inventory facts, but they
describe neighbouring files. They are excluded from roots, bundle roles, and the
anatomy components.
The projection does not read plists. Use `inspect_plist` on a bundle's
`info_plist_path` or a launchd plist to read `CFBundleExecutable`, identifiers,
`SMPrivilegedExecutables`, `BundleProgram`, URL schemes, and usage descriptions.
Use `inspect_signature` on an executable candidate for code-signing claims.
+16 -1
View File
@@ -16,6 +16,8 @@ graph TD
APP["src/application/<br/>Shared CLI/MCP workflows<br/>enhanced analysis and investigations"]
SESSION["SessionProviderRouter + BinarySession<br/>one immutable deep binding per target<br/>disjoint auxiliary routing"]
REGISTRY["AnalysisProviderRegistry<br/>sorted deep candidates<br/>deterministic selection, no fallback"]
RECORDS["src/application/investigation/<br/>composed InvestigationRecords + narrow ports<br/>EvidenceLedger and Unknown ownership"]
SNAPSHOT["BinarySessionRecords + AnalysisSnapshotCache<br/>binary target/profile snapshots<br/>compatibility facade and post-commit notifications"]
JS_RECON["src/application/ + src/artifacts/<br/>JavaScript artifact reconstruction<br/>directory/ASAR reads<br/>AST-only bundle projection"]
end
@@ -25,7 +27,9 @@ graph TD
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"]
BROWSER["src/browser/<br/>CDP / Playwright capture<br/>captured script adapters and publication"]
BROWSER["src/browser/<br/>CDP / Playwright capture<br/>captured scripts and native module resolution"]
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"]
DOTNET["src/dotnet/<br/>Managed static metadata provider<br/>PE/CLI metadata inventory, members, IL"]
FIRMWARE["src/firmware/<br/>Binwalk / Unblob adapters<br/>owned processes, report normalization, private extraction"]
ANDROID["src/android/<br/>Static APK adapter<br/>owned SDK stdio, pinned headless JADX, normalization"]
@@ -75,6 +79,11 @@ graph TD
CLI --> ERRORS
APP --> SESSION
SESSION --> REGISTRY
SESSION --> SNAPSHOT
SNAPSHOT --> RECORDS
APP -->|record callbacks| RECORDS
SERVER -->|narrow record ports| RECORDS
RECORDS --> DOMAIN
REGISTRY --> HOPPER
REGISTRY --> GHIDRA
REGISTRY --> IDA
@@ -87,6 +96,12 @@ graph TD
SERVER --> CONTRACTS
SERVER --> APP
APP --> BROWSER
APP -->|source-map codec port| JS_CODEC
JS_CODEC --> PROCESS
JS_CODEC --> DOMAIN
APP -->|runtime observation port| INSPECTOR
INSPECTOR -->|shared CDP and file-location helpers| BROWSER
INSPECTOR --> DOMAIN
APP --> DOTNET
DOTNET --> DOMAIN
HOPPER --> HOPPER_BRIDGE
+13
View File
@@ -3,6 +3,8 @@
Captured Debugger sources can be exported for REA's existing static JavaScript
analysis with `export_web_scripts`. See [captured website scripts](website-script-export.md)
for the capture options, source mappings, and browser/local resolution limits.
Use [captured module relationships](website-module-trace.md) to resolve one
exported script's native imports under an explicit URL/import-map context.
REA can attach to a user-owned Chrome-family browser through the Chrome DevTools Protocol (CDP) and produce bounded Evidence about an existing page. This is a passive reverse-engineering capability, not a general browser automation or remote-control surface.
@@ -22,6 +24,17 @@ REA can attach to a user-owned Chrome-family browser through the Chrome DevTools
Target discovery returns the complete in-scope target array in one result.
Electron `file://` pages use a separate provider and target boundary; see [electron-observation.md](electron-observation.md).
## Provider authorities
CDP page capture, V8 Inspector observation, and Playwright scenarios stay
separate providers because their authorities differ: CDP and V8 attach to an
already-running target and never launch, drive, evaluate, or mutate it, while
Playwright providers own and drive the runtime (launch browsers, run actions,
capture step snapshots). Merging them would mix attach-only and owned-process
lifecycles in one contract. What they share instead is one exclusion
vocabulary (`BrowserExclusionReason`): every denial reason in every stack maps
onto it, even where a wire schema keeps a narrower historical bucket.
Existing static application Evidence and passive web/Electron captures can be
combined later through
[JavaScript static/runtime reconciliation](javascript-runtime-reconciliation.md).
+264 -1
View File
@@ -13,7 +13,7 @@ there is no new universal provider interface or fixed module manifest.
| Producer parsing and external tool protocols | provider adapters | Keep behind the relevant typed port |
| Shared analyst workflows | application | Share between CLI and MCP |
| Concrete provider construction | binary runtime, composition, MCP startup | Consolidate one capability at a time in production composition |
| Investigation Evidence and Unknowns | BinarySessionRecords | Extract one composed record owner after caller migration |
| Investigation Evidence and Unknowns | InvestigationRecords/EvidenceLedger | Consume narrow read/write/atomic record ports |
| Target/profile snapshots and invalidation | BinarySessionRecords/BinarySession | Keep binary-owned |
| Subprocess ownership and host primitives | process/windows | Reuse; preserve cancellation and cleanup |
| Public translation | CLI/MCP adapters | Delegate to shared workflows and named contracts |
@@ -60,6 +60,21 @@ provider implementations. Production composition is a deliberate outer boundary.
Other existing adapter imports will migrate incrementally; do not widen a lint
exception to conceal new protocol code inside application workflows.
`npm run verify:module-boundaries` parses repository-owned source imports,
reexports, type imports and literal dynamic imports, then checks their resolved
source layer. Nested names such as `domain/android` are domain code; `src/android`
is the provider adapter. The check runs with lint, including cached static gates.
It covers tracked and untracked non-ignored TypeScript source and skips cached
paths removed during a move. Pass exact source paths for a focused check.
Pure source and its tests keep inward dependencies. Application/server provider
construction guards cover the migrated deep, Android, firmware, JavaScript recovery and observation
implementations. The existing binary runtime is an exact temporary composition
exception; shared browser capture/export helpers remain admitted while their
ownership is reviewed. Test lane restrictions still use Oxlint. Producer-dependent
browser and managed comparison tests live in boundary lanes with their original
fixtures and assertions. This is a development guard, not runtime authorization.
Select verification by the claim changed: schema/composition, package, real
engine or host-native workflow. Record host/target prerequisites and source-owned
fixtures. Run heavy lanes serially with bounded workers/heaps and reuse existing
@@ -93,3 +108,251 @@ providers. Composition may import their implementations. Binary composition is
still at its existing entrypoint while its direct-analysis callers are investigated.
This concrete pilot does not introduce a shared provider interface or module
manifest.
## Independent optional observation adapters
Browser observation, browser scenarios, Electron observation, Electron scenarios
and V8 Inspector observation now have separate factories in `src/composition/`.
CLI commands reuse them; MCP startup dynamically imports each factory independently.
`OptionalObservationProviders` describes these five existing typed ports only. It
is provisional startup wiring, not an extensible plugin manifest or a universal
provider interface.
A failed import or constructor retains the successful peers. The complete tool
catalog remains advertised. Session availability reports the failed adapter and
its actual reason, and affected handlers return the existing capability-unavailable
error. Explicit loading failures take precedence over configured availability.
Passive Electron and active Electron failures are independent; static JavaScript
analysis and runtime reconciliation remain usable without either adapter. CLI
Electron commands also load only their selected runtime factory at execution.
Source identities are available without importing the optional implementations.
Registration acquires no engines, probes no endpoints, changes no permissions and
installs nothing. Core transport failures and shutdown still use their existing
lifecycle. Fault-injection coverage uses the production loading/startup path and
actual MCP SDK calls; recording ports establish composition behavior, while the
real Inspector lane separately establishes runtime behavior.
## Investigation record ownership pilot
`src/application/investigation/InvestigationRecords.ts` owns one existing
EvidenceLedger per runtime. The production session composition constructs that
owner explicitly; the direct BinarySession constructor retains a fresh default
for compatibility. There is no second ledger, global store, persistence or new
retention policy.
The ledger, Unknown Evidence helper and the ledger's two original behavior tests
now live beside the owner in `src/application/investigation/`. Bundle validation
and record-owner consumers follow the new paths; retained-reference access still
uses the narrow record ports. The move preserves implementation bodies and test
assertions. Binary snapshot/cache files remain with their existing owner.
EvidenceReader, EvidenceWriter and EvidenceUnknownWriter describe existing
read/write/atomic callback needs. UnknownRegistryPort preserves optimistic
revisions and consistency verification. Non-binary MCP registrars use these
record types without depending on the binary execution/lifecycle surface.
BinarySessionPort retains its compatible record methods through composition.
BinarySessionRecords remains the snapshot owner and compatibility facade. It
passes the actual active target explicitly for recordUnknown/updateUnknown;
recordEvidenceWithUnknown keeps its target-free mutation subject. Snapshot import
merges records without notifying midway, then emits after the cache commits.
Bundle import retains the full change delta so Unknown-only changes notify even
when recordsAdded is zero. Clearing resets records, cache and invalidation before
notification; observer failures remain best effort after a committed mutation.
Close and failed-cleanup clearing, ordinary target-switch retention, detached
reads, Evidence identities and deterministic bundle transfer keep their existing
behavior. Snapshots still require an active target and concrete analysis profile;
extracting investigation ownership does not make them a target-free record store.
#721 remains responsible for any future lifetime-policy change.
## Inspector adapter ownership
`src/inspector/` owns V8 Inspector discovery, passive capture, script/target
location interpretation and its lightweight provider identity. The composition
factory and optional loader use this owner. Browser CDP connection, endpoint/value
and Electron file-location helpers remain deliberate shared adapter utilities at
their existing paths; no provider wire protocol moves into domain or process.
Loopback fixtures and producer boundaries now live under
`tests/fixtures/inspector/` and `tests/boundary/inspector/`. The socket-backed
unresolved-target reconciliation test moves from composition into the forked
boundary lane with its original assertions. Adapter tests retain fork isolation.
`npm run verify:inspector` keeps its public development entrypoint and delegates
to `scripts/verify/inspector/runtime-observation.mjs`, using the real Node fixture
under `tests/conformance/inspector/`. Catalog imports, source guards and test
discovery follow the new paths. Observation authority, lifecycle, producer
interpretation and caller-visible contracts stay unchanged.
## Auxiliary source declaration pilot
Artifact, macOS native and managed static metadata now belongs to each adapter's
`*ProviderMetadata.ts` module. The implementation and
`src/composition/auxiliaryAnalysisProviders.ts` reuse the same identity and exact
capability descriptors. The declarations reference existing AnalysisProvider
loaders; they cover these three disjoint auxiliary implementations only. Deep
candidate selection and the five typed observation ports remain separate.
The binary runtime reads these source declarations while auxiliary construction
stays lazy. The separate managed-only entrypoint retains direct construction for
its execution-free workflow. The MCP catalog generator reads declarations without
constructing implementations, using its canonical macOS platform; actual runtime
host restrictions remain explicit.
Discovery and binary operation-kind routing now consume canonical source contracts
rather than their generated JSON artifact. No declaration imports its generated
output. Generated catalog facts and public schemas stay unchanged.
All observation registrars resolve contracts by name. An isolated compiled-process
test reverses presentation arrays and performs actual MCP SDK calls to verify
advertised schemas and handler meaning. Reordering metadata must not select a
different operation, schema, provider or deep-provider priority.
## Android layer and verification ownership
Android workflows and their existing typed port live in `src/application/android/`,
pure APK/inventory semantics in `src/domain/android/`, and the named inspection
contracts in `src/contracts/android/`. The headless JADX producer remains in
`src/android/`; `src/composition/android.ts` supplies its fresh factory to both
caller adapters. Public names, result meaning and JVM/queue/cleanup limits are
unchanged. Inventory-only application projection remains execution-free.
The real lane and signal-cleanup helper belong to `scripts/verify/android/`; its
explicit downloader and fixed manifest belong to `scripts/fixtures/android/`.
`verify:android` and `fixtures:android` keep their command names and repository-root
`_reference/apk-integration/` fixture default. Synthetic producer fixtures stay in
`tests/fixtures/android/`, with matching boundary and composition ownership. Real
JADX CLI/MCP parity requires the fixed APK and existing audited JAR; synthetic
protocol/cancellation success does not establish that engine or an unverified host.
## Deep-provider source declarations
Hopper, Ghidra and IDA declare their implemented analyst operations in their
own `*ProviderCapabilities.ts` modules. Hopper's inventory is typed against the
existing direct-operation contracts; Ghidra reuses its inventory/function adapter
operations; IDA retains its read-only operation family. Frozen operation arrays
feed the existing capability builders without importing generated catalog data.
The catalog generator reads these source declarations and joins operation names
to canonical `TOOL_CONTRACTS`. A missing contract identifies the declaring provider
and operation. Public schemas and descriptions remain owned by contracts; the
declarations describe implementation coverage. Provider adapters must not consume
the generated MCP catalog that is derived from them. The resolved module guard
checks this edge while admitting canonical source contracts, generated package
metadata and the existing application snapshot-cache consumer.
Exact provider effects and lifecycle differences remain in their adapters:
Hopper's GUI and mutation effects, Ghidra's experimental Windows authority and
mutation restrictions, and IDA's attached/headless effects and live observations.
This source increment introduces no common provider lifecycle or new interface.
## JavaScript application ownership
`src/application/javascript/` owns JavaScript/Electron artifact analysis and
graph projection, feature/version/export/source comparisons, runtime Evidence
and reconciliation, observation ports, and source recovery workflows. Pure
analysis and public schemas keep their existing domain/contract ownership;
shared artifact inventory, investigation records and execution types remain
shared. CLI, MCP and production factories import the owning workflow directly.
`src/domain/javascript/` owns static parsing and semantic IR, application and
semantic graphs, Electron facts, runtime reconciliation, recovery result
semantics, and source-to-bundle comparisons. Shared Evidence, digests, ordering,
comparison semantics and historical-source graphs stay at their existing owners.
Native handoffs link exact-subject Evidence without selecting or executing a
provider.
`src/contracts/javascript/` owns the JavaScript/Electron observation and recovery
contracts, JavaScript-specific workflow requests, and their examples. The shared
`applicationToolContracts.ts` aggregate stays at the contract root because it also
owns reconstruction and Android/Apple projection operations. Canonical tool
aggregation, named handler binding and SDK schema presentation remain unchanged.
Catalog source paths, compiled verifier/SDK fixtures and recovery CI filters
follow the domain/contract owners.
`scripts/verify/javascript/` groups application-workflow, runtime-reconciliation,
large-digest and real-recovery entrypoints. Their existing npm commands retain
their invocation semantics. Recovery fixture compilation lives beside its
unchanged sources under `scripts/fixtures/javascript-recovery/`. Its default
REA entrypoint still resolves to `scripts/rea.mjs`. Managed conformance imports
the relocated compiled workflow; owning CI source filters follow the new paths.
`src/artifacts/javascript/` owns directory/ASAR reader construction and relevant
file acquisition, including root/nested byte verification, UTF-8 diagnostics and
nested reader cleanup. Its narrow factory returns the existing `ArtifactReader`;
the application retains input admission, inventory, graph construction and its
original root-reader `finally` close. Pure file facts live in
`src/domain/javascript/javascriptArtifactFiles.ts`, and the shared immutable
inventory snapshot lives in `src/domain/artifactInventorySnapshot.ts`. Inventory
options and integrity policy remain application-owned. Shared stream hashing
lives in `src/artifacts/ArtifactHash.ts`, preserving complete byte counts and the
bounded classification prefix. Acquisition/hash guards reject imports from
application, composition or caller adapters; JavaScript workflows use the factory
instead of importing concrete ASAR/directory reader classes.
ZIP, native-DMG and Mach-O inventory reader selection keep their existing
lifecycles. Runtime ports retain their distinct Inspector, passive Electron and
active Electron effects; shipped Electron hooks remain at their existing
provider boundary.
## Managed capability ownership
`src/domain/managed/` owns managed artifact and member semantics, reconstruction
import, native-boundary verification and application graph projection.
`src/application/managed/` owns their shared Evidence workflows, and
`src/contracts/managed/` owns their named contracts and examples. Producer
metadata/IL parsing stays in `src/dotnet/`; target resolution, Evidence, ordering
and investigation records retain their shared owners. Path-based member
comparison preserves its actual target/byte-digest admission and existing parser
helper.
`scripts/verify/managed/` groups the portable conformance entrypoint and its
support, manifest, oracle and completion-report helpers. Its PE byte fixture
builder and declaration live in `scripts/fixtures/managed/`, shared with the
installed-package lane and existing managed conformance tests. `verify:managed`
keeps its command name and optional caller-cwd manifest resolution. Compiled
catalog and generator imports follow the owners; document-check cache inputs
include the relocated runtime verifier and fixture files.
Portable byte fixtures, optional operator-local manifests, actual ILSpy oracles
and real Ghidra NativeAOT checks retain their distinct proof levels and
prerequisites. The existing NativeAOT fixture/engine lane keeps its owner. This
move preserves implementations, Evidence identities and generated commitments.
## Binary application ownership
`src/application/binary/` owns the active session, deep-provider registry and
evaluation, operation routing, disjoint provider composition, lazy auxiliary
clients, cancellation and client cleanup. Its records facade and snapshot
cache/files remain bound to the exact target and analysis profile. Investigation
records still have their separate owner in `src/application/investigation/`.
`AnalysisProvider.ts` stays shared because its identity/execution types also serve
nonbinary ports. `BinaryTargetResolver.ts` stays shared because Android, firmware,
managed and artifact workflows use its target parsing. The existing `runtime.ts`
remains the exact temporary production wiring entrypoint while direct-analysis
callers are migrated deliberately; application workflows gain no outward
composition dependency.
The three colocated application tests move with their owner. Composition cases
remain in `tests/composition/analysis-sessions/`; filesystem and SDK cases retain
their boundary lanes. The shared `tests/fixtures/binarySession.ts` factory supports
several capability families. Existing recursive application globs discover the
moved tests; bridge assets and real-verifier entrypoints remain at their owners.
## Firmware layer and verification ownership
Firmware workflows and their existing typed port live in
`src/application/firmware/`, pure request/result semantics in
`src/domain/firmware/`, and named contracts in `src/contracts/firmware/`.
`src/firmware/` keeps producer commands, report parsing, source copies, publication
and cleanup; `src/composition/firmware.ts` supplies a fresh typed factory.
The real lane belongs to `scripts/verify/firmware/analysis.mjs`; the fixture runner
and its unchanged Python producer belong together in `scripts/fixtures/firmware/`.
The npm command names and selected `REA_FIRMWARE_FIXTURE_ROOT` are unchanged. The
runner retains its working-directory-relative default; the verifier retains its
repository-root default. Both point to `_reference/firmware-integration/generated`
when invoked through the documented npm commands. Optional ext4/Ghidra lanes keep
their own prerequisites. Source-fixture generation and synthetic producer tests
do not establish real Binwalk/Unblob analysis or another host's execution support.
+9 -5
View File
@@ -1,7 +1,10 @@
# Firmware analysis
REA integrates **Binwalk 3.1.0** for region inspection and **Unblob 26.6.4** for
explicit extraction. This first firmware milestone is verified on Linux x64.
REA accepts **Binwalk 3.1.x** for region inspection and **Unblob 26.6.x** for
explicit extraction. The report parsers are verified with Binwalk 3.1.0 and
Unblob 26.6.4. Another build on those lines is accepted, the observed version
is reported, and a limitation records that the parser was not verified against
that build. This first firmware milestone is verified on Linux x64.
No device, root privilege, mount, emulation or execution of extracted programs
is needed. Arbitrary firmware bytes do not pass native executable admission;
select a returned ELF separately for supported Ghidra analysis.
@@ -16,7 +19,8 @@ export REA_UNBLOB_COMMAND=/absolute/path/to/unblob
```
REA does not install these tools, their dependencies or system packages. It
checks the actual tool version when executing a request. Inspection and
checks the tool's `--version` banner when executing a request. Binwalk must
print `binwalk <version>` and Unblob must print the version alone. Inspection and
extraction have independent prerequisites; one does not require the other.
Both require util-linux `prlimit`, normally `/usr/bin/prlimit`; override its
absolute path with `REA_FIRMWARE_PRLIMIT_COMMAND` if needed.
@@ -34,7 +38,7 @@ git submodule update --init third_party/binwalk third_party/unblob
```
Normal REA builds and npm packages do not include or build these sources or
engine binaries. See [upstream provenance](../third_party/README.md).
engine binaries. See [upstream provenance](https://github.com/morluto/rea/blob/main/third_party/README.md).
## CLI and MCP
@@ -161,7 +165,7 @@ Additional lanes are explicit, so the base lane stays small:
REA_FIRMWARE_VERIFY_EXT4=1 npm run fixtures:firmware
REA_FIRMWARE_VERIFY_EXT4=1 npm run verify:firmware
# Existing Ghidra 12.1.4 and Java required.
# Existing Ghidra 12.1.x and its declared JDK required.
REA_FIRMWARE_VERIFY_GHIDRA=1 npm run verify:firmware
```
+3 -3
View File
@@ -2,9 +2,9 @@
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
decompilation through the CLI and stdio MCP. It requires the same Ghidra
12.1.4 and 64-bit JDK 21 installation as other Ghidra sessions; no DOS emulator
or cross-compiler is required.
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.
## Open and inspect
+4 -1
View File
@@ -7,7 +7,10 @@ source notices are retained. No upstream source or algorithms are rewritten.
The initial tested layout is .NET runtime **8.0.22**, NativeAOT RTR **9.1**,
x86-64 ELF and native Windows PE **targets analyzed on Linux x64**, using
Ghidra **12.1.4** and JDK **21**. Other layouts/architectures/hosts are unsupported.
Ghidra **12.1.4** and JDK **21**. The build accepts any Ghidra 12.1.x release and
javac 21 or newer, and it compiles with `--release 21`. Real verification of
this layout remains the Ghidra 12.1.4 and JDK 21 pair. Other NativeAOT layouts,
architectures, and hosts are unsupported.
Windows Ghidra P0 has no database mutation authority. A PE/CLI or ReadyToRun
assembly should use `inspect_managed_artifact`; it is not NativeAOT.
+46
View File
@@ -0,0 +1,46 @@
---
layout: home
hero:
name: REA
text: Reverse Engineer Anything
tagline: Understand binaries, applications, and runtime behavior with one CLI and MCP server.
actions:
- theme: brand
text: Get started
link: /installation
- theme: alt
text: View on GitHub
link: https://github.com/morluto/rea
features:
- title: Investigate with your agent
details: Connect REA to your coding agent, describe the app or feature you want to understand, and follow the evidence.
link: /mcp-prompts
- title: Inspect across formats
details: Explore native binaries, JavaScript and Electron apps, .NET assemblies, Android packages, firmware, and websites.
link: /installation
- title: Keep the evidence
details: Analysis runs locally and returns observations, limitations, and unknowns through the CLI and MCP.
link: /mcp-contracts
---
## Start with one command
```bash
npx rea-agents setup
```
Choose your agent, review the planned changes, and approve setup. Native binary
analysis can use an existing Hopper, Ghidra, or IDA installation; static
JavaScript analysis needs no native analysis engine.
For a first result from an extracted JavaScript application or ASAR:
```bash
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
See [installation and setup](./installation.md) for requirements and configuration.
![REA inspecting a native binary in Hopper](./assets/rea-hopper-analysis.png)
+16 -6
View File
@@ -90,7 +90,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](../skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed).
[conditional connection guide](https://github.com/morluto/rea/blob/main/skills/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
@@ -108,7 +108,9 @@ 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.
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).
## Supported agents
@@ -129,6 +131,11 @@ Setup can configure these clients for REA's local MCP server:
| Command Code | `commandcode` |
| VS Code | `vscode` |
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.
## Review setup changes
`rea setup` first offers the supported agents in a multi-select. Existing REA
@@ -264,7 +271,10 @@ copying license secrets into logs, or killing unrelated Hopper processes.
REA connects to an existing Ghidra installation on Linux x64, macOS x64/arm64,
or experimental Windows x64 P0.
It requires Ghidra 12.1.4 and a 64-bit full JDK 21. On macOS, the installation
It accepts Ghidra 12.1.x and the 64-bit full JDK declared by that installation's
`application.java.min` and `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, the installation
must include the native decompiler for the host architecture; REA does not
build it or change Gatekeeper quarantine settings.
@@ -310,9 +320,9 @@ installation remains unavailable on Windows. Setup never installs Ghidra,
Java, or Python. It preserves valid detected Ghidra/JDK settings in agent
registrations. See [Windows Ghidra P0](windows-ghidra-p0.md) for provider diagnostics.
Doctor validates the platform, architecture, application version,
`support/analyzeHeadless` or `support/analyzeHeadless.bat`, Java
version/bitness, and the presence of `javac`/`javac.exe`.
Doctor validates the platform, architecture, Ghidra 12.1.x application version,
`support/analyzeHeadless` or `support/analyzeHeadless.bat`, the installation's
Java major range, 64-bit JDK bitness, and the presence of `javac`/`javac.exe`.
When Java is found through `PATH`, setup records its observed JDK home so GUI
MCP clients do not depend on an incidental shell path. Setup shows every exact
environment entry in its plan, writes only after approval, and never downloads,
+90
View File
@@ -0,0 +1,90 @@
# JavaScript source recovery
`recover_javascript_sources` / `recover-javascript-sources` derives readable
modules from one explicitly selected UTF-8 JavaScript file or bundle. Use it on
files returned by `export_web_scripts` or other local scripts. The operation
never executes the selected or recovered application code.
## Caller-supplied engine
The initial adapter is verified on Linux x64 with Wakaru **1.13.0**. Set
`REA_WAKARU_COMMAND` to the absolute path of an executable supplied by the caller.
The adapter uses util-linux `prlimit` (default `/usr/bin/prlimit`; override with
an absolute `REA_JAVASCRIPT_PRLIMIT_COMMAND`). REA does not install either tool.
Engine configuration is checked only when recovery is requested; other REA
capabilities remain available without it.
Unmodified upstream source is retained in `third_party/wakaru`, pinned to
`6070266d24b32951fa2fe1d2dad5b4313bd5c81b` (v1.13.0, Apache-2.0).
The result reports the executed binary's exact path, SHA-256 and observed version.
The audited source revision is distinct from `executed_source_revision: null`:
a version string cannot prove which source produced a caller-supplied binary.
## Recover and analyze
```sh
rea recover-javascript-sources /tmp/web-scripts/app.js /tmp/recovered-app --json
rea analyze-javascript-application /tmp/recovered-app/modules --json
```
MCP input:
```json
{
"path": "/tmp/web-scripts/app.js",
"output_directory": "/tmp/recovered-app"
}
```
The output directory must be absent, absolute, and have an existing parent.
The source is snapshotted in a private workspace, and the engine writes only to
its own staging directory. Reported module paths, input identities, UTF-8 byte
ranges, regular files and emitted map representations are validated before
publication. REA writes verified bytes to an exclusively owned output tree and
rolls it back on failure. It also checks that the original source and engine
bytes remain unchanged before committing. The existing output is never overwritten.
Published contents:
- `inputs/bundle.js`: exact original snapshot; its digest binds the Evidence.
- `modules/`: derived modules and any upstream-emitted source maps.
- `reports/stdout.json` and `reports/provenance.json`: exact producer reports.
- `manifest.json`: identities, modules, transform choices, warnings and limitations.
The inline result includes complete file paths/digests/sizes, extraction ranges,
warnings and `analysis_input`, directly usable as the arguments to
`analyze_javascript_application`. Raw Evidence retains selected command arguments,
reported input paths, exit status, stderr and reports; temporary paths are recorded
as historical execution coordinates after their workspace is removed.
## Transform choices and interpretation
`extraction_mode` defaults to `structural` (Wakaru `--unpack=strict`). `heuristic`
uses `--unpack=auto`; `inspection` uses `--unpack=inspect` and can return finer,
non-executable regions. `rewrite_level` accepts `minimal`, `standard` (default)
and `aggressive`. Maps and provenance are requested in every operation.
Structural extraction can fall back to a single file. In the real fixtures,
webpack 5 yields two modules; the esbuild scope-hoisted fixture remains one
unknown-format script. Returned module counts and detected formats report those
actual outcomes. A valid report with failed transformations returns `partial`
with original warnings and files. An unusable report, inconsistent provenance,
escaping path, symlink or missing module is an error, with owned cleanup.
Evidence confidence is `derived`. Extraction ranges are half-open **UTF-8 byte
offsets in the original snapshot**, rather than rewritten line positions or a
coverage assertion. Source maps are preserved without claiming independently
verified mapping accuracy. Original variable names and arbitrary runtime
equivalence remain unknown. No network fetching occurs.
## Resource policy and verification
Each operation uses one worker, a 1 GiB address-space ceiling and a 120-second
deadline; it accepts up to 64 MiB of input and 128 MiB/10000 entries of staged
output, with 8 MiB per diagnostic/report stream. Exceeding a limit is an explicit
failure with rollback. These limits apply per operation and do not claim aggregate
host containment across concurrent callers.
See [testing.md](testing.md#javascript-source-recovery) for the focused real
Wakaru CLI/MCP lane and its source-owned compiler fixtures. Other host platforms
and Bun/Vue recovery are not claimed by this adapter.
+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`](product-catalog.json).
tool inventory is [`product-catalog.json`](https://github.com/morluto/rea/blob/main/docs/product-catalog.json).
## Shipped scope
@@ -284,7 +284,13 @@ Cross-version matching first prefers exact CIL/signature identity. When that
does not match, it pairs an exact declared type, method name, and raw signature
before trying structural body shape. The exact-signature key uses names only
as part of that full tuple; names alone never select a pair. Duplicate tuples
remain ambiguous rather than being paired by token order.
remain ambiguous rather than being paired by token order. Fields follow the
same exact tiers. The raw signature is exact whether or not REA decoded it, so
undecoded signatures pair by that tuple too, but they never enter structural
rounds. An unmatched member is therefore reported as `unknown`, not added or
removed, when its own signature was not decoded or when the other side has an
unpaired member, one-sided or ambiguous, with the same declared type and name
whose signature was not decoded.
For a matched method, an unavailable or partial body makes body-shape facets
unknown while preserving observed signature differences. Structural identity
+21 -8
View File
@@ -30,8 +30,8 @@ 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.
- `inspect_native_dispatch_metadata` / `rea inspect-native-dispatch-metadata
<app-or-binary>` prefers a validated macOS Mach-O byte reader. It decodes
- `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,
absolute/relative method entries, ivar offsets/sizes/alignment and protocol
declarations. It also decodes simple Swift conformances, static synchronous
@@ -39,9 +39,20 @@ explicit limitation; architecture inventory locations remain available.
vtable descriptors. Records include exact virtual addresses/file offsets and
artifact evidence. Vtable indexes are metadata word offsets; witness indexes
start after the conformance header. Names unavailable in metadata remain null.
Chained fixups, external bindings, authenticated pointers, categories,
properties, generic/resilient/async/coroutine tables and inherited overrides
have explicit unsupported or partial coverage. Other providers retain the
Pointers are decoded through the image's fixups:
- `LC_DYLD_CHAINED_FIXUPS` rebases and binds, including the authenticated
arm64e formats;
- legacy `LC_DYLD_INFO` bind opcodes.
So an external superclass resolves from its `_OBJC_CLASS_$_` bind (for
example `NSObject`). The `pointer_fixups` coverage facet names the mechanism.
Class properties, with parsed attributes, and `__objc_catlist` categories are
also decoded. Categories record the extended class, local or external, plus
their methods, protocols and properties; category methods appear as
implementations with a `category`. Swift field-offset globals that are only
initialized at runtime stay unresolved. Generic, resilient, async and
coroutine tables, and inherited overrides, have explicit unsupported or
partial coverage. Other providers retain the
existing symbol-based inventory with its narrower coverage.
## Native instruction, call and type primitives
@@ -150,7 +161,7 @@ actions may change application data or trigger network activity.
## Provider and verification boundaries
Install Ghidra 12.1.4 and a full 64-bit JDK 21 separately, then configure REA to
Install a Ghidra 12.1.x release and the 64-bit full JDK it declares, then configure REA to
use them. Ghidra analysis supports Linux x64 and macOS x64/arm64; macOS requires
the matching native decompiler. Experimental Windows x64 P0 admits native
x86-64 PE applications on local NTFS using bundled Job Object ownership,
@@ -165,8 +176,10 @@ GUI authority, and REA never falls back automatically to Hopper.
instruction/call facts, value dependencies and process/project cleanup.
- `npm run verify:ghidra:aarch64-jump-table`: optimized ELF and byte/halfword
relative tables, plus ARM64 Mach-O on an ARM64 macOS host.
- `npm run verify:apple-dispatch`: source-built Objective-C protocols/classes and
Swift conformances/vtables, repeated after stripping local symbols.
- `npm run verify:apple-dispatch`: source-built Objective-C protocols, classes,
properties and categories, and Swift conformances and vtables. Each is linked
with legacy `LC_DYLD_INFO` and with chained fixups (and as arm64e on Apple
silicon), then repeated after stripping local symbols.
- `npm run verify:native-ui`: one source-owned fixture window, successful
selected-window capture and actions, changed-target rejection, and cleanup.
Missing OS permissions fail this lane.
+43 -11
View File
@@ -7,10 +7,10 @@
"client": "2.3.1",
"core": "2.3.1"
},
"skill_version": "26"
"skill_version": "27"
},
"tools": {
"total": 126,
"total": 131,
"families": [
{
"id": "direct",
@@ -179,7 +179,7 @@
{
"id": "application",
"surface": "application-workflow",
"count": 8,
"count": 13,
"tools": [
"build_reconstruction_obligation_ledger",
"compare_application_versions",
@@ -187,8 +187,13 @@
"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_javascript_semantics",
"trace_web_module_imports",
"trace_web_source_location"
]
},
{
@@ -350,6 +355,18 @@
"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",
@@ -455,13 +472,23 @@
"id": "rea-web-script-export",
"name": "REA captured script export",
"version": "1",
"capabilities": ["export_web_scripts"]
"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": [
@@ -539,7 +566,7 @@
}
],
"cli": {
"primary_count": 81,
"primary_count": 86,
"commands": [
"address-to-file-offset",
"analyze",
@@ -604,10 +631,13 @@
"observe-javascript-runtime",
"observe-native-ui",
"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",
@@ -618,6 +648,8 @@
"trace-javascript-semantics",
"trace-native-ui-action",
"trace-native-values",
"trace-web-module-imports",
"trace-web-source-location",
"uninstall",
"update",
"verify-managed-native-boundaries",
@@ -632,15 +664,15 @@
},
"runtime_catalog": {
"counts": {
"cli_commands": 81,
"mcp_tools": 126,
"cli_commands": 86,
"mcp_tools": 131,
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "569db1182f19869a3bdb22cf217bc75526f317b59635c8866d0e62741077cd52",
"tools_sha256": "f49e05c52c6fd9955edcede27fbaaa248fdb917caa7b27edb8d97f7a30ad250e",
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
"combined_sha256": "e21455dd7d64af94e0b07aff55ab82f03a37e46729ece51be38b50083d2d5b55",
"providers_sha256": "7949ddcf2eb5ba41b022bb3dfa1951ab96dbf5640285fa7fd5d35b0e4a5bdab3"
"combined_sha256": "54ac160db3b96580a9a0203a07fe3db01ffc05224db621f48b4abdf979e4f50b",
"providers_sha256": "a7d885531ee25d42c51385444faec544d34cd7eba6250a7dc75c4faedd4454a3"
}
}
}
+9 -5
View File
@@ -3,8 +3,10 @@
This guide describes repository main. Check the [released package boundary](installation.md#released-package-and-main) when using npm.
Ghidra read-only analysis is available on Linux x64 and macOS x64/arm64.
Install Ghidra 12.1.4 and a full 64-bit JDK 21 separately, then configure REA to
use them. macOS installations need the matching native decompiler.
Install a Ghidra 12.1.x release and the 64-bit full JDK that release declares,
then configure REA to use them. 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.
macOS installations need the matching native decompiler.
REA imports one target into a temporary project and exposes 25 read-only
operations after analysis completes. They cover inventories, search,
@@ -53,9 +55,11 @@ follow.
## Shipped foundation boundary
`GHIDRA_INSTALL_DIR` must identify an extracted official 12.1.4 release;
optional `JAVA_HOME` must identify a 64-bit full JDK 21, otherwise doctor probes
`java`/`javac` or `java.exe`/`javac.exe` from `PATH`. Supported Linux and macOS
`GHIDRA_INSTALL_DIR` must identify an extracted Ghidra 12.1.x release. Optional
`JAVA_HOME` must identify a 64-bit full JDK inside that installation's
`application.java.min` and `application.java.max` (JDK 21 or newer, with no
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
+4 -2
View File
@@ -45,8 +45,10 @@ 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.
Ghidra analysis supports Linux x64 and macOS x64/arm64 with Ghidra 12.1.4 and a
64-bit full JDK 21. macOS also requires the matching native decompiler. The
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
or newer and set no maximum; the bridge is verified with Ghidra 12.1.4 and JDK 21.
macOS also requires the matching native decompiler. The
adapter exposes thirteen inventory operations and twelve function-analysis
operations, for 25 read-only operations total. Linux and macOS additionally support
atomic function-name and entry-comment edits with refreshed analysis; metadata
-169
View File
@@ -1,169 +0,0 @@
# Test suite audit
## Scope and priority
The expanded audit on 2026-10-04 reviewed every one of the 355 Vitest files
present at its start: 82 domain/contracts, 56 application/composition, 147
adapter/configuration/process/filesystem/CLI files, and 70 MCP/acceptance/
conformance/evaluation files. Three review passes checked the proposed removals
against existing stronger coverage and checked the final changes for lost
assertions. The remaining suite has 351 files.
Prefer full real-provider E2E, then integration across schemas and transports,
then real-data goldens. Remove lower-level duplication once the replacement
asserts the same behavior. Keep distinct failure regressions that a successful
E2E cannot exercise. Do not lower coverage thresholds to accommodate pruning.
## Changes made
| Previous test or assertion | Decision | Replacement or retained claim |
| ----------------------------------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Asset application tests using fabricated `.car` bytes and injected utility JSON | Remove two plumbing cases | `verify:asset-catalog`: real actool → assetutil → CLI and stdio MCP, including digest, full metadata, pagination and invalid input |
| Keyed archive positive binary/XML objects assembled with the same plist library as the reader | Remove two synthetic success cases | Captured Foundation XML plus reviewed complete object/reference graph golden; real Foundation binary/XML → CLI and stdio MCP |
| Generic contract schema/annotation/example loops and output-map inventory comparisons | Consolidate into one SDK wire test | Assert exported schemas, guidance, effect annotations and valid examples on the actual advertised MCP catalog |
| Ghidra function operation enum membership | Remove | Canonical catalog, provider handshake and actual operation roundtrips establish admission; malformed provider results remain covered |
| Asset page assertion pinning `10_001` | Remove | Invalid-page rejection through real MCP; malformed utility data still covered |
| Suggested prompt tool positions | Remove incidental ordering assertions | Keep authority, evidence, untrusted input and preparation constraints |
| Package “acceptance” assertions pinning CI matrix/shard values and fake launcher command arrays | Remove configuration-shaped cases | Actual package/install verifier remains; retain a compact release verification-before-publish and registry-canary static guard |
| Package failure classifier under acceptance | Move to package evaluation | Preserve all malformed/unrelated-failure rejection cases; do not call them E2E |
| Native value graph checks conditional on success | Strengthen | Require success before pagination/ambiguity assertions; unexpected errors can no longer pass |
| Native UI verifier treating permission denial as a positive pass | Separate outcomes | Default lane requires real capture/action success; opt-in permissions lane explicitly reports no positive E2E proof |
Ten previous test cases were removed or consolidated; three replacement
Vitest cases cover the captured goldens and release guard. The two real artifact
E2E scripts add product-level evidence outside the deterministic Vitest count.
## Exhaustive follow-up
The follow-up removes or consolidates another 31 test declarations, including
three redundant test files. Parameterized declarations and executed cases have
different counts; the figures above count source declarations. It also repairs
assertions that could pass without exercising their stated behavior.
| Area | Cleanup | Stronger retained evidence |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Configuration | Merge duplicate target/loader tests and preserve custom launcher, explicit provider and actionable error diagnostics | Independent malformed inputs; each capability flag is rejected separately |
| Browser scenarios | Remove fake launch success and expression-selected artifact success | Real Chromium scenarios, actual artifacts, CLI capture and owned/external cleanup in `verify:browser` |
| CLI policy | Remove duplicate confirmation helper cases | Compiled CLI proves project files stay unchanged until `--yes`; distinct interactive choices remain |
| Provider capabilities | Remove numerical catalog freezes; consolidate Hopper byte-read discovery | Exact canonical capability identities, admission, mutation effects and detached descriptors |
| Evidence ledger | Merge reordered-key checks into serialized bundle import; remove one-record quota claims | Atomic import, conflicts, residual revisions, actual large collections and byte regressions |
| MCP batching and concurrency | Replace maximum concurrency and empty indistinguishable responses | Every distinct ordered batch result; concurrently gated requests complete in reverse order with matching payloads |
| MCP inventories and graphs | Remove traversal/inventory call order and redundant catalog/prose checks | Exact complete cyclic graph, full inline inventories, canonical SDK catalog and all-handler roundtrips |
| Authority and lifecycle | Attempt the refused Ghidra mutation; require successful setup, call and switch; close twice | Typed refusal with zero mutation dispatch, restored target calls and exact owned-resource cleanup |
| Process and provider output | Remove incidental environment read and command option order/defaults | PID/ownership validation, exact disabled-policy failure and complete decoded response above 10 MiB |
| Conformance and CI guards | Merge identical replay runs; remove duplicate missing-row fault, trivial hash/self equality and incidental worker/step counts | Distinct faulty evidence, digest order, actual test discovery, file existence and release/runner authority |
| Pure schemas | Remove fabricated comparison-dimension injection and impossible long boundary success | Real artifact comparison and SDK coverage evaluation with returned boundary identity |
The removed long boundary selector test only parsed a 1,000-character request;
workspace boundary identities admit at most 200 characters, so that positive
could never represent a successful evaluation. The actual MCP evaluation stays
covered. One unused ledger serialization helper was also retired after removing
its only test consumer; no CLI or MCP contract changed.
No further immediately safe pruning was identified in the final review. Retained
replacement candidates below are coverage gaps, rather than permission to delete
unique tests. Test-only domain scaffolding also stays pending a separate decision
about the corresponding source behavior; deleting those tests alone would leave
unverified code and fail unused-export checks.
## Stronger replacements from the closing pass
Three more synthetic success cases were removed. Native value composition now
runs through the production CLI and a separate stdio MCP subprocess with real
Ghidra, using a source-built native fixture. It checks argument/parameter/return
relationships, the fixture's global operand, authenticated Evidence, upstream
and workflow analysis profiles, identical CLI/MCP graphs, discovery availability
and successful MCP session closure. Ambiguity, cancellation, target changes,
missing flow and bounded-work regressions remain in the focused suite.
That replacement found missing CLI workflow routing for `trace_native_values`
and missing workflow profile classification. The routing and discovery now use
the composed operation's actual provider prerequisites; workflow Evidence commits
to the upstream Ghidra profile.
The lone hand-built client-document equality test is replaced by a real TOML
filesystem transaction: reversed environment keys leave the original bytes
unchanged and create no backup. A real ZIP entry with an application root longer
than 5,000 characters replaces injected schema-only positive data. Inventory
completeness tests partition actual scans into authenticated partial observations
and require exact assembled results and every source citation, including more
than 100 citations. These are consumer merge tests, not provider pagination
claims. Replay aggregation now covers all four statuses and retains actual runner
diagnostics; it does not claim to execute a real sandbox. Retained pixel-metric
fixtures now write correct PNG chunk checksums.
The all-zero screenshot buffer is replaced by a real Chromium PNG of 9,467,940
bytes, transported intact through CLI and stdio MCP and decoded by the production
pixel comparator. The verifier configures the SDK client's receive buffer because
its 10 MiB default is smaller than the inline JSON response. This does not claim
large comparison-request transport coverage. A real SDK initialize request,
captured during a successful production stdio handshake, replaces the JSON-RPC
decoder's field-count-only oracle with exact field/value expectations.
The closing pass also removed assertions tied to internal call order, defaults,
private cache sizes, duplicated polling and fabricated schema extensions. Unique
malformed-input, security-capacity and cleanup regressions remain.
## Real process regression found
The full suite exposed a renderer ordering race. Publishing a terminal output
journal entry could synchronously trigger a reactive resize before the preceding
output was queued for rendering. Queue that output first. The existing real
source-owned interactive PTY test now also requires the first resized frame to
contain the output that triggered the resize, preserving the original event
order rather than sorting or changing timestamps to conceal it.
## Coverage kept
Malformed archives, broken and cyclic references, unsupported metadata pointers,
relative Objective-C method layouts, ambiguous call candidates, digest mismatch,
cancellation, host permission denial, bounded output and owned-resource cleanup
remain covered. These cases provide evidence beyond a successful real-tool run.
The hand-assembled relative method fixture stays until compiler-produced
relative-list fixtures establish equivalent assertions.
## Remaining priorities
1. Extend the real Ghidra CLI/stdio MCP journey from value tracing to dispatch
and UI handlers before pruning their hand-authored composition success cases.
Native value tracing now establishes full transport parity.
2. Real binary dispatch and nib → full product transports, with normalized
semantic goldens. The compiler-backed readers already run in the new Apple
CI lane; synthetic pointer/layout rejection tests remain useful.
3. An approved interactive macOS runner for native UI capture/actions, including
stdio MCP scenarios. Hosted runners without permissions cannot supply this
proof. No arbitrary application's windows should be opened for this lane.
4. Real macOS/Linux Ghidra CI runners with bring-your-own installations. This
change adds Apple artifact CI without installing Java or Ghidra.
5. Classify fake-runner replay/application acceptance accurately and replace
repeated happy paths only after real sandbox journeys assert the same claims.
The replay workflow's allow-unavailable mode must not imply executed replay.
6. Replace repeated-array schema ceiling tests with real producer/transport
regressions before deleting them. Existing integration tests do not yet
establish their large-input guarantees.
Some static guards protect irreversible release authority rather than product
behavior. Keep these few guards explicit and separate from E2E evidence.
## Verification after the expanded audit
- `npm run check:pr`: 351 files passed; 1,794 tests passed and two skipped.
Typecheck, lint, formatting, unused-code and generated-document gates passed.
- `npm run check:fast`: passed; 70 lint warnings and zero errors.
- `npm run verify:browser`: actual Chromium observation and scenario CLI routes,
retained artifacts, owned/external lifecycle checks and large CLI/stdio MCP PNG
capture passed.
- `npm run verify:ghidra`: macOS ARM64 with Ghidra 12.1.4/JDK 21 passed, including
real native value tracing through CLI and stdio MCP: 171 nodes, 298 edges and
six decompilations.
- `npm run verify:asset-catalog`: real actool/assetutil → CLI and stdio MCP,
complete records, pagination and malformed input checks passed.
- `npm run verify:keyed-archive`: real Foundation binary/XML → CLI and stdio MCP,
full graph golden, shared/cyclic identity and malformed scope checks passed.
- `npm run verify:fixtures`: seven source-built host fixtures, source/artifact
digests and exhaustive inventory checks passed.
No Hopper windows were opened for this audit. The closing pass runs real Ghidra
on macOS ARM64; earlier Hopper evidence remains separate. No new hosted CI success
is claimed.
+147 -15
View File
@@ -32,6 +32,12 @@ registries but do not cross production filesystem, process, socket, or browser b
Boundary tests cross one production boundary. Only acceptance tests assemble
the complete runtime or invoke the compiled product surface.
Binary session, registry, lazy-client and snapshot module cases live under
`src/application/binary/`. Their composition cases stay in
`tests/composition/analysis-sessions/`; snapshot persistence and actual SDK calls
stay in the filesystem and MCP boundary lanes. The shared injected session fixture
serves several capabilities and remains under `tests/fixtures/`.
Focused immutable builders and recording ports shared by one test family live
beside their production owner as `src/**/*.fixture.ts`. They are typechecked
with the suite and excluded from package builds; broader runtime and provider
@@ -55,6 +61,16 @@ needed, exercise the actual target through browser, Electron, or process
capture. Real model trials are manual; Vitest covers deterministic evaluator
logic.
`verify:managed` runs the portable PE byte-fixture conformance entrypoint under
`scripts/verify/managed/`, with its byte builder under
`scripts/fixtures/managed/`. It checks static classification, members,
reconstruction, native-boundary relationships and application graphs without
executing fixture PE files. Operator-local manifests and actual ILSpy oracles
remain optional, separately reported checks; the real Ghidra NativeAOT lane has
its own toolchain prerequisites. See [the managed guide](managed-code-analysis.md)
for those configurations. Generated completion-ledger checks use the same owning
entrypoint and include its verifier/fixture files in their cache inputs.
## End-to-end, integration and golden evidence
Full E2E tests invoke the production command dispatcher and real providers,
@@ -65,6 +81,13 @@ an XML graph golden. `verify:asset-catalog` compiles source-owned colors with
`actool`, invokes real `assetutil`, then checks CLI/MCP results, exact catalog
digest, every raw metadata field, pagination and malformed input rejection.
Neither artifact workflow requires Hopper or launches it. Both run in macOS CI.
`verify:macos-bundle` needs only macOS with Command Line Tools. It compiles a
source-owned app with `clang`: a versioned framework, XPC services, an app
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.
MCP SDK transport tests with recording providers remain integration tests.
They are useful for schema drift and failure projection but do not prove that
@@ -106,8 +129,13 @@ It uses an installed browser and accepts an optional installed REA entrypoint.
The complete `verify:browser` lane also exercises passive script export through
both public adapters. See [website script export](website-script-export.md).
The [test suite audit](test-suite-audit.md) records the pruning decisions,
replacement evidence and remaining priorities.
`verify:browser:modules` compares CLI and stdio MCP traces against an independent
real Chromium module-loading fixture: import-map scopes, package prefixes,
null/backtracking rejection, query/fragment identities, repeated module instances,
lazy and computed unknowns, exact source/map identities and no implicit refetch.
Set `REA_BROWSER_EXECUTABLE`; the optional script argument selects an installed
package entrypoint. The complete `verify:browser` lane and existing conditional
Chrome CI include this verifier. See [module relationships](website-module-trace.md).
## Real-toolchain verification lanes
@@ -118,6 +146,11 @@ commands. Check prerequisites before starting expensive work and name the
missing command, target, and lane in any failure message. A lane must not imply
that a host or target is covered when it was skipped.
Provider admission accepts Ghidra 12.1.x and the JDK range declared by that
installation (`application.java.min` through `application.java.max`). Current
12.1 releases require JDK 21 or newer and set no maximum. The lanes below still
prove behavior on the verified Ghidra 12.1.4 and JDK 21 build.
| Ghidra lane | Supported runner/target | Additional local tools |
| ------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `npm run verify:ghidra` | Linux x64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
@@ -163,12 +196,21 @@ requires unresolved table bounds or case mappings to remain visible as
residual unknowns.
`npm run verify:inspector` requires the supported Node.js runtime and installed
REA dependencies. CI runs it on Linux and Windows. It starts owned loopback
REA dependencies. CI runs it on Linux and macOS x64/arm64 and Windows x64. It starts owned loopback
Node Inspector fixtures and verifies discovery and passive observation through
the CLI and stdio MCP, including special filenames, unresolved discovery
locations, and independently resolved loaded scripts. Double-quote filenames
are tested on POSIX only because Windows does not support them.
Inspector adapters live in `src/inspector/`, with loopback fixtures under
`tests/fixtures/inspector/` and forked producer boundaries under
`tests/boundary/inspector/`. The real lane is owned by
`scripts/verify/inspector/runtime-observation.mjs` and
`tests/conformance/inspector/`. Browser and Inspector deliberately share the
existing browser CDP transport/value and file-location helpers. Node fixture
success proves this Inspector workflow on the tested host; it does not prove
Electron GUI behavior or another engine.
## Android APK analysis
`npm run verify:android` requires an existing Java 17+ and an explicit
@@ -179,6 +221,16 @@ kept under ignored `_reference/`. No Gradle build, Android SDK, emulator or
application execution is required. The lane compares real CLI/MCP package,
class search, class inventory, method decompilation and incoming references.
See [Android analysis](android-analysis.md) for boundaries and resource budgets.
Authenticated IPA and macOS application inventory projection is documented in
[Apple application analysis](apple-application-analysis.md).
The lane and owned-process cancellation helper live in `scripts/verify/android/`.
The explicit downloader and fixed manifest live in `scripts/fixtures/android/`;
their default remains repository-root `_reference/apk-integration/`. Producer
fixtures live in `tests/fixtures/android/`, boundary cases in
`tests/boundary/android/`, and inventory projection composition cases in
`tests/composition/android/`. Real engine success and synthetic protocol success
are separate proof levels.
Synthetic producer regressions run independently:
@@ -277,6 +329,35 @@ Keep the provider-specific acceptance path independent from optional
cross-compilers. Cross-format failures belong to the cross-format lane and must
not make native host acceptance unavailable.
## Native platform baseline in CI
CI exercises the pinned Node.js runtime on native hosted runners:
| Host | Runner | Baseline checks |
| --------------------- | ------------------ | ---------------------------------------------------------------------------------------- |
| Linux x64 | `ubuntu-latest` | Installed package and real Node Inspector CLI/MCP |
| Linux arm64 (aarch64) | `ubuntu-24.04-arm` | Installed package and real Node Inspector CLI/MCP |
| macOS 15 arm64 | `macos-15` | Installed package and real Node Inspector CLI/MCP |
| macOS 15 x64 | `macos-15-intel` | Installed package and real Node Inspector CLI/MCP |
| Windows x64 | `windows-latest` | Curated capabilities, native controls, installed package and real Node Inspector CLI/MCP |
Package and Inspector matrices assert the actual Node platform/architecture
before verification and record those values with the Node version. Each matrix
runs at most two jobs concurrently with explicit timeouts and Node heap/thread
limits. Package checks cover installation, CLI/MCP discovery, target-free
analysis, configuration backups/recovery, Evidence and owned lifecycle; Inspector
checks execute source-owned loopback targets and special filename cases.
macOS uses one OS version with one native baseline job per architecture. Each job
runs package and Inspector checks after a single dependency installation. The
separate Inspector matrix covers Linux and Windows; Apple artifact checks retain
their own macOS 15 arm64 job for Xcode-dependent workflows.
These native baseline checks complement the Linux source-test shards and the
separate Apple-artifact and real-provider lanes. Actual Hopper, Ghidra, IDA,
browser and managed-tool claims require their corresponding verification lanes.
Runner labels follow the [GitHub hosted-runner reference](https://docs.github.com/en/actions/reference/runners/github-hosted-runners).
## Developer commands
Use source feedback while editing, explicit boundary checks for the changed
@@ -382,26 +463,77 @@ that explicit lane, not the routine iteration requirement.
## Apple native metadata and UI
`npm run verify:apple-dispatch` compiles Objective-C class/protocol and Swift
conformance/vtable fixtures, inspects their bytes and repeats after stripping
local symbols. It requires macOS and the host Xcode toolchain; targets are not
executed. `npm run verify:native-ui` launches exactly one source-owned fixture
window and requires successful selected-window capture and selected actions.
An OS permission denial fails the positive lane. `npm run verify:native-ui:permissions`
allows a host-permission-boundary-only result and explicitly reports
`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:apple-dispatch` compiles the Objective-C fixture (classes,
protocols, a property and an `NSString` category) and the Swift
conformance/vtable fixture.
- Each fixture is linked with legacy `LC_DYLD_INFO` binds and with chained
fixups; on Apple silicon the ObjC fixture is also built as arm64e, which uses
authenticated pointers.
- The lane inspects the bytes and repeats after stripping local symbols.
- It requires the bound `NSObject` superclass, the external category, and the
matching `pointer_fixups` coverage. It requires macOS and the host Xcode toolchain; targets are not
executed. `npm run verify:native-ui` launches exactly one source-owned fixture
window and requires successful selected-window capture and selected actions.
An OS permission denial fails the positive lane. `npm run verify:native-ui:permissions`
allows a host-permission-boundary-only result and explicitly reports
`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.
### Firmware adapters
`npm run fixtures:firmware` uses existing Python 3 and a host C compiler to make
an ignored gzip/USTAR firmware fixture and independent offset/hash oracle.
`npm run verify:firmware` requires caller-supplied Binwalk 3.1.0, Unblob 26.6.4
and util-linux prlimit on Linux. It verifies CLI/MCP parity, selected ranges,
and util-linux prlimit on Linux. The provider also accepts other 3.1.x and
26.6.x builds and reports them as unverified; this lane proves the audited
releases. It verifies CLI/MCP parity, selected ranges,
unknown chunks, depth limits and extracted child digests. The optional
`REA_FIRMWARE_VERIFY_EXT4=1` lane requires existing mke2fs/debugfs; the separate
`REA_FIRMWARE_VERIFY_GHIDRA=1` lane checks a selected host ELF through real Ghidra.
Neither optional toolchain is a base-lane prerequisite. See
[firmware analysis](firmware-analysis.md) for limits and unverified formats.
The real entrypoint is `scripts/verify/firmware/analysis.mjs`; the fixture runner
and unchanged Python producer belong together in `scripts/fixtures/firmware/`.
Producer fixtures and boundaries remain in `tests/fixtures/firmware/` and
`tests/boundary/firmware/`. Source-fixture generation proves the runner and
independent oracle; actual Binwalk/Unblob CLI/MCP proof requires selected tools.
### JavaScript source recovery
Build once with `npm run build:cached`, then run `npm run verify:javascript:recovery`.
This focused lane requires Linux x64, util-linux `prlimit`,
`REA_WAKARU_COMMAND` pointing to the official Wakaru 1.13.0 Linux x64 binary,
and `REA_JAVASCRIPT_FIXTURE_TOOLS` pointing to an isolated npm prefix containing
esbuild 0.25.10 and webpack 5.101.3. No global installation is required.
The lane compiles source-owned fixtures, exercises CLI and stdio MCP, verifies
published bytes and provenance, feeds recovered modules into existing analysis,
and compares a finite set of known fixture results. It does not establish
arbitrary recovered-application equivalence. CI installs these prerequisites only
in `.github/workflows/real-javascript-recovery.yml`; the existing `real-browser`
lane uses real Chrome for browser capture and website workflows.
### Captured website source-map lane
`npm run verify:browser:source-maps` checks actual Chromium capture/export and
source-map point tracing through CLI and stdio MCP. It requires absolute
`REA_BROWSER_EXECUTABLE` and `REA_WEB_SOURCE_MAP_COMPILER` pointing to esbuild
0.25.10's `lib/main.js` in a caller-owned isolated installation. Preflight reports
missing prerequisites for this lane. The compiler is used only to generate the
source-owned fixture. No Hopper, Ghidra or application dependency installation
is required.
```bash
REA_BROWSER_EXECUTABLE=/absolute/path/to/chromium \
REA_WEB_SOURCE_MAP_COMPILER=/absolute/path/to/fixture-tools/node_modules/esbuild/lib/main.js \
npm run verify:browser:source-maps
```
`scripts/verify-browser-source-maps.mjs /absolute/path/to/installed/rea.mjs`
checks an installed package after building the verifier dependencies. The separate
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.
+2 -2
View File
@@ -56,7 +56,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
46,src/application/Uninstall.ts:removeManagedPath symlink,Managed cache/state/skill path is a symbolic link,Refused to follow symlink {path}.,Exposes local path and does not explain manual safety check,"REA did not remove this item because its managed path is a symbolic link. Verify the link target before removing it manually.",implemented,focused assertion passed
47,src/application/Uninstall.ts:removeManagedPath failure,Managed item removal fails,{path} could not be removed.,Exposes local path and gives no recovery,"This item could not be removed. Check file permissions, then rerun uninstall.",implemented,verified by injected managed-path removal failure test
48,src/application/DoctorDiagnostics.ts:node,Runtime version is outside supported ranges,"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact dependency recovery,Keep current copy,unchanged,verified by doctor unit test
49,src/application/Doctor.ts:host,Host OS/distribution is unsupported,"REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.",Low risk; clearly states supported choices,Keep current copy,unchanged,verified by doctor unit tests
49,src/application/Doctor.ts:host,Host OS/distribution is unsupported,"REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and CachyOS.",Low risk; clearly states supported choices,Keep current copy,unchanged,verified by doctor unit tests
50,src/application/Doctor.ts:analysis engine,Optional deep-analysis executable cannot be found,"Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.",Low risk in setup-specific diagnostic; exact recovery paths,Keep current copy,unchanged,verified by doctor unit tests
51,src/application/Doctor.ts:target,Optional target path fails shared validation,Supply a readable local app or program path.,Low risk; tells caller valid target class,Keep current copy,unchanged,verified by doctor unit test
52,src/application/Setup.ts:approval,Setup plan lacks approval,"Review the setup plan, then rerun interactively or with --yes.",Low risk; exact authorization recovery,Keep current copy,unchanged,verified by setup unit tests
@@ -67,7 +67,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
57,src/application/Setup.ts:unsupported platform,Platform is neither macOS nor Linux,REA supports Hopper on macOS and selected 64-bit Linux distributions.,Low risk; states support boundary,Keep current copy,unchanged,verified by setup unit test
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+, and 64-bit Arch Linux.",Low risk; exact supported alternatives,Keep current copy,unchanged,verified by setup unit test
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
1 harm_rank location trigger current_copy user_risk proposed_replacement implementation_status verification_result
56 46 src/application/Uninstall.ts:removeManagedPath symlink Managed cache/state/skill path is a symbolic link Refused to follow symlink {path}. Exposes local path and does not explain manual safety check REA did not remove this item because its managed path is a symbolic link. Verify the link target before removing it manually. implemented focused assertion passed
57 47 src/application/Uninstall.ts:removeManagedPath failure Managed item removal fails {path} could not be removed. Exposes local path and gives no recovery This item could not be removed. Check file permissions, then rerun uninstall. implemented verified by injected managed-path removal failure test
58 48 src/application/DoctorDiagnostics.ts:node Runtime version is outside supported ranges Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Low risk; exact dependency recovery Keep current copy unchanged verified by doctor unit test
59 49 src/application/Doctor.ts:host Host OS/distribution is unsupported REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux. REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and CachyOS. Low risk; clearly states supported choices Keep current copy unchanged verified by doctor unit tests
60 50 src/application/Doctor.ts:analysis engine Optional deep-analysis executable cannot be found Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH. Low risk in setup-specific diagnostic; exact recovery paths Keep current copy unchanged verified by doctor unit tests
61 51 src/application/Doctor.ts:target Optional target path fails shared validation Supply a readable local app or program path. Low risk; tells caller valid target class Keep current copy unchanged verified by doctor unit test
62 52 src/application/Setup.ts:approval Setup plan lacks approval Review the setup plan, then rerun interactively or with --yes. Low risk; exact authorization recovery Keep current copy unchanged verified by setup unit tests
67 57 src/application/Setup.ts:unsupported platform Platform is neither macOS nor Linux REA supports Hopper on macOS and selected 64-bit Linux distributions. Low risk; states support boundary Keep current copy unchanged verified by setup unit test
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+, and 64-bit Arch Linux. 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 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 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 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
@@ -1,5 +1,5 @@
{
"manifest_id": "ecm_d68a1575bdaa1a044ccce8e27c5dd335612f909bc81c52ce3089b576fd772940",
"manifest_id": "ecm_e7e60d5d08bfe335c4c0b38ab4598eb634444b835202e325241749d152808c94",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "d2d971cc67bb0f7b80b4e39382a6bd1146445ed1dbf52043f89e2da2985c97ba"
"sha256": "265556bd52f52ad829187fe71831cf05329248286c86e3102f0cdbf8841e1d6d"
}
],
"claims": [
+97
View File
@@ -0,0 +1,97 @@
# Captured website source locations
`trace_web_source_location` maps one selected position in a script retained by
`export_web_scripts` through one explicitly paired local source map. The shared
CLI/MCP workflow verifies actual manifest, script and map byte identities and
returns the original position and embedded source inline. It acquires no browser,
fetches no URL and executes no application code.
```bash
rea trace-web-source-location \
/analysis/scripts/manifest.json 0 \
/analysis/app.js.map 'https://example.test/maps/app.js.map?v=7#context' \
1 25 --json
```
The six positional arguments select the manifest, zero-based script index,
absolute map path, absolute map URL context, one-based generated line and
zero-based UTF-16 column. The MCP arguments are:
```json
{
"manifest_path": "/analysis/scripts/manifest.json",
"script_index": 0,
"source_map": {
"path": "/analysis/app.js.map",
"url": "https://example.test/maps/app.js.map?v=7#context"
},
"generated_position": { "line": 1, "column": 25 }
}
```
The URL supplies the context for resolving `sources` and `sourceRoot`; it is not
an instruction to fetch. Absolute virtual or file contexts can be selected as
well. The map/script association is caller-selected: a declaration or local map
does not prove deployment authenticity or execution. Use
[import tracing](website-module-trace.md) to inspect outgoing native module edges.
## Meaning of the evidence
Lookup follows the greatest generated position less than or equal to the
selected point, across lines, and returns **every** segment at that position.
It includes generated-only mappings. This follows
[ECMA-426 GetOriginalPositions](https://tc39.es/ecma426/#sec-getoriginalpositions),
with one-based lines in REA's public contract. A point result does not establish
coverage of a source range, execution or UI causality.
Mapped entries preserve the raw source name/root, section path, section-local
source index and flattened index. Duplicate URLs remain distinct, including
entries with different embedded contents. Missing source names retain a null
resolved URL. Original coordinates outside embedded text retain the reported
coordinates and a null offset.
Embedded content hashes identify the UTF-8 encoding of the decoded
`sourcesContent` string, including its line endings. They do not identify an
independently captured original file. Raw map text is retained in Evidence's
`raw_result`, including unknown extensions; these extensions are not interpreted.
Capture context and source Evidence links accompany independently verified
selected file identities.
## Adapter and resource boundaries
The adapter uses the locked upstream
[`@jridgewell/trace-mapping` 0.3.31](https://github.com/jridgewell/trace-mapping)
without modifying its code. It validates map/VLQ structure before allocation,
normalizes nested indexed offsets before upstream flattening, checks section
boundaries and indexes, and retains raw declaration identity. It supports regular
maps and inline indexed sections. External section URLs return an actionable
unsupported-profile error. This is a defined decoder profile, not a claim to
implement every current source-map extension.
One owned Node subprocess decodes the map. Its private request directory is
removed before success is returned. Only this child's inherited `NODE_OPTIONS`
is removed; explicit V8 flags set a 192 MiB old-generation and 8 MiB semi-space
budget, one V8 worker and a 20-second deadline. Reported actual V8 heap limits
are checked. These are heap limits, not an aggregate process RSS limit. Cleanup
may extend the deadline and an unconfirmed cleanup overrides success with
`cleanup_incomplete` and the owning operation/resources.
Complete evidence is bounded by a 4 MiB map, 32 MiB codec reply, 262144 decoded
rows/segments and 64 nested section levels. Exceeding a budget returns an explicit
failure, without partial results. The existing 32 MiB manifest and 16 MiB selected
script boundaries also apply. No global toolchain or browser configuration changes
are made.
## Verification
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
public CLI behavior. The actual codec process is exercised on the test host.
The optional `verify:browser:source-maps` lane compiles a source-owned TypeScript
fixture with isolated esbuild 0.25.10, serves it to real Chromium, captures and
exports the actual response, then checks the original compiler point and exact
retained content through CLI and stdio MCP. It can target an installed package.
Only this lane requires Chromium and the fixture compiler. Linux x64 is the real
workflow covered by the dedicated CI job; other host workflows remain unverified.
+85
View File
@@ -0,0 +1,85 @@
# Captured website module relationships
`trace_web_module_imports` / `rea trace-web-module-imports` traces one script's
outgoing native ES imports and re-exports from an
[exported capture](website-script-export.md). It verifies the selected source's
SHA-256 and byte count, preserves browser URL identities, and returns every
matching capture candidate inline. It does not refetch website assets.
## Select the source and context
Provide a caller-supplied Chromium executable supporting `import.meta.resolve`.
REA does not install or upgrade a browser. Real verification covers Linux x64;
other hosts have not received this feature's real workflow verification.
```bash
REA_BROWSER_EXECUTABLE=/absolute/path/to/chrome \
rea trace-web-module-imports /analysis/export/manifest.json 0 \
--importer-url 'https://example.test/app/main.js?build=2#entry' \
--import-map-path /analysis/import-map.json \
--import-map-base-url https://example.test/maps/ \
--json
```
The MCP input expresses the same choices:
```json
{
"manifest_path": "/analysis/export/manifest.json",
"script_index": 0,
"importer_url": "https://example.test/app/main.js?build=2#entry",
"import_map": {
"path": "/analysis/import-map.json",
"base_url": "https://example.test/maps/"
}
}
```
`script_index` is zero-based in `manifest.scripts`. If `importer_url` is omitted,
the trace selects the reported source URL. An inline script's document base
cannot be established from that URL alone; choose the context explicitly when
known. `import_map` is optional and reads local JSON only. Its exact bytes and
base URL are retained; selecting it does not prove the map was installed in the
captured page. HTTP(S) importer contexts without URL userinfo are supported initially.
## Interpret the evidence
- Static imports, re-exports and literal dynamic imports receive the native
engine's resolved URL or exact rejection message. Empty strings are retained.
- Computed dynamic imports retain their expression and source positions with
`computed-specifier` unknown resolution. Erased type-only imports/exports and
inferred bundler IDs are excluded from native import claims.
- Locations use UTF-16 offsets and columns, with one-based lines. Parse recovery
diagnostics and unparseable input are explicit.
- Query variants and fragments remain distinct. An exact reported URL is an
`exact-reported-url` candidate. A response URL stripped of its fragment is a
weaker `response-url-without-fragment` content candidate. Multiple captured
versions are all returned; the trace selects none as canonical.
- Candidate metadata remains reported capture evidence; only the selected
source's bytes are reverified. Missing/lazy targets can resolve successfully
with no captured candidate. Every relationship reports execution as unknown.
- Source-map declarations remain declarations; this operation does not read or
invent map bytes, original-source mappings, worker ownership or UI causality.
The selected source is loaded from `files/` under the manifest's current
directory. Relocation retains the manifest's originally reported paths and the
actual current source path separately. Unsafe paths, symlink source directories,
nonregular files, wrong digests/sizes and malformed UTF-8 fail explicitly.
## Engine and lifecycle
An owned temporary browser context runs REA's trusted resolver stub. The selected
application is parsed as data and is never executed by this trace. Page requests
are fulfilled with REA's synthetic document/stub or blocked; no website asset is
downloaded. This does not establish an OS sandbox or browser-wide network
containment. The engine version and native report are retained in Evidence.
Sources without literal imports do not acquire a browser. The manifest, source
and import map have 32 MiB, 16 MiB and 4 MiB input limits. Native resolution has
one 20-second deadline; cleanup may extend that deadline. One renderer is
requested, and there is no aggregate host memory-limit claim. Cancellation
closes only the owned browser; cleanup failure cannot return a successful trace.
`REA_BROWSER_NO_SANDBOX=true` passes Chromium's corresponding launch option when
the host requires it; it does not enable an REA security boundary.
See [testing](testing.md) for the native loading oracle and CLI/MCP real lane.
+3 -3
View File
@@ -13,8 +13,8 @@ additional permission flag or degraded mode is required.
- Windows 10+ x64 with fixed local NTFS target and temporary volumes;
- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+;
- an operator-installed official Ghidra 12.1.4 distribution;
- a 64-bit full JDK 21;
- 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
- the 25 read-only Ghidra inventory, memory, and function-analysis operations.
@@ -148,7 +148,7 @@ private runtime files; crash-time transactional deletion is not claimed.
## Verification
See [native controls](../native/windows/README.md) for the separate artifact
See [native controls](https://github.com/morluto/rea/blob/main/native/windows/README.md) for the separate artifact
build lane and native boundary checks. Development tarballs without the built
artifact report native controls unavailable; publishing requires the verified
artifact and manifest.
+5 -1
View File
@@ -2,7 +2,11 @@
"$schema": "./node_modules/knip/schema.json",
"entry": ["src/main.ts", "src/cli.ts", "src/contracts/errorSchemas.ts"],
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
"ignoreDependencies": ["electron"],
"vitepress": {
"entry": ["docs/.vitepress/config.ts"]
},
"ignoreBinaries": ["mkfifo"],
"ignoreDependencies": ["electron", "esbuild", "webpack"],
"ignoreExportsUsedInFile": true,
"rules": {
"files": "error",
+2579 -22
View File
File diff suppressed because it is too large Load Diff
+29 -13
View File
@@ -42,10 +42,12 @@
"dist",
"install.sh",
"bridge/hopper_bridge.py",
"bridge/native/NativeUIChildren.swift",
"bridge/native/ReaNativeUI.swift",
"bridge/ghidra/ReaGhidraBridge.java",
"bridge/ghidra/ReaGhidraExtensions.java",
"bridge/ghidra/ReaGhidraPrepareCom.java",
"bridge/android/ReaJadxBridge.java",
"scripts/rea.mjs",
"scripts/electron-active-hook.cjs",
"scripts/electron-active-hook-boundaries.cjs",
@@ -84,8 +86,8 @@
"build:cached": "turbo run build",
"format": "oxfmt .",
"format:check": "oxfmt --check .",
"lint": "oxlint",
"lint:fix": "oxlint --fix",
"lint": "oxlint && npm run verify:module-boundaries",
"lint:fix": "oxlint --fix && npm run verify:module-boundaries",
"knip": "knip",
"jscpd": "jscpd src/ tests/",
"scan:todos": "node scripts/scan-todos.mjs",
@@ -122,6 +124,7 @@
"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-keyed-archive.mjs",
"verify:macos-bundle": "npm run build:cached && node scripts/verify-macos-bundle.mjs",
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
"verify:ida": "npm run build:cached && node scripts/verify-real-ida.mjs",
"verify:ghidra:switch": "npm run build:cached && node scripts/verify-real-ghidra-switch.mjs",
@@ -135,15 +138,15 @@
"verify:browser:dom": "node scripts/verify-browser-dom-destinations.mjs",
"verify:browser:network": "npm run build:cached && node scripts/verify-browser-network.mjs",
"verify:electron": "npm run build:cached && node scripts/verify-real-electron.mjs",
"verify:inspector": "npm run build:cached && node scripts/verify-real-javascript-runtime-observation.mjs",
"verify:javascript-runtime": "npm run build:cached && node scripts/verify-local-javascript-runtime.mjs",
"verify:javascript:digests": "npm run build:cached && node scripts/verify-large-javascript-digests.mjs",
"verify:application-workflows": "npm run build:cached && node scripts/verify-local-application-workflows.mjs",
"verify:managed": "npm run build:cached && node scripts/verify-managed-conformance.mjs",
"verify:firmware": "npm run build:cached && node scripts/verify-real-firmware.mjs",
"fixtures:firmware": "node scripts/generate-firmware-fixtures.mjs",
"verify:android": "npm run build:cached && node scripts/verify-real-android.mjs",
"fixtures:android": "node scripts/fetch-android-fixtures.mjs",
"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: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",
"fixtures:firmware": "node scripts/fixtures/firmware/generate.mjs",
"verify:android": "npm run build:cached && node scripts/verify/android/analysis.mjs",
"fixtures:android": "node scripts/fixtures/android/fetch.mjs",
"verify:agent": "npm run build:cached && node scripts/verify-agent-experience.mjs",
"verify:hopper:linux": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-linux.mjs",
"verify:setup:linux": "npm run build:cached && node scripts/verify-linux-setup-e2e.mjs",
@@ -158,6 +161,9 @@
"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",
"docs:dev": "vitepress dev docs",
"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:check": "node scripts/run-exclusive.mjs docs turbo run docs:check:built",
@@ -177,7 +183,11 @@
"build:fixtures:nativeaot": "node scripts/generate-nativeaot-fixtures.mjs",
"verify:ghidra:nativeaot": "npm run build:cached && node scripts/verify-real-ghidra-nativeaot.mjs",
"verify:browser:scripts": "npm run build:cached && node scripts/verify-browser-scripts.mjs",
"verify:test-discovery": "node scripts/verify-test-discovery.mjs"
"verify:browser:modules": "npm run build:cached && node scripts/verify-browser-modules.mjs",
"verify:test-discovery": "node scripts/verify-test-discovery.mjs",
"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"
},
"lint-staged": {
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
@@ -209,11 +219,16 @@
"playwright-core": "1.58.2",
"plist": "5.0.0",
"semver": "7.8.5",
"smol-toml": "1.7.0",
"smol-toml": "1.9.0",
"write-file-atomic": "7.0.0",
"ws": "8.21.0",
"zod": "4.4.3"
},
"overrides": {
"vitepress": {
"vite": "6.4.4"
}
},
"devDependencies": {
"@fast-check/vitest": "0.5.0",
"@types/cross-spawn": "6.0.6",
@@ -231,6 +246,7 @@
"oxlint": "1.87.0",
"turbo": "2.11.7",
"typescript": "7.0.2",
"vitepress": "1.6.4",
"vitest": "5.0.3",
"yaml": "2.9.0"
}
+45 -7
View File
@@ -20,14 +20,17 @@ const source = resolve(
const install = process.env.GHIDRA_INSTALL_DIR;
if (!install)
throw new Error(
"NativeAOT build lane requires existing GHIDRA_INSTALL_DIR (Ghidra 12.1.4).",
"NativeAOT build lane requires existing GHIDRA_INSTALL_DIR (Ghidra 12.1.x).",
);
const properties = await readFile(
join(install, "Ghidra/application.properties"),
"utf8",
);
if (!/^application.version=12\.1\.4\s*$/m.test(properties))
throw new Error("NativeAOT build lane requires Ghidra 12.1.4.");
const ghidraVersion = propertyValue(properties, "application.version");
if (!ghidraReleaseLineAccepted(ghidraVersion))
throw new Error(
`NativeAOT build lane accepts Ghidra 12.1.x (verified with 12.1.4). Observed application.version=${ghidraVersion ?? "missing"}.`,
);
const output = resolve(
process.env.REA_NATIVEAOT_BUILD_ROOT ??
join(root, "_reference/nativeaot-integration/extension"),
@@ -47,9 +50,11 @@ const run = (command, args) => {
return result.stdout.trim();
};
const javac = java ? join(java, "javac") : "javac";
if (!/^javac 21(?:\.|$)/u.test(run(javac, ["-version"])))
const javacBanner = capture(javac, ["-version"]);
const javacMajor = Number(/^javac (\d+)/mu.exec(javacBanner)?.[1]);
if (!Number.isInteger(javacMajor) || javacMajor < 21)
throw new Error(
"NativeAOT build lane requires an existing JDK 21 javac; REA does not install or upgrade Java.",
`NativeAOT build lane requires javac 21 or newer and compiles with --release 21. Observed: ${javacBanner || "no version"}. REA does not install or upgrade Java.`,
);
const revision = run("git", ["-C", source, "rev-parse", "HEAD"]);
if (
@@ -93,6 +98,8 @@ try {
run(javac, [
"-J-Xmx512m",
"-J-XX:ActiveProcessorCount=1",
"--release",
"21",
`@${join(output, "javac.args")}`,
]);
await writeFile(
@@ -119,7 +126,7 @@ try {
jar,
sha256,
upstream_revision: revision,
ghidra_version: "12.1.4",
ghidra_version: ghidraVersion,
integration_api: 1,
},
null,
@@ -131,10 +138,41 @@ try {
jar,
sha256,
upstream_revision: revision,
ghidra_version: "12.1.4",
ghidra_version: ghidraVersion,
integration_api: 1,
}),
);
} finally {
await rm(classes, { recursive: true, force: true });
}
/** Read one application.properties value. Empty and missing values stay unknown. */
function propertyValue(content, name) {
for (const line of content.split(/\r?\n/u)) {
const separator = line.indexOf("=");
if (separator < 0 || line.slice(0, separator).trim() !== name) continue;
const value = line.slice(separator + 1).trim();
return value.length === 0 ? null : value;
}
return null;
}
/** Same 12.1 line as GhidraInstallationPolicy, including a distro suffix. */
function ghidraReleaseLineAccepted(raw) {
const match = /^(\d+)\.(\d+)(?:\.(\d+))?([-+].*)?$/u.exec(raw ?? "");
return match !== null && Number(match[1]) === 12 && Number(match[2]) === 1;
}
function capture(command, args) {
const result = spawnSync(command, args, {
cwd: root,
encoding: "utf8",
timeout: 120_000,
maxBuffer: 4 * 1024 * 1024,
});
if (result.error || result.status !== 0)
throw new Error(
`NativeAOT build prerequisite/action ${command} failed: ${result.error?.message ?? ""}\n${result.stdout ?? ""}${result.stderr ?? ""}`,
);
return `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
}
@@ -3,15 +3,17 @@ import { createReadStream } from "node:fs";
import { link, mkdir, open, rm } from "node:fs/promises";
import { resolve, join } from "node:path";
import { fileURLToPath } from "node:url";
import { JADX_RELEASE } from "../src/android/JadxRelease.ts";
import fixture from "./fixtures/android-apidemos.json" with { type: "json" };
import { JADX_RELEASE } from "../../../src/android/JadxRelease.ts";
import fixture from "./apidemos.json" with { type: "json" };
const args = process.argv.slice(2);
if (args.length > 1)
throw new Error("Usage: node scripts/fetch-android-fixtures.mjs [DIRECTORY]");
throw new Error("Usage: npm run fixtures:android -- [DIRECTORY]");
const directory = resolve(
args[0] ??
fileURLToPath(new URL("../_reference/apk-integration/", import.meta.url)),
fileURLToPath(
new URL("../../../_reference/apk-integration/", import.meta.url),
),
);
await mkdir(directory, { recursive: true });
const hashFile = async (path) => {
+61
View File
@@ -0,0 +1,61 @@
import { createServer } from "node:http";
/** Source-owned module graph with actual native loading and an uncalled lazy edge. */
export async function startBrowserModuleSite() {
const requests = [];
const importMap = {
imports: { "pkg/": "/pkg/", alias: "./global.js", blocked: null },
scopes: { "/scoped/": { alias: "./scoped.js" } },
};
const main = `
import {marker as alias} from 'alias';
import {marker as pkg} from 'pkg/nested.js';
const a = await import('/query.js?v=1#one');
const b = await import('/query.js?v=1#two');
const c = await import('/query.js?v=2');
const again = await import('/query.js?v=1#one');
const specifiers = ['alias', 'pkg/nested.js', '/query.js?v=1#one', '/query.js?v=1#two', '/query.js?v=2', '/query.js?v=1#one', './lazy.js', 'blocked', 'pkg/../escape.js', 'pkg/%2e%2e/escape.js', ''];
globalThis.__moduleProof = {alias, pkg, urls: [a.url,b.url,c.url], instances: [a.instance,b.instance,c.instance], same_module: again === a, different_fragments: a !== b, resolutions: specifiers.map(specifier => {try{return {state: 'resolved', url: import.meta.resolve(specifier)}}catch(error){return {state: 'rejected', message: String(error)}}})};
document.body.dataset.ready = 'true';
function uncalled(){return import('./lazy.js')}
function blocked(){return import('blocked')}
function escaped(){return import('pkg/../escape.js')}
function encodedEscape(){return import('pkg/%2e%2e/escape.js')}
function empty(){return import('')}
function computed(name){return import(name)}
`;
const server = createServer((request, response) => {
requests.push(request.url);
if (request.url === "/")
return response
.writeHead(200, { "Content-Type": "text/html" })
.end(
`<body><base href="/maps/"><script type="importmap">${JSON.stringify(importMap)}</script><script type="module" src="/scoped/main.js?build=2#entry"></script></body>`,
);
response.setHeader("Content-Type", "text/javascript");
if (request.url?.startsWith("/scoped/main.js?")) return response.end(main);
if (request.url === "/maps/scoped.js")
return response.end("export const marker = 'scoped';");
if (request.url === "/pkg/nested.js")
return response.end("export const marker = 'pkg';");
if (request.url?.startsWith("/query.js?"))
return response.end(
"globalThis.__moduleCount = (globalThis.__moduleCount ?? 0) + 1; export const instance = globalThis.__moduleCount; export const url = import.meta.url;",
);
response.writeHead(404).end("Not selected");
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
if (address === null || typeof address === "string")
throw new Error("Module fixture has no TCP address");
return {
origin: `http://127.0.0.1:${address.port}`,
requests,
importMap,
main,
close: () =>
new Promise((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
),
};
}
@@ -0,0 +1,77 @@
import { createServer } from "node:http";
import { pathToFileURL } from "node:url";
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() {
const command = process.env.REA_WEB_SOURCE_MAP_COMPILER;
if (!command || !isAbsolute(command))
throw new Error(
"verify:browser:source-maps requires absolute REA_WEB_SOURCE_MAP_COMPILER pointing to isolated esbuild 0.25.10 lib/main.js",
);
await access(command);
const compiler = await import(pathToFileURL(command).href);
if (compiler.version !== "0.25.10")
throw new Error(`Expected esbuild 0.25.10; observed ${compiler.version}`);
const original =
'// 🎋 UTF-16 and CRLF fixture\r\nfunction boot(): void {\r\n const marker: string = "source-map proof";\r\n document.body.dataset.ready = marker;\r\n}\r\nboot();\r\n';
const compiled = await compiler
.transform(original, {
loader: "ts",
sourcefile: "../src/fixture.ts?v=7#original",
sourcemap: "external",
minify: true,
format: "iife",
target: "es2022",
})
.finally(() => compiler.stop());
const token = '"source-map proof"';
const generatedOffset = compiled.code.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 originalPosition = textPosition(original, originalOffset);
let mapRequests = 0;
const server = createServer((request, response) => {
if (request.url === "/")
return response
.writeHead(200, { "Content-Type": "text/html" })
.end('<body><script src="/app.js?build=7"></script></body>');
if (request.url === "/app.js?build=7")
return response
.writeHead(200, { "Content-Type": "text/javascript" })
.end(compiled.code);
if (request.url?.startsWith("/maps/app.js.map")) {
mapRequests += 1;
return response
.writeHead(200, { "Content-Type": "application/json" })
.end(compiled.map);
}
response.writeHead(404).end("Unselected");
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
if (!address || typeof address === "string")
throw new Error("Source map fixture has no TCP address");
return {
origin: `http://127.0.0.1:${address.port}`,
original,
generated: compiled.code,
map: compiled.map,
position,
originalPosition,
originalOffset,
mapRequests: () => mapRequests,
compiler: { id: "esbuild", version: compiler.version },
close: () =>
new Promise((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
),
};
}
const textPosition = (text, offset) => {
const preceding = text.slice(0, offset).split(/\r\n?|\n|\u2028|\u2029/u);
return { line: preceding.length, column: preceding.at(-1).length };
};
@@ -34,7 +34,7 @@ const ext4 =
: [];
for (const command of ext4) await exec(command, ["-V"]);
const generated = await exec(process.env.REA_FIRMWARE_PYTHON ?? "python3", [
fileURLToPath(new URL("./lib/generate-firmware-fixture.py", import.meta.url)),
fileURLToPath(new URL("./generate.py", import.meta.url)),
root,
process.env.REA_FIRMWARE_CC ?? "cc",
...ext4,
@@ -0,0 +1,100 @@
import { createRequire } from "node:module";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
/** Compile source-owned fixtures using explicitly supplied, pinned toolchains. */
export async function buildRecoveryFixtures(root, prefix) {
if (!prefix)
throw new Error(
"verify:javascript:recovery requires REA_JAVASCRIPT_FIXTURE_TOOLS pointing to the isolated esbuild/webpack installation",
);
const require = createRequire(join(prefix, "package.json"));
const esbuild = require("esbuild");
const webpack = require("webpack");
const webpackVersion = require("webpack/package.json").version;
if (esbuild.version !== "0.25.10" || webpackVersion !== "5.101.3")
throw new Error(
"Fixture compilers must be esbuild 0.25.10 and webpack 5.101.3",
);
const source = fileURLToPath(new URL("./entry.mjs", import.meta.url));
const webpackOutput = join(root, "webpack");
await mkdir(webpackOutput);
await new Promise((resolve, reject) => {
const compiler = webpack({
mode: "production",
entry: source,
parallelism: 1,
optimization: { concatenateModules: false },
output: { path: webpackOutput, filename: "bundle.js" },
});
compiler.run((error, stats) => {
compiler.close((closeError) => {
if (error || closeError) return reject(error ?? closeError);
if (!stats || stats.hasErrors())
return reject(new Error(stats?.toString() ?? "No webpack stats"));
resolve();
});
});
});
const esbuildPath = join(root, "esbuild.js");
await esbuild.build({
entryPoints: [source],
outfile: esbuildPath,
bundle: true,
format: "iife",
minify: true,
logLevel: "silent",
});
// One self-contained source fixture avoids unresolved imports in the oracle.
const helperSource = await readFile(
fileURLToPath(new URL("./helpers.mjs", import.meta.url)),
"utf8",
);
const plain = await esbuild.transform(
helperSource.replaceAll("export ", "") +
"\nglobalThis.recoveryFixture = {greet, total};",
{ minify: true },
);
const minifiedPath = join(root, "minified.js");
await writeFile(minifiedPath, plain.code);
if (plain.code.length === 0)
throw new Error("Expected a real minifier output");
return {
files: [
{
name: "minified",
path: minifiedPath,
expectedFormat: "unknown",
expectedModules: 1,
},
{
name: "webpack",
path: join(webpackOutput, "bundle.js"),
expectedFormat: "webpack5",
expectedModules: 2,
},
{
name: "esbuild-scope-hoisted",
path: esbuildPath,
expectedFormat: "unknown",
expectedModules: 1,
},
{
name: "heuristic-aggressive",
path: minifiedPath,
expectedFormat: "unknown",
expectedModules: 1,
options: { extraction_mode: "heuristic", rewrite_level: "aggressive" },
},
{
name: "inspection-minimal",
path: join(webpackOutput, "bundle.js"),
expectedFormat: "webpack5",
expectedModules: 2,
options: { extraction_mode: "inspection", rewrite_level: "minimal" },
},
],
toolchains: { esbuild: esbuild.version, webpack: webpackVersion },
};
}
@@ -0,0 +1,3 @@
import { greet, total } from "./helpers.mjs";
globalThis.recoveryFixture = { greet, total };
@@ -0,0 +1,6 @@
export function greet(name) {
return `Hello ${name ?? "world"}`;
}
export function total(items) {
return items.reduce((sum, value) => sum + value, 0);
}
+1 -1
View File
@@ -22,7 +22,7 @@ delete verifierEnvironment.REA_ILSPY_CMD_PATH;
const { stdout } = await exec(
process.execPath,
[join(root, "scripts/verify-managed-conformance.mjs")],
[join(root, "scripts/verify/managed/conformance.mjs")],
{
cwd: root,
env: verifierEnvironment,
+5 -12
View File
@@ -6,10 +6,8 @@ import { Ajv2020 } from "ajv/dist/2020.js";
import { McpServer } from "@modelcontextprotocol/server";
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managedWorkflowToolContracts.js";
import { ArtifactProvider } from "../dist/artifacts/ArtifactProvider.js";
import { ManagedStaticProvider } from "../dist/dotnet/ManagedStaticProvider.js";
import { NativeMacOSProvider } from "../dist/native/NativeMacOSProvider.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managed/managedWorkflowToolContracts.js";
import { auxiliaryAnalysisProviderDeclarations } from "../dist/composition/auxiliaryAnalysisProviders.js";
import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
@@ -88,14 +86,9 @@ async function sdkToolCatalog() {
await Promise.allSettled([client.close(), server.close()]);
}
}
const auxiliaryProviders = [
new ArtifactProvider(CATALOG_PLATFORM),
new NativeMacOSProvider(undefined, CATALOG_PLATFORM),
new ManagedStaticProvider(),
].map((provider) => ({
identity: provider.identity(),
capabilities: provider.capabilities(),
}));
const auxiliaryProviders = auxiliaryAnalysisProviderDeclarations(
CATALOG_PLATFORM,
).map(({ identity, capabilities }) => ({ identity, capabilities }));
const canonicalizeJson = (value) => {
if (Array.isArray(value)) return value.map(canonicalizeJson);
+67 -8
View File
@@ -20,11 +20,14 @@ import time
# Keep this digest aligned with LinuxHopper.ts and rerun real-Hopper verification
# before accepting another vendor build.
SUPPORTED_HOPPER_SHA256 = {
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded"
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded",
"1339f9e58377442b0c6fcb0dfc3cec20d593cc557408521fad9a00dbc6b8da13",
}
EXPECTED_SCREEN = (1280, 1024)
EXPECTED_DIALOG = (189, 370, 901, 284)
DEMO_CLICK = (305, 632)
DIALOG_TITLES = {"Registration", "Registration — Hopper Disassembler"}
HOPPER_WINDOW_TITLE = "Hopper Disassembler"
DEMO_CLICK_LEFT = 116
DEMO_CLICK_BOTTOM = 22
PRIVATE_DISPLAY_UNAVAILABLE = 70
X11_AUTHORIZATION_FAILED = 71
UNSUPPORTED_HOPPER_BUILD = 72
@@ -196,6 +199,47 @@ def windows(x11: ctypes.CDLL, display: int, root: int) -> list[tuple[int, int, i
return found
def window_title(x11: ctypes.CDLL, display: int, window: int) -> str | None:
title = ctypes.c_char_p()
if not x11.XFetchName(display, window, ctypes.byref(title)) or not title.value:
return None
try:
return title.value.decode("utf-8", "replace")
finally:
x11.XFree(ctypes.cast(title, ctypes.c_void_p))
def window_transient_for(
x11: ctypes.CDLL, display: int, window: int
) -> int | None:
parent = ctypes.c_ulong()
if not x11.XGetTransientForHint(display, window, ctypes.byref(parent)):
return None
return int(parent.value)
def find_demo_dialog(
x11: ctypes.CDLL,
display: int,
observed: list[tuple[int, int, int, int, int]],
) -> tuple[int, int, int, int, int] | None:
for candidate in observed:
window = candidate[0]
if window_title(x11, display, window) not in DIALOG_TITLES:
continue
parent = window_transient_for(x11, display, window)
if parent is None:
continue
if window_title(x11, display, parent) == HOPPER_WINDOW_TITLE:
return candidate
return None
def demo_click(dialog: tuple[int, int, int, int, int]) -> tuple[int, int]:
_window, x, y, _width, height = dialog
return x + DEMO_CLICK_LEFT, y + height - DEMO_CLICK_BOTTOM
def configure_x11(x11: ctypes.CDLL, xtst: ctypes.CDLL) -> None:
window_pointer = ctypes.POINTER(ctypes.c_ulong)
x11.XOpenDisplay.argtypes = [ctypes.c_char_p]
@@ -212,6 +256,18 @@ def configure_x11(x11: ctypes.CDLL, xtst: ctypes.CDLL) -> None:
ctypes.POINTER(XWindowAttributes),
]
x11.XGetWindowAttributes.restype = ctypes.c_int
x11.XFetchName.argtypes = [
ctypes.c_void_p,
ctypes.c_ulong,
ctypes.POINTER(ctypes.c_char_p),
]
x11.XFetchName.restype = ctypes.c_int
x11.XGetTransientForHint.argtypes = [
ctypes.c_void_p,
ctypes.c_ulong,
ctypes.POINTER(ctypes.c_ulong),
]
x11.XGetTransientForHint.restype = ctypes.c_int
x11.XQueryTree.argtypes = [
ctypes.c_void_p,
ctypes.c_ulong,
@@ -644,6 +700,7 @@ def run_launch(args: argparse.Namespace) -> int:
deadline = time.monotonic() + 10
observed: list[tuple[int, int, int, int, int]] = []
dialog: tuple[int, int, int, int, int] | None = None
while time.monotonic() < deadline:
if socket.exists():
x11.XCloseDisplay(display)
@@ -659,7 +716,8 @@ def run_launch(args: argparse.Namespace) -> int:
"owned Hopper process exited",
)
observed = windows(x11, display, root)
if any(window[1:] == EXPECTED_DIALOG for window in observed):
dialog = find_demo_dialog(x11, display, observed)
if dialog is not None:
break
time.sleep(0.1)
else:
@@ -675,8 +733,10 @@ def run_launch(args: argparse.Namespace) -> int:
"expected Hopper demo dialog not found",
)
assert dialog is not None
click_x, click_y = demo_click(dialog)
input_results = (
xtst.XTestFakeMotionEvent(display, -1, DEMO_CLICK[0], DEMO_CLICK[1], 0),
xtst.XTestFakeMotionEvent(display, -1, click_x, click_y, 0),
xtst.XTestFakeButtonEvent(display, 1, True, 0),
xtst.XTestFakeButtonEvent(display, 1, False, 0),
)
@@ -695,9 +755,8 @@ def run_launch(args: argparse.Namespace) -> int:
finally:
if display is not None:
x11.XCloseDisplay(display)
if child is not None and child.poll() is None:
child.terminate()
child.wait()
if child is not None:
stop_process(child)
stop_process(xvfb)
capture.close()
+7 -1
View File
@@ -15,6 +15,12 @@ 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_))
.normalized_result;
}
/** Execute the compiled CLI and return its complete Evidence record. */
export async function artifactCliEvidence(command, target, arguments_ = []) {
const { stdout } = await promisify(execFile)(
process.execPath,
[
@@ -29,7 +35,7 @@ export async function artifactCli(command, target, arguments_ = []) {
const evidence = JSON.parse(stdout);
assert.equal(evidence.error, undefined, stdout);
assert.ok(evidence.normalized_result, "CLI omitted normalized result");
return evidence.normalized_result;
return evidence;
}
/** Run a real stdio MCP subprocess, owning and closing its transport. */
+390
View File
@@ -0,0 +1,390 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { chromium } from "playwright-core";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../../dist/domain/evidence.js";
import { webModuleTraceResultSchema } from "../../dist/domain/webModuleTrace.js";
import { NativeModuleResolver } from "../../dist/browser/modules/NativeModuleResolver.js";
import { startBrowserModuleSite } from "../fixtures/browser-module-site.mjs";
import { mcpTextValue } from "./mcp-verifier-results.mjs";
/** Check native URL resolution against an independently loaded module graph through CLI/MCP. */
export async function verifyBrowserModules(
executable,
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
) {
const site = await startBrowserModuleSite();
const root = await mkdtemp(join(tmpdir(), "rea-browser-modules-"));
const env = {
...process.env,
REA_LOG_LEVEL: "silent",
REA_BROWSER_EXECUTABLE: executable,
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
};
const cli = async (...args) => {
const { stdout } = await promisify(execFile)(
process.execPath,
[entrypoint, ...args, "--json"],
{ env, timeout: 45000, maxBuffer: 16 * 1024 * 1024 },
);
return parseEvidence(JSON.parse(stdout));
};
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({ name: "module-real-e2e", version: "1" });
try {
const oracle = await loadingOracle(executable, site);
const scenario = {
browser: { mode: "launch", executable_path: executable },
start_url: { url: site.origin },
actions: [
{
step_id: "ready",
action: "wait_for",
locator: { kind: "css", selector: '[data-ready="true"]' },
state: "visible",
timeout_ms: 10000,
},
],
capture: { network: { response_body: true } },
};
const capture = await cli(
"capture-browser-scenario",
JSON.stringify(scenario),
);
const capturePath = join(root, "capture.json");
await writeFile(capturePath, JSON.stringify(capture));
const exported = await cli(
"export-web-scripts",
capturePath,
join(root, "export"),
);
const manifest = exported.normalized_result;
const scriptIndex = manifest.scripts.findIndex(
(script) => script.url === `${site.origin}/scoped/main.js?build=2`,
);
assert.ok(
scriptIndex >= 0,
"Real capture did not retain main module response",
);
const mapPath = join(root, "import-map.json");
await writeFile(mapPath, JSON.stringify(site.importMap));
const input = {
manifest_path: manifest.manifest.path,
script_index: scriptIndex,
importer_url: `${site.origin}/scoped/main.js?build=2#entry`,
import_map: { path: mapPath, base_url: `${site.origin}/maps/` },
};
const before = [...site.requests];
const cliEvidence = await cli(
"trace-web-module-imports",
input.manifest_path,
String(scriptIndex),
"--importer-url",
input.importer_url,
"--import-map-path",
mapPath,
"--import-map-base-url",
input.import_map.base_url,
);
const cliResult = assertTrace(cliEvidence, oracle, site, scriptIndex);
await client.connect(transport);
const response = await client.callTool({
name: "trace_web_module_imports",
arguments: input,
});
assert.notEqual(response.isError, true, mcpTextValue(response));
const mcpEvidence = parseEvidence(
JSON.parse(mcpTextValue(response)).evidence,
);
const mcpResult = assertTrace(mcpEvidence, oracle, site, scriptIndex);
assert.deepEqual(mcpResult, cliResult, "CLI and MCP module traces differ");
assert.equal(
cliEvidence.subject.digest.sha256,
createHash("sha256").update(site.main).digest("hex"),
);
const mapBytes = await readFile(mapPath);
assert.equal(
cliResult.import_map.sha256,
createHash("sha256").update(mapBytes).digest("hex"),
);
assert.deepEqual(
site.requests,
before,
"Tracing refetched or executed a captured website dependency",
);
for (const surface of ["cli", "stdio_mcp"]) {
const withoutMap =
surface === "cli"
? await cli(
"trace-web-module-imports",
input.manifest_path,
String(scriptIndex),
)
: parseEvidence(
JSON.parse(
mcpTextValue(
await client.callTool({
name: "trace_web_module_imports",
arguments: {
manifest_path: input.manifest_path,
script_index: scriptIndex,
},
}),
),
).evidence,
);
const result = webModuleTraceResultSchema.parse(
withoutMap.normalized_result,
);
assert.equal(result.import_map, null);
assert.equal(result.imports[0].resolution.state, "rejected");
assert.equal(
result.imports[2].resolution.url,
`${site.origin}/query.js?v=1#one`,
);
}
assert.deepEqual(
site.requests,
before,
"Map-free tracing refetched an asset",
);
const warnings = await new NativeModuleResolver(env).resolve({
importerUrl: input.importer_url,
specifiers: ["alias"],
importMap: {
baseUrl: input.import_map.base_url,
value: { imports: { alias: "./global.js", ignored: 42 } },
},
});
assert.equal(warnings.ok, true, warnings.ok ? "" : warnings.error.message);
assert.ok(
warnings.value.diagnostics.length > 0,
"Native import-map warnings were discarded",
);
assert.equal(
warnings.value.resolutions[0].url,
`${site.origin}/maps/global.js`,
);
const controller = new AbortController();
let owned;
const interrupted = await new NativeModuleResolver(env, async (options) => {
owned = await chromium.launch(options);
controller.abort();
return owned;
}).resolve(
{
importerUrl: input.importer_url,
importMap: null,
specifiers: ["./dep.js"],
},
{ signal: controller.signal },
);
assert.equal(
interrupted.ok,
false,
"Cancellation returned successful native resolution",
);
assert.equal(interrupted.error._tag, "AnalysisCancelledError");
assert.equal(
owned.isConnected(),
false,
"Cancelled native resolver left its owned browser connected",
);
const syntheticImporter = "http://app.invalid/main.js?build=1#entry";
const syntheticMapBase = "https://maps.invalid/maps/map.json?x=1&y=2#base";
for (const useMap of [false, true])
for (const surface of ["cli", "stdio_mcp"]) {
let evidence;
if (surface === "cli")
evidence = await cli(
"trace-web-module-imports",
input.manifest_path,
String(scriptIndex),
"--importer-url",
syntheticImporter,
...(useMap
? [
"--import-map-path",
mapPath,
"--import-map-base-url",
syntheticMapBase,
]
: []),
);
else {
const response = await client.callTool({
name: "trace_web_module_imports",
arguments: {
manifest_path: input.manifest_path,
script_index: scriptIndex,
importer_url: syntheticImporter,
...(useMap
? { import_map: { path: mapPath, base_url: syntheticMapBase } }
: {}),
},
});
assert.notEqual(response.isError, true, mcpTextValue(response));
evidence = parseEvidence(JSON.parse(mcpTextValue(response)).evidence);
}
const result = webModuleTraceResultSchema.parse(
evidence.normalized_result,
);
if (useMap)
assert.deepEqual(result.imports[0].resolution, {
state: "resolved",
url: "https://maps.invalid/maps/global.js",
});
else assert.equal(result.imports[0].resolution.state, "rejected");
assert.equal(
result.imports[2].resolution.url,
"http://app.invalid/query.js?v=1#one",
);
assert.equal(
evidence.raw_result.native_report.importer_url,
syntheticImporter,
);
assert.equal(
result.source.script.url,
`${site.origin}/scoped/main.js?build=2`,
);
}
const escapedBase = await new NativeModuleResolver(env).resolve({
importerUrl: syntheticImporter,
importMap: {
baseUrl: "https://maps.invalid/maps/a&amp;b/map.json?x=1&y=2#base",
value: { imports: { alias: "./alias.js" } },
},
specifiers: ["alias"],
});
assert.equal(
escapedBase.ok,
true,
escapedBase.ok ? "" : escapedBase.error.message,
);
assert.deepEqual(escapedBase.value.resolutions[0], {
state: "resolved",
url: "https://maps.invalid/maps/a&amp;b/alias.js",
});
assert.deepEqual(
site.requests,
before,
"Synthetic-context tracing refetched or executed a website asset",
);
return {
independent_http_importer_and_https_map_base: true,
cancellation_cleanup: true,
native_warnings_retained: true,
cli: true,
stdio_mcp: true,
public_trace_cases: 8,
literal_imports: 11,
computed_unknowns: 1,
native_loading_oracle: true,
independent_module_instances: 3,
import_map_scopes: true,
null_and_backtracking_errors: true,
query_fragment_identity: true,
source_identity_verified: true,
map_identity_verified: true,
implicit_refetches: 0,
};
} finally {
await client.close();
await transport.close();
await site.close();
await rm(root, { recursive: true, force: true });
}
}
const assertTrace = (evidence, oracle, site, scriptIndex) => {
const result = webModuleTraceResultSchema.parse(evidence.normalized_result);
assert.equal(result.source.script_index, scriptIndex);
assert.equal(result.parser.state, "parsed");
assert.equal(result.imports.length, 12);
assert.deepEqual(
result.imports.slice(0, 11).map((item) => item.resolution),
oracle.resolutions,
);
assert.deepEqual(result.imports[11].resolution, {
state: "unknown",
reason: "computed-specifier",
});
assert.ok(result.imports.every((item) => item.execution === "unknown"));
assert.equal(
result.imports[6].captured_candidates.length,
0,
"An unloaded lazy URL became a captured source",
);
for (const index of [2, 3]) {
assert.ok(result.imports[index].captured_candidates.length > 0);
assert.ok(
result.imports[index].captured_candidates.every(
(candidate) => candidate.match === "response-url-without-fragment",
),
);
}
assert.equal(result.imports[4].resolution.url, `${site.origin}/query.js?v=2`);
assert.equal(
result.importer.url,
`${site.origin}/scoped/main.js?build=2#entry`,
);
assert.equal(result.engine.id, "chromium-native-module-resolver");
return result;
};
const loadingOracle = async (executable, site) => {
const browser = await chromium.launch({
executablePath: executable,
headless: true,
timeout: 15000,
args: [
"--disable-dev-shm-usage",
...(process.env.REA_BROWSER_NO_SANDBOX === "true"
? ["--no-sandbox"]
: []),
],
});
try {
const page = await browser.newPage();
await page.goto(site.origin);
await page.waitForFunction(
() => globalThis.__moduleProof !== undefined,
undefined,
{ timeout: 10000 },
);
const proof = await page.evaluate(() => globalThis.__moduleProof);
assert.equal(proof.alias, "scoped");
assert.equal(proof.pkg, "pkg");
assert.equal(proof.same_module, true);
assert.equal(proof.different_fragments, true);
assert.equal(new Set(proof.instances).size, 3);
assert.deepEqual(proof.urls, [
`${site.origin}/query.js?v=1#one`,
`${site.origin}/query.js?v=1#two`,
`${site.origin}/query.js?v=2`,
]);
assert.ok(
!site.requests.some(
(url) =>
url.includes("lazy") ||
url.includes("escape") ||
url.includes("blocked"),
),
);
return proof;
} finally {
await browser.close();
}
};
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { webScriptExportResultSchema } from "../../dist/domain/webScriptExport.js";
import { javascriptApplicationAnalysisResultSchema } from "../../dist/domain/javascriptApplicationAnalysis.js";
import { javascriptApplicationAnalysisResultSchema } from "../../dist/domain/javascript/javascriptApplicationAnalysis.js";
/** Verify exact exported bytes and the independently authored source manifest. */
export const assertWebScriptExport = async (evidence, capture, assets) => {
+193
View File
@@ -0,0 +1,193 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../../dist/domain/evidence.js";
import { webSourceLocationResultSchema } from "../../dist/domain/webSourceLocation.js";
import { startBrowserSourceMapSite } from "../fixtures/browser-source-map-site.mjs";
import { mcpTextValue } from "./mcp-verifier-results.mjs";
/** Verify real Chrome capture → export → original compiler point through packaged CLI/MCP. */
export async function verifyBrowserSourceMap(
executable,
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
) {
const site = await startBrowserSourceMapSite();
const root = await mkdtemp(join(tmpdir(), "rea-browser-source-map-"));
const env = {
...process.env,
REA_LOG_LEVEL: "silent",
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
};
const cli = async (...args) => {
const { stdout } = await promisify(execFile)(
process.execPath,
[entrypoint, ...args, "--json"],
{ env, timeout: 45000, maxBuffer: 16 * 1024 * 1024 },
);
return parseEvidence(JSON.parse(stdout));
};
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({ name: "source-map-real-e2e", version: "1" });
try {
const capture = await cli(
"capture-browser-scenario",
JSON.stringify({
browser: { mode: "launch", executable_path: executable },
start_url: { url: site.origin },
actions: [
{
step_id: "ready",
action: "wait_for",
locator: {
kind: "css",
selector: '[data-ready="source-map proof"]',
},
state: "visible",
timeout_ms: 10000,
},
],
capture: { network: { response_body: true } },
}),
);
const capturePath = join(root, "capture.json");
await writeFile(capturePath, JSON.stringify(capture));
const exported = await cli(
"export-web-scripts",
capturePath,
join(root, "export"),
);
const manifest = exported.normalized_result;
const manifestBytes = await readFile(manifest.manifest.path);
const identity = {
sha256: sha(manifestBytes),
bytes: manifestBytes.length,
};
const scriptIndex = manifest.scripts.findIndex(
(script) => script.url === `${site.origin}/app.js?build=7`,
);
assert.ok(
scriptIndex >= 0,
"Actual browser capture omitted compiled source",
);
const mapPath = join(root, "app.js.map");
await writeFile(mapPath, site.map);
const before = site.mapRequests();
await client.connect(transport);
let expected;
for (const context of [
site.origin,
site.origin.replace("://", "://selected-user:selected-value@"),
]) {
const input = {
manifest_path: manifest.manifest.path,
script_index: scriptIndex,
source_map: {
path: mapPath,
url: `${context}/maps/app.js.map?v=7#context`,
},
generated_position: site.position,
};
expected = await assertPublicPair(cli, client, {
site,
input,
capture,
identity,
});
}
assert.equal(
site.mapRequests(),
before,
"Point tracing fetched the selected map URL",
);
return {
compiler: site.compiler,
generated_sha256: sha(site.generated),
source_map_sha256: sha(site.map),
original_sha256: sha(site.original),
public_cli: true,
public_stdio_mcp: true,
actual_chrome_capture: true,
source_map_refetch: false,
original_point: site.originalPosition,
codec: expected.engine,
runtime: expected.runtime,
selected_inert_userinfo_context_preserved: true,
public_point_cases: 4,
};
} finally {
await client.close();
await transport.close();
await site.close();
await rm(root, { recursive: true, force: true });
}
}
function assertPoint(evidence, { site, input, capture, identity }) {
const result = webSourceLocationResultSchema.parse(
evidence.normalized_result,
);
assert.equal(result.manifest.sha256, identity.sha256);
assert.equal(result.manifest.bytes, identity.bytes);
assert.equal(result.source.bytes, Buffer.byteLength(site.generated));
assert.equal(result.source.sha256, sha(site.generated));
assert.equal(result.source_map.sha256, sha(site.map));
assert.equal(result.source_map.url, input.source_map.url);
assert.equal(result.manifest.source_evidence_id, capture.evidence_id);
assert.equal(result.execution, "unknown");
assert.equal(result.source_authenticity, "unknown");
assert.equal(result.matches.length, 1);
const match = result.matches[0];
assert.equal(match.state, "mapped");
assert.deepEqual(match.original_position, {
...site.originalPosition,
offset: site.originalOffset,
content_position: "verified-in-embedded-text",
});
assert.equal(
match.resolved_url,
new URL("../src/fixture.ts?v=7#original", input.source_map.url).href,
);
assert.equal(match.content.state, "embedded");
assert.equal(match.content.text, site.original);
assert.equal(match.content.utf8_sha256, sha(site.original));
assert.ok(result.runtime.v8_heap_limit_bytes <= 256 * 1024 * 1024);
assert.deepEqual(evidence.raw_result, { source_map_text: site.map });
return result;
}
const sha = (text) => createHash("sha256").update(text).digest("hex");
async function assertPublicPair(cli, client, oracle) {
const { input } = oracle;
const evidence = await cli(
"trace-web-source-location",
input.manifest_path,
String(input.script_index),
input.source_map.path,
input.source_map.url,
String(input.generated_position.line),
String(input.generated_position.column),
);
const expected = assertPoint(evidence, oracle);
const response = await client.callTool({
name: "trace_web_source_location",
arguments: input,
});
assert.notEqual(response.isError, true, mcpTextValue(response));
const mcpEvidence = parseEvidence(
JSON.parse(mcpTextValue(response)).evidence,
);
assert.deepEqual(assertPoint(mcpEvidence, oracle), expected);
assert.equal(evidence.parameters.source_map.url, input.source_map.url);
return expected;
}
+48 -4
View File
@@ -67,6 +67,8 @@ export const toolFamilyCatalog = (sources) => {
contracts: [
...sources.applicationContracts.APPLICATION_TOOL_CONTRACTS,
...sources.webScriptContracts.WEB_SCRIPT_TOOL_CONTRACTS,
...sources.javascriptRecoveryContracts
.JAVASCRIPT_RECOVERY_TOOL_CONTRACTS,
],
},
{
@@ -94,6 +96,21 @@ export const toolFamilyCatalog = (sources) => {
/** Build provider identities with their sorted capability names. */
export const providerCatalog = (sources) => {
const contractsByName = new Map(
sources.toolContracts.TOOL_CONTRACTS.map((contract) => [
contract.name,
contract,
]),
);
const declaredContracts = (providerId, operations) =>
operations.map((operation) => {
const contract = contractsByName.get(operation);
if (contract === undefined)
throw new TypeError(
`Provider ${providerId} declares an unknown analyst operation: ${operation}`,
);
return contract;
});
const {
applicationContracts,
reconciliationContracts,
@@ -103,15 +120,21 @@ export const providerCatalog = (sources) => {
return [
{
identity: sources.hopperProvider.HOPPER_PROVIDER_IDENTITY,
contracts: sources.hopperProvider.HOPPER_PROVIDER_TOOL_CONTRACTS,
contracts: declaredContracts(
"hopper",
sources.hopperProvider.HOPPER_OPERATIONS,
),
},
{
identity: sources.ghidraProvider.GHIDRA_PROVIDER_IDENTITY,
contracts: sources.ghidraProvider.GHIDRA_PROVIDER_TOOL_CONTRACTS,
contracts: declaredContracts(
"ghidra",
sources.ghidraProvider.GHIDRA_OPERATIONS,
),
},
{
identity: sources.idaProvider.IDA_PROVIDER_IDENTITY,
contracts: sources.idaProvider.IDA_PROVIDER_TOOL_CONTRACTS,
contracts: declaredContracts("ida", sources.idaProvider.IDA_OPERATIONS),
},
{
identity: sources.nativeProvider.NATIVE_MACOS_PROVIDER_IDENTITY,
@@ -185,12 +208,33 @@ export const providerCatalog = (sources) => {
{
identity:
sources.artifactProviders.JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER,
contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS,
contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS.filter(
({ name }) =>
name !== "project_android_application_graph" &&
name !== "project_apple_application_graph",
),
},
{
identity: sources.artifactProviders.ANDROID_APPLICATION_PROVIDER,
contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS.filter(
({ name }) => name === "project_android_application_graph",
),
},
{
identity: sources.artifactProviders.APPLE_APPLICATION_PROVIDER,
contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS.filter(
({ name }) => name === "project_apple_application_graph",
),
},
{
identity: sources.artifactProviders.WEB_SCRIPT_EXPORT_PROVIDER,
contracts: sources.webScriptContracts.WEB_SCRIPT_TOOL_CONTRACTS,
},
{
identity: sources.javascriptRecoveryProvider.WAKARU_PROVIDER_IDENTITY,
contracts:
sources.javascriptRecoveryContracts.JAVASCRIPT_RECOVERY_TOOL_CONTRACTS,
},
]
.map(({ identity, contracts }) => ({
id: identity.id,
+34 -23
View File
@@ -42,30 +42,34 @@ const SOURCE_PATHS = {
sessionContracts: "dist/contracts/sessionToolContracts.js",
nativeContracts: "dist/contracts/nativeToolContracts.js",
artifactContracts: "dist/contracts/artifactToolContracts.js",
managedContracts: "dist/contracts/managedToolContracts.js",
firmwareContracts: "dist/contracts/firmwareToolContracts.js",
managedContracts: "dist/contracts/managed/managedToolContracts.js",
firmwareContracts: "dist/contracts/firmware/firmwareToolContracts.js",
firmwareProvider: "dist/firmware/FirmwareRelease.js",
androidContracts: "dist/contracts/androidToolContracts.js",
androidContracts: "dist/contracts/android/androidToolContracts.js",
androidProvider: "dist/android/JadxRelease.js",
managedWorkflowContracts: "dist/contracts/managedWorkflowToolContracts.js",
managedWorkflowContracts:
"dist/contracts/managed/managedWorkflowToolContracts.js",
browserContracts: "dist/contracts/browserToolContracts.js",
browserScenarioContracts: "dist/contracts/browserScenarioToolContracts.js",
electronContracts: "dist/contracts/electronToolContracts.js",
electronContracts: "dist/contracts/javascript/electronToolContracts.js",
javascriptRuntimeObservationContracts:
"dist/contracts/javascriptRuntimeObservationToolContracts.js",
"dist/contracts/javascript/javascriptRuntimeObservationToolContracts.js",
applicationContracts: "dist/contracts/applicationToolContracts.js",
webScriptContracts: "dist/contracts/webScriptToolContracts.js",
javascriptRecoveryContracts:
"dist/contracts/javascript/javascriptRecoveryToolContracts.js",
javascriptRecoveryProvider: "dist/javascript/recovery/WakaruRelease.js",
supportedClients: "dist/application/SupportedClients.js",
hopperProvider: "dist/hopper/HopperProvider.js",
ghidraProvider: "dist/ghidra/GhidraProvider.js",
hopperProvider: "dist/hopper/HopperProviderCapabilities.js",
ghidraProvider: "dist/ghidra/GhidraProviderCapabilities.js",
idaProvider: "dist/ida/IdaProviderCapabilities.js",
nativeProvider: "dist/native/NativeMacOSProvider.js",
nativeProvider: "dist/native/NativeMacOSProviderMetadata.js",
artifactProviders: "dist/application/InvestigationProviders.js",
browserProvider: "dist/browser/CdpBrowserProvider.js",
browserScenarioProvider: "dist/browser/PlaywrightBrowserScenarioProvider.js",
electronProvider: "dist/browser/CdpElectronProvider.js",
electronActiveProvider: "dist/browser/PlaywrightElectronActiveProvider.js",
v8InspectorProvider: "dist/browser/V8InspectorProvider.js",
v8InspectorProvider: "dist/inspector/V8InspectorProvider.js",
evidence: "dist/domain/evidence.js",
evidenceBundle: "dist/domain/evidenceBundle.js",
evidenceCompletion: "dist/domain/evidenceCompletionLedger.js",
@@ -78,24 +82,31 @@ const SOURCE_PATHS = {
browserScenarioCapture: "dist/domain/browserScenarioCapture.js",
browserScenarioDiff: "dist/domain/browserScenarioDiff.js",
browserSession: "dist/domain/browserSession.js",
electronObservation: "dist/domain/electronObservation.js",
electronActiveObservation: "dist/domain/electronActiveObservation.js",
javascriptRuntimeObservation: "dist/domain/javascriptRuntimeObservation.js",
electronObservation: "dist/domain/javascript/electronObservation.js",
electronActiveObservation:
"dist/domain/javascript/electronActiveObservation.js",
javascriptRuntimeObservation:
"dist/domain/javascript/javascriptRuntimeObservation.js",
webBundleAnalysis: "dist/domain/webBundleAnalysis.js",
webCaptureDiff: "dist/domain/webCaptureDiff.js",
managedArtifact: "dist/domain/managedArtifact.js",
managedComparison: "dist/domain/managedMemberComparison.js",
managedNativeVerification: "dist/domain/managedNativeVerification.js",
managedArtifact: "dist/domain/managed/managedArtifact.js",
managedComparison: "dist/domain/managed/managedMemberComparison.js",
managedNativeVerification: "dist/domain/managed/managedNativeVerification.js",
webMcpDiscovery: "dist/domain/webMcpDiscovery.js",
webScreenshot: "dist/domain/webScreenshot.js",
javascriptApplicationGraph: "dist/domain/javascriptApplicationGraph.js",
javascriptApplicationAnalysis: "dist/domain/javascriptApplicationAnalysis.js",
javascriptSemanticGraph: "dist/domain/javascriptSemanticGraph.js",
javascriptSemanticQuery: "dist/domain/javascriptSemanticQuerySchemas.js",
javascriptSemanticTrace: "dist/domain/javascriptSemanticTraceSchemas.js",
javascriptFeatureTrace: "dist/domain/javascriptFeatureTraceSchemas.js",
javascriptApplicationGraph:
"dist/domain/javascript/javascriptApplicationGraph.js",
javascriptApplicationAnalysis:
"dist/domain/javascript/javascriptApplicationAnalysis.js",
javascriptSemanticGraph: "dist/domain/javascript/javascriptSemanticGraph.js",
javascriptSemanticQuery:
"dist/domain/javascript/javascriptSemanticQuerySchemas.js",
javascriptSemanticTrace:
"dist/domain/javascript/javascriptSemanticTraceSchemas.js",
javascriptFeatureTrace:
"dist/domain/javascript/javascriptFeatureTraceSchemas.js",
javascriptVersionComparison:
"dist/domain/javascriptApplicationVersionComparisonSchemas.js",
"dist/domain/javascript/javascriptApplicationVersionComparisonSchemas.js",
reconstructionVerification:
"dist/domain/reconstructionVerificationSchemas.js",
residualUnknown: "dist/domain/residualUnknown.js",
+269
View File
@@ -0,0 +1,269 @@
import { execFile } from "node:child_process";
import { mkdir, rm, symlink, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
const exec = promisify(execFile);
/** Commands the macOS bundle fixture needs from Command Line Tools. */
export const MACOS_BUNDLE_FIXTURE_COMMANDS = [
["/usr/bin/xcrun", ["--find", "clang"]],
["/usr/bin/xcrun", ["--find", "codesign"]],
["/usr/bin/ditto", ["-h"]],
["/usr/bin/hdiutil", ["help"]],
];
/** Fail with the missing command name before building any fixture. */
export async function preflightMacosBundleFixture() {
if (process.platform !== "darwin")
throw new Error(
"macOS bundle verification requires a macOS host with Command Line Tools",
);
for (const [command, arguments_] of MACOS_BUNDLE_FIXTURE_COMMANDS)
try {
await exec(command, arguments_);
} catch (cause) {
// `ditto -h` and `hdiutil help` exit non-zero while printing usage.
if (cause?.code === "ENOENT")
throw new Error(
`macOS bundle verification requires ${command} ${arguments_.join(" ")}`,
{ cause },
);
if (command === "/usr/bin/xcrun")
throw new Error(
`macOS bundle verification requires Command Line Tools (${arguments_.join(" ")} failed)`,
{ cause },
);
}
}
const plist = (entries) =>
`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
${Object.entries(entries)
.map(([key, value]) => ` <key>${key}</key>\n <string>${value}</string>`)
.join("\n")}
</dict>
</plist>
`;
const bundlePlist = (identifier, executable, packageType = "APPL") =>
plist({
CFBundleIdentifier: identifier,
CFBundleExecutable: executable,
CFBundlePackageType: packageType,
});
const write = async (path, text) => {
await mkdir(dirname(path), { recursive: true });
await writeFile(path, text);
};
/** Compile one C source beneath `root/sources` into `output`. */
const clang = async (root, { name, source, output, flags = [] }) => {
const sourcePath = join(root, "sources", `${name}.c`);
await write(sourcePath, source);
await mkdir(dirname(output), { recursive: true });
await exec("/usr/bin/xcrun", [
"clang",
"-O0",
sourcePath,
"-o",
output,
...flags,
]);
};
const PROGRAM = `int main(void) { return 0; }\n`;
/** A versioned framework whose own rpath supplies a second `libchain.dylib`. */
const buildCoreFramework = async (root, frameworks) => {
const core = join(frameworks, "Core.framework");
const version = join(core, "Versions", "A");
const chainFlags = ["-dynamiclib", "-install_name", "@rpath/libchain.dylib"];
await clang(root, {
name: "chain-app",
source: `const char *chain_variant(void) { return "app"; }\n`,
output: join(frameworks, "libchain.dylib"),
flags: chainFlags,
});
await clang(root, {
name: "chain-framework",
source: `const char *chain_variant(void) { return "framework"; }\n`,
output: join(version, "Libraries", "libchain.dylib"),
flags: chainFlags,
});
await clang(root, {
name: "sibling",
source: `int sibling_value(void) { return 7; }\n`,
output: join(version, "libsibling.dylib"),
flags: ["-dynamiclib", "-install_name", "@loader_path/libsibling.dylib"],
});
await clang(root, {
name: "core",
source: `const char *chain_variant(void);
int sibling_value(void);
const char *core_chain(void) { return sibling_value() == 7 ? chain_variant() : "broken"; }
`,
output: join(version, "Core"),
flags: [
"-dynamiclib",
"-install_name",
"@rpath/Core.framework/Versions/A/Core",
"-Wl,-rpath,@loader_path/Libraries",
join(version, "libsibling.dylib"),
join(version, "Libraries", "libchain.dylib"),
],
});
await write(
join(version, "Resources", "Info.plist"),
bundlePlist("com.example.rea.core", "Core", "FMWK"),
);
const service = join(version, "XPCServices", "FwSvc.xpc", "Contents");
await clang(root, {
name: "framework-service",
source: PROGRAM,
output: join(service, "MacOS", "FwSvc"),
});
await write(
join(service, "Info.plist"),
bundlePlist("com.example.rea.core.service", "FwSvc", "XPC!"),
);
await symlink("A", join(core, "Versions", "Current"));
await symlink("Versions/Current/Core", join(core, "Core"));
await symlink("Versions/Current/Resources", join(core, "Resources"));
await symlink("Versions/Current/XPCServices", join(core, "XPCServices"));
return join(version, "Core");
};
/** The main executable, with a weak dependency deleted after linking. */
const buildMainExecutable = async (root, contents, coreBinary) => {
const gone = join(contents, "Frameworks", "libgone.dylib");
await clang(root, {
name: "gone",
source: `int gone_value(void) { return 1; }\n`,
output: gone,
flags: ["-dynamiclib", "-install_name", "@rpath/libgone.dylib"],
});
const executable = join(contents, "MacOS", "MacFixture");
await clang(root, {
name: "main",
source: `#include <stdio.h>
const char *core_chain(void);
extern int gone_value(void) __attribute__((weak_import));
int main(void) {
printf("chain=%s weak=%s\\n", core_chain(), gone_value ? "present" : "absent");
return 0;
}
`,
output: executable,
flags: [
"-Wl,-rpath,@executable_path/../Frameworks",
coreBinary,
`-Wl,-weak_library,${gone}`,
],
});
await rm(gone);
await write(
join(contents, "Info.plist"),
bundlePlist("com.example.rea.fixture", "MacFixture"),
);
return executable;
};
/** One `Contents/`-layout bundle with a trivial executable. */
const buildNestedBundle = async (root, { bundle, executable, plist }) => {
await clang(root, {
name: executable,
source: PROGRAM,
output: join(bundle, "Contents", "MacOS", executable),
});
await write(join(bundle, "Contents", "Info.plist"), plist);
};
const buildNestedBundles = async (root, contents, coreBinary) => {
const service = join(contents, "XPCServices", "Svc.xpc", "Contents");
await clang(root, {
name: "service",
source: `const char *core_chain(void);
int main(void) { return core_chain()[0] == 'b'; }
`,
output: join(service, "MacOS", "Svc"),
flags: ["-Wl,-rpath,@executable_path/../../../../Frameworks", coreBinary],
});
await write(
join(service, "Info.plist"),
bundlePlist("com.example.rea.fixture.service", "Svc", "XPC!"),
);
await buildNestedBundle(root, {
bundle: join(contents, "PlugIns", "Ext.appex"),
executable: "Ext",
plist: bundlePlist("com.example.rea.fixture.extension", "Ext", "XPC!"),
});
await buildNestedBundle(root, {
bundle: join(contents, "Library", "LoginItems", "Login.app"),
executable: "Login",
plist: bundlePlist("com.example.rea.fixture.login", "Login"),
});
};
/** A privileged helper, its launchd plists, and a non-bundle helper tool. */
const buildLaunchItems = async (root, contents) => {
await clang(root, {
name: "privileged-helper",
source: PROGRAM,
output: join(
contents,
"Library",
"LaunchServices",
"com.example.rea.helper",
),
});
await write(
join(contents, "Library", "LaunchDaemons", "com.example.rea.helper.plist"),
plist({
Label: "com.example.rea.helper",
BundleProgram: "Contents/Library/LaunchServices/com.example.rea.helper",
}),
);
await write(
join(contents, "Library", "LaunchAgents", "com.example.rea.agent.plist"),
plist({
Label: "com.example.rea.agent",
BundleProgram: "Contents/Helpers/rea-tool",
}),
);
await clang(root, {
name: "tool",
source: PROGRAM,
output: join(contents, "Helpers", "rea-tool"),
});
};
/**
* Build `MacFixture.app` with every bundle role and dylib-loading shape REA
* projects. The main executable prints which `libchain.dylib` copy dyld
* selected, so callers can compare static resolution with a real load.
*/
export async function buildMacosBundleFixture(root) {
const app = join(root, "MacFixture.app");
const contents = join(app, "Contents");
const coreBinary = await buildCoreFramework(
root,
join(contents, "Frameworks"),
);
const executable = await buildMainExecutable(root, contents, coreBinary);
await buildNestedBundles(root, contents, coreBinary);
await buildLaunchItems(root, contents);
await exec("/usr/bin/xcrun", [
"codesign",
"--force",
"--deep",
"--sign",
"-",
app,
]);
return { app, executable };
}
+20
View File
@@ -0,0 +1,20 @@
/** One dependency crossing a settled source ownership boundary. */
export interface ModuleBoundaryViolation {
readonly file: string;
readonly line: number;
readonly specifier: string;
readonly target: string;
readonly boundary:
| "artifact-acquisition"
| "pure-layer"
| "application-composition"
| "provider-construction"
| "provider-generated-catalog";
}
/** Check resolved source ownership, including type imports and reexports. */
export function inspectModuleBoundaries(
file: string,
source: string,
root: string,
): readonly ModuleBoundaryViolation[];
+129
View File
@@ -0,0 +1,129 @@
import { parse } from "@babel/parser";
import { traverseFast } from "@babel/types";
import { isAbsolute, relative, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const PURE_LAYERS = new Set(["domain", "contracts"]);
const MIGRATED_PROVIDER_ROOTS = new Set([
"android",
"firmware",
"ghidra",
"hopper",
"ida",
"inspector",
"javascript",
]);
const PROVIDER_ROOTS = new Set([
...MIGRATED_PROVIDER_ROOTS,
"artifacts",
"browser",
"dotnet",
"native",
]);
const repositoryPath = (root, path) =>
relative(root, path).replaceAll("\\", "/");
const literalValue = (node) => {
if (node?.type === "StringLiteral") return node.value;
if (node?.type === "TemplateLiteral" && node.expressions.length === 0)
return node.quasis[0]?.value.cooked ?? undefined;
return undefined;
};
const importSpecifier = (node) => {
switch (node.type) {
case "ImportDeclaration":
case "ExportNamedDeclaration":
case "ExportAllDeclaration":
case "ImportExpression":
case "TSImportType":
return literalValue(node.source);
case "TSImportEqualsDeclaration":
return node.moduleReference.type === "TSExternalModuleReference"
? literalValue(node.moduleReference.expression)
: undefined;
default:
return undefined;
}
};
const sourceTarget = (file, specifier) => {
if (specifier.startsWith("file:")) return fileURLToPath(specifier);
if (isAbsolute(specifier)) return resolve(specifier);
return specifier.startsWith(".")
? fileURLToPath(new URL(specifier, pathToFileURL(file)))
: undefined;
};
const failedBoundary = (file, target) => {
if (!file.startsWith("src/") || !target.startsWith("src/")) return undefined;
const layer = file.split("/")[1];
const targetLayer = target.split("/")[1];
if (
file.startsWith("src/application/javascript/") &&
/^src\/artifacts\/(?:Asar|Directory)ArtifactReader\.(?:js|ts)$/u.test(
target,
)
)
return "provider-construction";
if (
(file.startsWith("src/artifacts/javascript/") ||
/^src\/artifacts\/ArtifactHash\.(?:js|ts)$/u.test(file)) &&
(["application", "composition", "server", "cli", "main"].includes(
targetLayer,
) ||
/^src\/(?:cli|main)\./u.test(target))
)
return "artifact-acquisition";
if (
PROVIDER_ROOTS.has(layer) &&
/^src\/generatedMcpToolCatalog\.(?:js|ts)$/u.test(target)
)
return "provider-generated-catalog";
if (PURE_LAYERS.has(layer)) {
if (
(/^src\/[^/]+\//u.test(target) && !PURE_LAYERS.has(targetLayer)) ||
/^src\/(?:cli|main)\./u.test(target)
)
return "pure-layer";
}
if (file === "src/application/runtime.ts") return undefined;
if (layer === "application" && targetLayer === "composition")
return "application-composition";
if (
(layer === "application" || layer === "server") &&
(MIGRATED_PROVIDER_ROOTS.has(targetLayer) ||
/^src\/browser\/[^/]*Provider\.[^/]+$/u.test(target))
)
return "provider-construction";
return undefined;
};
/** Check resolved source ownership, including type imports and reexports. */
export const inspectModuleBoundaries = (file, source, root) => {
const absoluteFile = resolve(root, file);
const importer = repositoryPath(root, absoluteFile);
const tree = parse(source, {
sourceType: "module",
plugins: ["typescript"],
});
const violations = [];
traverseFast(tree, (node) => {
const specifier = importSpecifier(node);
if (specifier === undefined) return;
const resolved = sourceTarget(absoluteFile, specifier);
if (resolved === undefined) return;
const target = repositoryPath(root, resolved);
const boundary = failedBoundary(importer, target);
if (boundary !== undefined)
violations.push({
file: importer,
line: node.loc?.start.line ?? 1,
specifier,
target,
boundary,
});
});
return violations;
};
+9 -1
View File
@@ -41,6 +41,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
const { createCli } = await import("../dist/cli.js");
const {
renderCliOutputArgumentError,
renderEmptyFilteredCliOutput,
sanitizeCliOutput,
validateCliOutputArguments,
} = await import("../dist/cliOutput.js");
@@ -49,9 +50,16 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
process.stdout.write(renderCliOutputArgumentError(outputArguments));
process.exitCode = 1;
} else {
let wroteOutput = false;
await createCli().serve(args, {
stdout: (output) => process.stdout.write(sanitizeCliOutput(output)),
stdout: (output) => {
const sanitized = sanitizeCliOutput(output);
if (sanitized.length > 0) wroteOutput = true;
process.stdout.write(sanitized);
},
});
if (!wroteOutput)
process.stdout.write(renderEmptyFilteredCliOutput(args) ?? "");
}
}
+181 -109
View File
@@ -12,118 +12,190 @@ if (process.platform !== "darwin")
"Apple dispatch verification requires macOS with a host Objective-C compiler and Foundation SDK",
);
const exec = promisify(execFile);
const source = (name) =>
fileURLToPath(
new URL(`../tests/conformance/native/${name}`, import.meta.url),
);
/** Both link modes: legacy LC_DYLD_INFO binds and the default chained fixups. */
const LINK_MODES = [
{
mode: "dyld-info",
flags: ["-Wl,-no_fixup_chains"],
fixups: "LC_DYLD_INFO bind opcodes",
swift: true,
},
{ mode: "chained", flags: [], fixups: "chained fixups:", swift: true },
// arm64e uses authenticated chained pointers (DYLD_CHAINED_PTR_ARM64E_*).
...(process.arch === "arm64"
? [
{
mode: "chained-arm64e",
flags: ["-arch", "arm64e"],
fixups: "chained fixups: DYLD_CHAINED_PTR_ARM64E",
swift: false,
},
]
: []),
];
const fixupCoverage = (metadata) =>
metadata.coverage.find(({ facet }) => facet === "pointer_fixups");
const inspect = async (path) => {
const target = await parseBinaryTarget(path);
if (!target.ok) throw target.error;
return {
target: target.value,
metadata: (await inspectAppleDispatchMetadata(target.value, 5000)).result,
};
};
const checkObjc = (metadata, mode) => {
const fixture = metadata.objc_classes.find(
({ name, is_meta_class: meta }) => name === "ReaDispatchFixture" && !meta,
);
const category = metadata.objc_categories.find(
({ name }) => name === "ReaDispatchAdditions",
);
const objcCoverage = metadata.coverage.find(
({ facet }) => facet === "objc_class_method_ivar_metadata",
);
if (
!metadata.objc_dispatch_implementations.some(
(item) =>
item.class_name === "ReaDispatchFixture" &&
item.selector === "performAction:" &&
item.implementation_address !== null,
) ||
!metadata.objc_dispatch_implementations.some(
(item) =>
item.selector === "fixtureVersion" && item.method_type === "class",
) ||
!metadata.objc_ivars.some(
(item) => item.name === "state" && item.offset !== null,
) ||
!metadata.objc_protocol_records.some(
(protocol) =>
protocol.name === "ReaDispatchProtocol" &&
protocol.methods.some(
(method) => method.selector === "performAction:",
) &&
protocol.optional_methods.some(
(method) => method.selector === "optionalFixtureValue",
),
) ||
// The superclass lives in libobjc, so it comes from a bind fixup.
fixture?.super_class !== "NSObject" ||
fixture.decode.status !== "decoded" ||
!fixture.properties.some(
(property) =>
property.name === "fixtureName" &&
property.type_encoding === '@"NSString"' &&
property.attributes.some(({ name }) => name === "C"),
) ||
category?.class_name !== "NSString" ||
category.class_source !== "external" ||
!category.instance_methods.includes("reaFixtureLength") ||
!metadata.objc_dispatch_implementations.some(
(item) =>
item.class_name === "NSString" &&
item.category === "ReaDispatchAdditions" &&
item.selector === "reaFixtureLength",
) ||
objcCoverage?.status !== "complete" ||
fixupCoverage(metadata)?.reason?.startsWith(mode.fixups) !== true
)
throw new Error(
`Apple metadata fixture drifted (${mode.mode}): ${JSON.stringify(metadata)}`,
);
};
const checkSwift = (metadata, mode) => {
if (
!metadata.swift_dispatch_slots.some(
(slot) =>
slot.table_kind === "class_vtable" &&
slot.owner === "ReaVtableFixture" &&
slot.decode.status === "decoded",
)
)
throw new Error(
`Swift class vtable fixture unavailable (${mode.mode}): ${JSON.stringify(metadata.coverage)}`,
);
if (
!metadata.swift_conformances.some(
(item) =>
item.type_name === "ReaWitnessFixture" &&
item.protocol_name === "ReaWitnessFixtureProtocol",
) ||
!metadata.swift_dispatch_slots.some(
(item) =>
item.owner === "ReaWitnessFixture: ReaWitnessFixtureProtocol" &&
item.implementation_address !== null,
) ||
fixupCoverage(metadata)?.reason?.startsWith(mode.fixups) !== true
)
throw new Error(
`Swift metadata fixture drifted (${mode.mode}): ${JSON.stringify(metadata)}`,
);
};
const root = await mkdtemp(join(tmpdir(), "rea-dispatch-"));
try {
const targetPath = join(root, "fixture");
await exec("/usr/bin/xcrun", [
"clang",
"-O2",
"-framework",
"Foundation",
"-Wl,-no_fixup_chains",
fileURLToPath(
new URL("../tests/conformance/native/dispatch.m", import.meta.url),
),
"-o",
targetPath,
]);
const results = [];
for (const variant of ["symbols", "stripped"]) {
if (variant === "stripped")
await exec("/usr/bin/strip", ["-x", targetPath]);
const target = await parseBinaryTarget(targetPath);
if (!target.ok) throw target.error;
const observation = await inspectAppleDispatchMetadata(target.value, 5000);
const metadata = observation.result;
if (
!metadata.objc_dispatch_implementations.some(
(item) =>
item.class_name === "ReaDispatchFixture" &&
item.selector === "performAction:" &&
item.implementation_address !== null,
) ||
!metadata.objc_dispatch_implementations.some(
(item) =>
item.selector === "fixtureVersion" && item.method_type === "class",
) ||
!metadata.objc_ivars.some(
(item) => item.name === "state" && item.offset !== null,
) ||
!metadata.objc_protocol_records.some(
(protocol) =>
protocol.name === "ReaDispatchProtocol" &&
protocol.methods.some(
(method) => method.selector === "performAction:",
) &&
protocol.optional_methods.some(
(method) => method.selector === "optionalFixtureValue",
),
)
)
throw new Error(
`Apple metadata fixture drifted: ${JSON.stringify(observation)}`,
);
results.push({
variant,
target_sha256: target.value.sha256,
implementations: metadata.objc_dispatch_implementations.length,
ivars: metadata.objc_ivars.length,
coverage: metadata.coverage,
});
}
const swiftPath = join(root, "libReaWitnessFixture.dylib");
await exec("/usr/bin/xcrun", [
"swiftc",
"-emit-library",
"-O",
"-Xlinker",
"-no_fixup_chains",
fileURLToPath(
new URL("../tests/conformance/native/dispatch.swift", import.meta.url),
),
"-o",
swiftPath,
]);
for (const variant of ["swift-symbols", "swift-stripped"]) {
if (variant === "swift-stripped")
await exec("/usr/bin/strip", ["-x", swiftPath]);
const target = await parseBinaryTarget(swiftPath);
if (!target.ok) throw target.error;
const observation = await inspectAppleDispatchMetadata(target.value, 5000);
const metadata = observation.result;
if (
!metadata.swift_dispatch_slots.some(
(slot) =>
slot.table_kind === "class_vtable" &&
slot.owner === "ReaVtableFixture" &&
slot.decode.status === "decoded",
)
)
throw new Error(
`Swift class vtable fixture unavailable: ${JSON.stringify(metadata.coverage)}`,
);
if (
!metadata.swift_conformances.some(
(item) =>
item.type_name === "ReaWitnessFixture" &&
item.protocol_name === "ReaWitnessFixtureProtocol",
) ||
!metadata.swift_dispatch_slots.some(
(item) =>
item.owner === "ReaWitnessFixture: ReaWitnessFixtureProtocol" &&
item.implementation_address !== null,
)
)
throw new Error(
`Swift metadata fixture drifted: ${JSON.stringify(observation)}`,
);
results.push({
variant,
target_sha256: target.value.sha256,
conformances: metadata.swift_conformances.length,
slots: metadata.swift_dispatch_slots.length,
coverage: metadata.coverage,
});
for (const mode of LINK_MODES) {
const objcPath = join(root, `fixture-${mode.mode}`);
await exec("/usr/bin/xcrun", [
"clang",
"-O2",
"-framework",
"Foundation",
...mode.flags,
source("dispatch.m"),
"-o",
objcPath,
]);
for (const variant of ["symbols", "stripped"]) {
if (variant === "stripped")
await exec("/usr/bin/strip", ["-x", objcPath]);
const { target, metadata } = await inspect(objcPath);
checkObjc(metadata, mode);
results.push({
variant: `objc-${mode.mode}-${variant}`,
target_sha256: target.sha256,
classes: metadata.objc_classes.length,
categories: metadata.objc_categories.length,
implementations: metadata.objc_dispatch_implementations.length,
fixups: fixupCoverage(metadata)?.reason,
});
}
if (!mode.swift) continue;
const swiftPath = join(root, `libReaWitnessFixture-${mode.mode}.dylib`);
await exec("/usr/bin/xcrun", [
"swiftc",
"-emit-library",
"-O",
...mode.flags
.map((flag) => flag.replace(/^-Wl,/u, "-Xlinker\0"))
.flatMap((flag) => flag.split("\0")),
source("dispatch.swift"),
"-o",
swiftPath,
]);
for (const variant of ["symbols", "stripped"]) {
if (variant === "stripped")
await exec("/usr/bin/strip", ["-x", swiftPath]);
const { target, metadata } = await inspect(swiftPath);
checkSwift(metadata, mode);
results.push({
variant: `swift-${mode.mode}-${variant}`,
target_sha256: target.sha256,
conformances: metadata.swift_conformances.length,
slots: metadata.swift_dispatch_slots.length,
fixups: fixupCoverage(metadata)?.reason,
});
}
}
process.stdout.write(
`${JSON.stringify({ ok: true, target_executed: false, fixtures: results })}\n`,
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env node
import { access } from "node:fs/promises";
import { verifyBrowserModules } from "./lib/browser-module-e2e.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
const executable = process.env.REA_BROWSER_EXECUTABLE;
if (!executable)
throw new Error(
"verify:browser:modules requires REA_BROWSER_EXECUTABLE for caller-supplied Chromium",
);
await access(executable);
const run = createVerifierRun();
const proof = await verifyBrowserModules(executable, process.argv[2]);
process.stdout.write(
`${JSON.stringify({ ...proof, verifier_run: await completeVerifierRun(run), verified: true })}\n`,
);
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env node
import { access } from "node:fs/promises";
import { isAbsolute } from "node:path";
import { verifyBrowserSourceMap } from "./lib/browser-source-map-e2e.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
const executable = process.env.REA_BROWSER_EXECUTABLE;
if (!executable || !isAbsolute(executable))
throw new Error(
"verify:browser:source-maps requires absolute REA_BROWSER_EXECUTABLE pointing to caller-supplied Chromium",
);
await access(executable);
const run = createVerifierRun();
const proof = await verifyBrowserSourceMap(executable, process.argv[2]);
process.stdout.write(
`${JSON.stringify({ ...proof, verifier_run: await completeVerifierRun(run), verified: true })}\n`,
);
+34 -1
View File
@@ -117,8 +117,41 @@ try {
).length < 2
)
throw new Error("Real shared object identity was not preserved");
const uidRoot = join(root, "uid-root.plist");
await promisify(execFile)("/usr/bin/xcrun", [
"swift",
"-module-cache-path",
join(root, "modules"),
fileURLToPath(
new URL(
"../tests/conformance/native/keyed-archive-uid-root.swift",
import.meta.url,
),
),
uidRoot,
]);
const uidRootXml = join(root, "uid-root.xml.plist");
await promisify(execFile)("/usr/bin/plutil", [
"-convert",
"xml1",
"-o",
uidRootXml,
uidRoot,
]);
for (const path of [uidRoot, uidRootXml]) {
const uidGraph = await artifactCli("inspect-keyed-archive", path);
assert.deepEqual(
uidGraph.references.map(({ source, path, target, status }) => ({
source,
path,
target,
status,
})),
[{ source: null, path: ["UID"], target: 1, status: "resolved" }],
);
}
process.stdout.write(
`${JSON.stringify({ ok: true, mocked: false, cli: true, stdio_mcp: true, xml_golden: true, format: graph.archive_format, objects: graph.total_objects, references: graph.total_references, shared_identity: true, cyclic_identity: true, target_classes_instantiated_by_reader: false })}\n`,
`${JSON.stringify({ ok: true, mocked: false, cli: true, stdio_mcp: true, xml_golden: true, uid_named_root: true, format: graph.archive_format, objects: graph.total_objects, references: graph.total_references, shared_identity: true, cyclic_identity: true, target_classes_instantiated_by_reader: false })}\n`,
);
} finally {
await rm(root, { recursive: true, force: true });
+167
View File
@@ -0,0 +1,167 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import {
artifactCliEvidence,
artifactMcpResult,
withArtifactMcp,
} from "./lib/artifact-e2e.mjs";
import {
buildMacosBundleFixture,
preflightMacosBundleFixture,
} from "./lib/macos-bundle-fixture.mjs";
const exec = promisify(execFile);
await preflightMacosBundleFixture();
const root = await mkdtemp(join(tmpdir(), "rea-macos-bundle-"));
/** Bundle roles relative to the application root, independent of the container. */
const anatomy = (projection, applicationRoot) => {
const relative = (path) =>
applicationRoot === "."
? path
: path === applicationRoot
? "."
: path.slice(applicationRoot.length + 1);
const paths = (components) =>
components.map(({ path }) => relative(path)).sort();
return {
bundles: projection.bundles.map((bundle) => ({
path: relative(bundle.path),
role: bundle.role,
layout: bundle.layout,
parent: bundle.parent_path === null ? null : relative(bundle.parent_path),
info_plist: bundle.info_plist_path && relative(bundle.info_plist_path),
executables: bundle.executable_candidates.map(relative),
signing: bundle.signing_paths.map(relative),
})),
privileged_helpers: paths(projection.components.privileged_helpers),
launchd_plists: projection.components.launchd_plists
.map(({ path, domain }) => `${domain}:${relative(path)}`)
.sort(),
helpers: paths(projection.components.helpers),
};
};
const FRAMEWORK = "Contents/Frameworks/Core.framework";
const EXPECTED_BUNDLES = [
[".", "application", "macos-deep", null],
[FRAMEWORK, "framework", "versioned-framework", "."],
[
`${FRAMEWORK}/Versions/A/XPCServices/FwSvc.xpc`,
"xpc-service",
"macos-deep",
FRAMEWORK,
],
["Contents/Library/LoginItems/Login.app", "login-item", "macos-deep", "."],
["Contents/PlugIns/Ext.appex", "app-extension", "macos-deep", "."],
["Contents/XPCServices/Svc.xpc", "xpc-service", "macos-deep", "."],
];
try {
const { app } = await buildMacosBundleFixture(root);
const zip = join(root, "MacFixture.zip");
await exec("/usr/bin/ditto", ["-c", "-k", "--keepParent", app, zip]);
const staging = join(root, "dmg-source");
await mkdir(staging);
await exec("/usr/bin/ditto", [app, join(staging, "MacFixture.app")]);
await symlink("/Applications", join(staging, "Applications"));
const dmg = join(root, "MacFixture.dmg");
// The default APFS image attaches as an image disk plus a synthesized
// container, which exercises detach of devices sharing one image.
await exec("/usr/bin/hdiutil", [
"create",
"-quiet",
"-srcfolder",
staging,
"-volname",
"ReaFixture",
"-format",
"UDZO",
dmg,
]);
const containers = [
{ target: app, format: "directory", applicationRoot: "." },
{ target: zip, format: "zip", applicationRoot: "MacFixture.app" },
{
target: dmg,
format: "dmg",
applicationRoot: "MacFixture.dmg/ReaFixture/MacFixture.app",
},
];
const anatomies = [];
for (const { target, format, applicationRoot } of containers) {
const inventory = await artifactCliEvidence("inspect-artifact", target);
const input = join(root, `${format}-projection.json`);
await writeFile(input, JSON.stringify({ inventory_evidence: [inventory] }));
const evidence = await artifactCliEvidence(
"project-apple-application-graph",
input,
);
const projection = evidence.normalized_result;
assert.equal(evidence.subject?.format, format);
assert.equal(projection.root_format, format);
assert.deepEqual(projection.platforms, ["macos"]);
assert.deepEqual(projection.application_roots, [applicationRoot]);
assert.equal(projection.coverage.status, "complete-within-inventory");
const observed = anatomy(projection, applicationRoot);
assert.deepEqual(
observed.bundles.map(({ path, role, layout, parent }) => [
path,
role,
layout,
parent,
]),
EXPECTED_BUNDLES,
`${format} bundle roles drifted`,
);
assert.deepEqual(observed.privileged_helpers, [
"Contents/Library/LaunchServices/com.example.rea.helper",
]);
assert.deepEqual(observed.launchd_plists, [
"agent:Contents/Library/LaunchAgents/com.example.rea.agent.plist",
"daemon:Contents/Library/LaunchDaemons/com.example.rea.helper.plist",
]);
assert.deepEqual(observed.helpers, ["Contents/Helpers/rea-tool"]);
assert.ok(
projection.symlinks.some((path) =>
path.endsWith(`${FRAMEWORK}/Versions/Current`),
),
"Versions/Current symlink was not reported",
);
anatomies.push(observed);
if (format === "directory")
await withArtifactMcp(app, async (client) => {
assert.deepEqual(
await artifactMcpResult(client, "project_apple_application_graph", {
inventory_evidence: [inventory],
}),
projection,
);
});
}
assert.deepEqual(anatomies[1], anatomies[0], "ZIP anatomy differs");
assert.deepEqual(anatomies[2], anatomies[0], "DMG anatomy differs");
const { stdout: attached } = await exec("/usr/bin/hdiutil", ["info"]);
assert.ok(!attached.includes(dmg), "DMG remained attached after inventory");
process.stdout.write(
`${JSON.stringify({
ok: true,
mocked: false,
cli: true,
stdio_mcp: true,
containers: containers.map(({ format }) => format),
bundles: EXPECTED_BUNDLES.length,
dmg_detached: true,
})}\n`,
);
} finally {
await rm(root, { recursive: true, force: true });
}
+56
View File
@@ -0,0 +1,56 @@
import { execFile } from "node:child_process";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { promisify } from "node:util";
import { inspectModuleBoundaries } from "./lib/module-boundaries.mjs";
const execute = promisify(execFile);
const root = process.cwd();
const selected = process.argv.slice(2);
for (const file of selected)
if (!file.endsWith(".ts"))
throw new TypeError(`Expected an exact TypeScript source file: ${file}`);
const files =
selected.length > 0
? selected
: (
await execute(
"git",
[
"ls-files",
"-z",
"--cached",
"--others",
"--exclude-standard",
"--",
"src",
],
{ cwd: root, maxBuffer: 8 * 1024 * 1024 },
)
).stdout.split("\0");
let checked = 0;
for (const file of new Set(files.filter((path) => path.endsWith(".ts")))) {
let source;
try {
source = await readFile(resolve(root, file), "utf8");
} catch (cause) {
if (selected.length === 0 && cause.code === "ENOENT") continue;
throw cause;
}
checked += 1;
try {
for (const violation of inspectModuleBoundaries(file, source, root)) {
console.error(
`${violation.file}:${violation.line}: ${violation.boundary}: ${violation.specifier} resolves to ${violation.target}`,
);
process.exitCode = 1;
}
} catch (cause) {
console.error(`Could not inspect ${file}: ${cause.message}`);
process.exitCode = 1;
}
}
if (process.exitCode !== 1)
console.log(`Checked resolved module boundaries in ${checked} source files.`);
+17 -3
View File
@@ -15,6 +15,23 @@ const root = await mkdtemp(join(tmpdir(), "rea-ui-fixture-"));
let child;
let fixturePid;
try {
const childRetrievalTest = join(root, "child-retrieval-test");
await promisify(execFile)("/usr/bin/xcrun", [
"swiftc",
fileURLToPath(
new URL("../bridge/native/NativeUIChildren.swift", import.meta.url),
),
fileURLToPath(
new URL(
"../tests/conformance/native/native-ui-children/main.swift",
import.meta.url,
),
),
"-o",
childRetrievalTest,
]);
await promisify(execFile)(childRetrievalTest, []);
const contents = join(root, "Fixture.app", "Contents");
await mkdir(join(contents, "MacOS"), { recursive: true });
await writeFile(
@@ -82,7 +99,6 @@ try {
"capture_native_ui_scenario",
{
...scope,
restore: "leave-as-is",
screenshot: false,
steps: [
{ kind: "click", path: button.path },
@@ -111,8 +127,6 @@ try {
String(scope.pid),
"--window-id",
String(scope.window_id),
"--restore",
"leave-as-is",
"--steps",
JSON.stringify([
{ kind: "click", path: button.path },
+1 -1
View File
@@ -2,7 +2,7 @@ import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { createEvidence } from "../dist/domain/evidence.js";
import { buildManagedPeFixture } from "./lib/managed-pe-fixture.mjs";
import { buildManagedPeFixture } from "./fixtures/managed/pe.mjs";
import { functionDossier, json, run } from "./lib/verify-package-core.mjs";
const buildManagedFixtures = async (workspace) => {
+12 -2
View File
@@ -37,6 +37,7 @@ import { verifyLargeScreenshotE2e } from "./lib/browser-screenshot-e2e.mjs";
import { verifyPopupEventCoverage } from "./lib/browser-popup-e2e.mjs";
import { verifyBrowserNetworkEvidence } from "./lib/browser-network-e2e.mjs";
import { verifyBrowserScriptExport } from "./lib/browser-script-export-e2e.mjs";
import { verifyBrowserModules } from "./lib/browser-module-e2e.mjs";
import { verifyBrowserDomDestinations } from "./lib/browser-dom-destinations-e2e.mjs";
const REAL_BROWSER_STARTUP_TIMEOUT_MS = 60_000;
@@ -336,6 +337,7 @@ try {
cdp_endpoint: endpoint,
target_id: target,
});
const moduleTrace = await verifyBrowserModules(executable);
process.stdout.write(
`${JSON.stringify({
verifier_run: await completeVerifierRun(verifierRun),
@@ -357,6 +359,7 @@ try {
popupEvents,
networkEvidence,
scriptExport,
moduleTrace,
domDestinations,
browserScenarioCli: true,
browserScenarioAttachCleanup: "disconnected-external",
@@ -413,9 +416,16 @@ async function verifyPageScopedTransport(provider, proxy, origin) {
target_id: target,
observation_ms: 5_000,
}),
{
progress: {
report(event) {
if (event.phase === "browser_observation" && event.completed === 1)
proxy.disconnectClients();
return Promise.resolve();
},
},
},
);
await delay(100);
proxy.disconnectClients();
const disconnected = await disconnectedPromise;
if (
!disconnected.ok ||
+1 -1
View File
@@ -4,7 +4,7 @@ import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { PlaywrightElectronActiveProvider } from "../dist/browser/PlaywrightElectronActiveProvider.js";
import { electronActiveObservationInputSchema } from "../dist/domain/electronActiveObservation.js";
import { electronActiveObservationInputSchema } from "../dist/domain/javascript/electronActiveObservation.js";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
const executable = process.env.REA_ELECTRON_EXECUTABLE;
+106
View File
@@ -0,0 +1,106 @@
"""Check local website references and the sole GitHub Pages publisher."""
from html.parser import HTMLParser
from pathlib import Path
import re
import sys
from urllib.parse import unquote, urlsplit
import xml.etree.ElementTree as ET
class HtmlReferences(HTMLParser):
"""Collect document IDs and references without fetching external URLs."""
def __init__(self):
super().__init__()
self.ids = set()
self.references = []
self.duplicate_ids = []
def handle_starttag(self, tag, attrs):
attributes = dict(attrs)
identifier = attributes.get("id")
if identifier is not None:
if identifier in self.ids:
self.duplicate_ids.append(identifier)
self.ids.add(identifier)
for attribute in ("href", "src"):
reference = attributes.get(attribute)
if reference is not None:
self.references.append(reference)
def check_site(site):
"""Check assets, HTML fragment destinations and SVG syntax."""
pages = {}
errors = []
references = 0
for path in sorted(site.rglob("*.html")):
parser = HtmlReferences()
parser.feed(path.read_text(encoding="utf-8"))
pages[path] = parser
if site / "index.html" not in pages:
errors.append("website/public/index.html is missing")
for path, page in pages.items():
label = path.relative_to(site)
for identifier in page.duplicate_ids:
errors.append(f"{label}: duplicate ID {identifier!r}")
for reference in page.references:
url = urlsplit(reference)
if url.scheme or url.netloc:
continue
references += 1
if url.path.startswith("/"):
errors.append(f"{label}: use a relative site reference: {reference}")
continue
target = (path.parent / unquote(url.path)).resolve() if url.path else path
if not target.is_relative_to(site):
errors.append(f"{label}: reference leaves the website: {reference}")
continue
if target.is_dir():
target /= "index.html"
if not target.is_file():
errors.append(f"{label}: missing local target: {reference}")
elif target in pages and url.fragment and unquote(url.fragment) not in pages[target].ids:
errors.append(f"{label}: missing HTML fragment: {reference}")
for path in sorted(site.rglob("*.svg")):
try:
ET.parse(path)
except ET.ParseError as error:
errors.append(f"{path.relative_to(site)}: invalid SVG XML: {error}")
return len(pages), references, errors
def check_publisher(root):
"""Detect a second Pages action that could replace the website."""
expected = root / ".github/workflows/website-pages.yml"
workflows = root / ".github/workflows"
deploy_action = re.compile(r"^\s*-?\s*uses:\s*['\"]?actions/deploy-pages@", re.MULTILINE)
publishers = [
path
for path in sorted(workflows.iterdir())
if path.suffix in (".yml", ".yaml") and deploy_action.search(path.read_text(encoding="utf-8"))
]
if publishers != [expected]:
actual = ", ".join(str(path.relative_to(root)) for path in publishers) or "none"
return [f"Pages must have one publisher, website-pages.yml; found: {actual}"]
return []
def main():
"""Run the same checks locally, on website PRs and before publishing."""
root = Path(__file__).resolve().parent.parent
pages, references, errors = check_site(root / "website/public")
errors.extend(check_publisher(root))
if errors:
print("Website checks failed:\n" + "\n".join(errors), file=sys.stderr)
return 1
print(f"Verified {pages} HTML pages, {references} local references and SVG XML.")
print("website-pages.yml is the sole GitHub Pages publisher.")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -6,18 +6,19 @@ import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../dist/domain/evidence.js";
import { hashAndroidFile } from "../dist/android/AndroidTargetSnapshot.js";
import { JADX_RELEASE } from "../dist/android/JadxRelease.js";
import fixture from "./fixtures/android-apidemos.json" with { type: "json" };
import { verifyAndroidCliCancellation } from "./lib/android-cli-cancellation.mjs";
import { parseEvidence } from "../../../dist/domain/evidence.js";
import { hashAndroidFile } from "../../../dist/android/AndroidTargetSnapshot.js";
import { JADX_RELEASE } from "../../../dist/android/JadxRelease.js";
import fixture from "../../fixtures/android/apidemos.json" with { type: "json" };
import { verifyAndroidCliCancellation } from "./cli-cancellation.mjs";
import { verifyAndroidMetadata } from "./metadata.mjs";
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
const apk = process.env.REA_ANDROID_TEST_APK;
const jar = process.env.REA_JADX_MCP_JAR;
if (apk === undefined || jar === undefined)
throw new Error(
"verify:android requires REA_ANDROID_TEST_APK and REA_JADX_MCP_JAR. Run node scripts/fetch-android-fixtures.mjs and select the downloaded files; see docs/android-analysis.md.",
"verify:android requires REA_ANDROID_TEST_APK and REA_JADX_MCP_JAR. Run npm run fixtures:android and select the downloaded files; see docs/android-analysis.md.",
);
await access(apk);
await access(jar);
@@ -41,6 +42,14 @@ const entrypoint =
process.env.REA_ANDROID_ENTRYPOINT ?? resolve(repository, "scripts/rea.mjs");
const environment = { ...process.env, REA_JADX_MCP_JAR: resolve(jar) };
const path = resolve(apk);
await verifyAndroidMetadata({
java,
jar: resolve(jar),
apk: path,
repository,
execute,
bridge: resolve(dirname(entrypoint), "../bridge/android/ReaJadxBridge.java"),
});
const tasks = [
{
name: "inspect_android_package",
@@ -212,6 +221,8 @@ const report = {
original_apk_unchanged: true,
apk_executed: false,
cli_sigterm_cleanup: true,
metadata_without_code_generation: true,
stable_overload_selection: true,
};
if (process.env.REA_ANDROID_REPORT !== undefined)
await writeFile(
@@ -3,7 +3,7 @@ import { spawn } from "node:child_process";
import {
cleanupOwnedProcessGroup,
readProcessRunId,
} from "../../dist/process/ProcessOwnership.js";
} from "../../../dist/process/ProcessOwnership.js";
/** Exercise a real CLI signal while the owned Java engine is still running. */
export const verifyAndroidCliCancellation = async ({
@@ -47,7 +47,7 @@ export const verifyAndroidCliCancellation = async ({
entry !== null &&
Number(entry[2]) === child.pid &&
entry[4].includes("engine.jar") &&
entry[4].includes("-Xmx512m")
entry[4].includes("ReaJadxBridge.java")
) {
group = Number(entry[3]);
const runId = await readProcessRunId(Number(entry[1]));
+64
View File
@@ -0,0 +1,64 @@
import assert from "node:assert/strict";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { delimiter, join, resolve } from "node:path";
/** Prove cold metadata stays unmaterialized and overload identities survive code generation. */
export const verifyAndroidMetadata = async ({
java,
jar,
apk,
bridge,
repository,
execute,
}) => {
const javac =
process.env.JAVA_HOME === undefined
? "javac"
: resolve(process.env.JAVA_HOME, "bin/javac");
try {
await execute(javac, ["-version"], { timeout: 10_000 });
} catch (cause) {
throw new Error(
"verify:android metadata requires the full selected JDK, including javac",
{ cause },
);
}
const classes = await mkdtemp(join(tmpdir(), "rea-android-metadata-"));
try {
await execute(
javac,
[
"-classpath",
jar,
"-d",
classes,
bridge,
resolve(
repository,
"tests/conformance/android/ReaJadxMetadataRegression.java",
),
],
{ timeout: 30_000 },
);
const result = await execute(
java,
[
"-classpath",
`${classes}${delimiter}${jar}`,
"ReaJadxMetadataRegression",
apk,
],
{ timeout: 120_000, maxBuffer: 8 * 1024 * 1024 },
);
assert.match(
result.stdout,
/METADATA_REGRESSION_PASS classes=\d+ overloads=3/u,
);
console.log(
"PASS real cold metadata, full generic types and stable overloaded method selection",
);
} finally {
await rm(classes, { recursive: true, force: true });
}
};
@@ -14,12 +14,12 @@ import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { firmwareResultSchemas } from "../dist/domain/firmwareAnalysis.js";
import { parseEvidence } from "../dist/domain/evidence.js";
import { hashFirmwareFile } from "../dist/firmware/FirmwareFiles.js";
import { firmwareResultSchemas } from "../../../dist/domain/firmware/firmwareAnalysis.js";
import { parseEvidence } from "../../../dist/domain/evidence.js";
import { hashFirmwareFile } from "../../../dist/firmware/FirmwareFiles.js";
const exec = promisify(execFile);
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
const entrypoint = resolve(
process.env.REA_FIRMWARE_TEST_ENTRYPOINT ??
join(repository, "scripts/rea.mjs"),
@@ -11,7 +11,7 @@ import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
const execute = promisify(execFile);
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
const root = await realpath(await mkdtemp(join(tmpdir(), "rea-inspector-")));
const environment = {
...process.env,
@@ -43,9 +43,7 @@ try {
for (const name of names) {
const targetPath =
name === "node-target.mjs"
? await realpath(
join(repository, "tests/conformance/runtime-inspector", name),
)
? await realpath(join(repository, "tests/conformance/inspector", name))
: join(root, name);
if (name !== "node-target.mjs")
await writeFile(targetPath, "setInterval(() => {}, 1000);\n");
@@ -3,16 +3,19 @@
import { realpath } from "node:fs/promises";
import { parseArgs } from "node:util";
import { analyzeJavaScriptApplication } from "../dist/application/JavaScriptApplicationService.js";
import { analyzeJavaScriptApplication } from "../../../dist/application/javascript/JavaScriptApplicationService.js";
import {
compareApplicationVersionsEvidence,
compareJavaScriptExportShapesEvidence,
traceApplicationFeatureEvidence,
} from "../dist/application/JavaScriptApplicationWorkflowService.js";
import { applicationVersionComparisonResultSchema } from "../dist/domain/javascriptApplicationVersionComparisonSchemas.js";
import { applicationFeatureTraceResultSchema } from "../dist/domain/javascriptFeatureTraceSchemas.js";
import { javaScriptExportShapeComparisonResultSchema } from "../dist/domain/javascriptExportShapeComparisonSchemas.js";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
} from "../../../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
import { applicationVersionComparisonResultSchema } from "../../../dist/domain/javascript/javascriptApplicationVersionComparisonSchemas.js";
import { applicationFeatureTraceResultSchema } from "../../../dist/domain/javascript/javascriptFeatureTraceSchemas.js";
import { javaScriptExportShapeComparisonResultSchema } from "../../../dist/domain/javascript/javascriptExportShapeComparisonSchemas.js";
import {
completeVerifierRun,
createVerifierRun,
} from "../../lib/verifier-run.mjs";
const verifierRun = createVerifierRun();
@@ -4,8 +4,11 @@ import assert from "node:assert/strict";
import { constants } from "node:buffer";
import { createHash } from "node:crypto";
import { canonicalDigest } from "../dist/domain/comparisonSemantics.js";
import { createEvidence, parseEvidence } from "../dist/domain/evidence.js";
import { canonicalDigest } from "../../../dist/domain/comparisonSemantics.js";
import {
createEvidence,
parseEvidence,
} from "../../../dist/domain/evidence.js";
// This opt-in check crosses the actual engine string limit; keep it outside
// routine source tests. Build first, then run this script with the local Node.
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../../../dist/domain/evidence.js";
import { javascriptRecoveryResultSchema } from "../../../dist/domain/javascript/javascriptRecovery.js";
import { buildRecoveryFixtures } from "../../fixtures/javascript-recovery/build.mjs";
import { mcpTextValue } from "../../lib/mcp-verifier-results.mjs";
const OFFICIAL_BINARY_SHA256 =
"e8c7ca052974604197389f6ace7464e4dd24dce64e2730971521bd6606ec9c9e";
if (!process.env.REA_WAKARU_COMMAND)
throw new Error(
"Provide REA_WAKARU_COMMAND for the official Wakaru 1.13.0 Linux x64 binary",
);
const entrypoint =
process.argv[2] ?? fileURLToPath(new URL("../../rea.mjs", import.meta.url));
const root = await mkdtemp(join(tmpdir(), "rea-real-recovery-"));
const environment = { ...process.env, REA_LOG_LEVEL: "silent" };
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env: environment,
stderr: "pipe",
});
const client = new Client({ name: "real-recovery", version: "1" });
const run = promisify(execFile);
const cli = async (...args) => {
const { stdout } = await run(
process.execPath,
[entrypoint, ...args, "--json"],
{ env: environment, timeout: 150000, maxBuffer: 32 * 1024 * 1024 },
);
return parseEvidence(JSON.parse(stdout));
};
const call = async (name, args) => {
const result = await client.callTool({ name, arguments: args }, undefined, {
timeout: 150000,
});
assert.notEqual(result.isError, true, mcpTextValue(result));
return parseEvidence(JSON.parse(mcpTextValue(result)).evidence);
};
const fingerprint = (bytes) => createHash("sha256").update(bytes).digest("hex");
const counts = [];
try {
const fixtures = await buildRecoveryFixtures(
root,
process.env.REA_JAVASCRIPT_FIXTURE_TOOLS,
);
await client.connect(transport);
for (const fixture of fixtures.files) {
for (const surface of ["cli", "stdio_mcp"]) {
const output = join(root, `${fixture.name}-${surface}`);
const flags =
fixture.options === undefined
? []
: [
"--extraction-mode",
fixture.options.extraction_mode,
"--rewrite-level",
fixture.options.rewrite_level,
];
const evidence =
surface === "cli"
? await cli(
"recover-javascript-sources",
fixture.path,
output,
...flags,
)
: await call("recover_javascript_sources", {
path: fixture.path,
output_directory: output,
...fixture.options,
});
const result = javascriptRecoveryResultSchema.parse(
evidence.normalized_result,
);
await assertRecoveredArtifacts(evidence, result, fixture);
const analysis =
surface === "cli"
? await cli(
"analyze-javascript-application",
result.analysis_input.input_path,
)
: await call("analyze_javascript_application", result.analysis_input);
assert.equal(analysis.normalized_result.statistics.parse_failures, 0);
assert.equal(
analysis.normalized_result.statistics.parsed_javascript_files,
result.modules.length,
);
const recoveredEntry =
result.modules.find(
(module) => module.reported_filename === "entry.js",
) ?? result.modules[0];
assert.ok(recoveredEntry);
const oracle =
'await import((await import("node:url")).pathToFileURL(process.argv[1])); const f=globalThis.recoveryFixture; console.log(JSON.stringify([f.greet(null),f.greet("REA"),f.total([1,2,3])]));';
for (const selected of fixture.options?.extraction_mode === "inspection"
? []
: [fixture.path, recoveredEntry.artifact.path]) {
const execution = await run(
process.execPath,
["--input-type=module", "-e", oracle, selected],
{ env: environment, timeout: 10000 },
);
assert.deepEqual(JSON.parse(execution.stdout), [
"Hello world",
"Hello REA",
6,
]);
}
counts.push({
fixture: fixture.name,
surface,
modules: result.modules.length,
maps: result.modules.filter((module) => module.source_map !== null)
.length,
finite_fixture_oracle:
fixture.options?.extraction_mode !== "inspection",
});
}
}
console.log(
JSON.stringify({
engine: "wakaru 1.13.0",
toolchains: fixtures.toolchains,
results: counts,
verified: true,
}),
);
} finally {
await client.close();
await transport.close();
await rm(root, { recursive: true, force: true });
}
async function assertRecoveredArtifacts(evidence, result, fixture) {
assert.deepEqual(result.options, {
extraction_mode: "structural",
rewrite_level: "standard",
...fixture.options,
});
if (fixture.options?.extraction_mode === "inspection")
assert.equal(result.reported_safety, "inspection-only");
assert.equal(result.engine.executable.sha256, OFFICIAL_BINARY_SHA256);
assert.equal(result.engine.executed_source_revision, null);
assert.equal(evidence.confidence, "derived");
assert.equal(result.runtime_equivalence, "unknown");
assert.equal(result.status, "complete");
assert.equal(result.reported_format, fixture.expectedFormat);
assert.equal(result.modules.length, fixture.expectedModules);
assert.equal(result.source.sha256, fingerprint(await readFile(fixture.path)));
for (const file of [
result.source.published_copy,
result.report,
result.provenance,
result.manifest,
...result.modules.flatMap((module) => [
module.artifact,
...(module.source_map ? [module.source_map] : []),
]),
]) {
const bytes = await readFile(file.path);
assert.equal(bytes.length, file.bytes);
assert.equal(fingerprint(bytes), file.sha256);
}
for (const module of result.modules) {
assert.equal(module.provenance.original_sha256, result.source.sha256);
assert.equal(module.provenance.reported_input, result.source.snapshot_path);
for (const range of module.provenance.byte_ranges)
assert.ok(range.start <= range.end && range.end <= result.source.bytes);
if (module.source_map) {
const map = JSON.parse(await readFile(module.source_map.path, "utf8"));
for (const source of map.sources)
assert.equal(
resolve(dirname(module.source_map.path), source),
result.source.published_copy.path,
);
}
}
}
@@ -3,9 +3,12 @@
import { readFile, realpath } from "node:fs/promises";
import { parseArgs } from "node:util";
import { analyzeJavaScriptApplication } from "../dist/application/JavaScriptApplicationService.js";
import { reconcileJavaScriptRuntimeEvidence } from "../dist/application/JavaScriptRuntimeReconciliationService.js";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
import { analyzeJavaScriptApplication } from "../../../dist/application/javascript/JavaScriptApplicationService.js";
import { reconcileJavaScriptRuntimeEvidence } from "../../../dist/application/javascript/JavaScriptRuntimeReconciliationService.js";
import {
completeVerifierRun,
createVerifierRun,
} from "../../lib/verifier-run.mjs";
const verifierRun = createVerifierRun();
@@ -1,4 +1,4 @@
import { createEvidence } from "../../dist/domain/evidence.js";
import { createEvidence } from "../../../dist/domain/evidence.js";
const COMPLETION_PROVIDER = {
id: "rea-managed-conformance",
@@ -3,21 +3,21 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import assert from "node:assert/strict";
import { inspectManagedArtifactBytes } from "../dist/dotnet/ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "../dist/dotnet/ManagedMemberInspector.js";
import { inspectManagedNativeBoundariesBytes } from "../dist/dotnet/ManagedNativeBoundaryInspector.js";
import { compareManagedMemberPaths } from "../dist/application/ManagedMemberComparisonService.js";
import { verifyManagedNativeBoundariesEvidence } from "../dist/application/ManagedNativeVerificationService.js";
import { importManagedReconstructionEvidence } from "../dist/application/ManagedReconstructionService.js";
import { projectManagedApplicationGraphEvidence } from "../dist/application/ManagedApplicationGraphService.js";
import { traceApplicationFeatureEvidence } from "../dist/application/JavaScriptApplicationWorkflowService.js";
import { MANAGED_STATIC_PROVIDER } from "../dist/application/InvestigationProviders.js";
import { createEvidence } from "../dist/domain/evidence.js";
import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "../../../dist/dotnet/ManagedMemberInspector.js";
import { inspectManagedNativeBoundariesBytes } from "../../../dist/dotnet/ManagedNativeBoundaryInspector.js";
import { compareManagedMemberPaths } from "../../../dist/application/managed/ManagedMemberComparisonService.js";
import { verifyManagedNativeBoundariesEvidence } from "../../../dist/application/managed/ManagedNativeVerificationService.js";
import { importManagedReconstructionEvidence } from "../../../dist/application/managed/ManagedReconstructionService.js";
import { projectManagedApplicationGraphEvidence } from "../../../dist/application/managed/ManagedApplicationGraphService.js";
import { traceApplicationFeatureEvidence } from "../../../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
import { MANAGED_STATIC_PROVIDER } from "../../../dist/application/InvestigationProviders.js";
import { createEvidence } from "../../../dist/domain/evidence.js";
import {
alternateMvid,
buildNativePeFixture,
} from "./lib/managed-pe-fixture.mjs";
import { createManagedConformanceSupport } from "./lib/managed-conformance-support.mjs";
} from "../../fixtures/managed/pe.mjs";
import { createManagedConformanceSupport } from "./support.mjs";
import {
comparisonLimits,
defaultIlBody,
@@ -25,9 +25,12 @@ import {
inspectionLimits,
memberLimits,
nativeBoundaryLimits,
} from "./lib/managed-conformance-config.mjs";
import { createManagedCompletionReport } from "./lib/managed-completion-report.mjs";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
} from "./config.mjs";
import { createManagedCompletionReport } from "./completion-report.mjs";
import {
completeVerifierRun,
createVerifierRun,
} from "../../lib/verifier-run.mjs";
const verifierRun = createVerifierRun();
const workspace = await mkdtemp(join(tmpdir(), "rea-managed-conformance-"));
@@ -1,18 +1,18 @@
import { basename, dirname, isAbsolute, resolve } from "node:path";
import { readFile } from "node:fs/promises";
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
import { inspectManagedArtifactBytes } from "../../dist/dotnet/ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "../../dist/dotnet/ManagedMemberInspector.js";
import { projectManagedApplicationGraphEvidence } from "../../dist/application/ManagedApplicationGraphService.js";
import { traceApplicationFeatureEvidence } from "../../dist/application/JavaScriptApplicationWorkflowService.js";
import { createEvidence } from "../../dist/domain/evidence.js";
import { MANAGED_STATIC_PROVIDER } from "../../dist/application/InvestigationProviders.js";
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "../../../dist/dotnet/ManagedMemberInspector.js";
import { projectManagedApplicationGraphEvidence } from "../../../dist/application/managed/ManagedApplicationGraphService.js";
import { traceApplicationFeatureEvidence } from "../../../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
import { createEvidence } from "../../../dist/domain/evidence.js";
import { MANAGED_STATIC_PROVIDER } from "../../../dist/application/InvestigationProviders.js";
import {
ensure,
methodTokenRow,
parseManagedAppManifest,
} from "./managed-conformance-manifest-parser.mjs";
} from "./manifest-parser.mjs";
export const createManagedManifestVerifier = (context) => ({
verifyManagedAppManifest: (rawManifest, manifestPath) =>
@@ -5,12 +5,12 @@ import { basename, isAbsolute, join, resolve } from "node:path";
import assert from "node:assert/strict";
import { promisify } from "node:util";
import { inspectManagedArtifactBytes } from "../../dist/dotnet/ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "../../dist/dotnet/ManagedMemberInspector.js";
import { importManagedReconstructionEvidence } from "../../dist/application/ManagedReconstructionService.js";
import { createEvidence } from "../../dist/domain/evidence.js";
import { MANAGED_STATIC_PROVIDER } from "../../dist/application/InvestigationProviders.js";
import { createManagedManifestVerifier } from "./managed-conformance-manifest.mjs";
import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "../../../dist/dotnet/ManagedMemberInspector.js";
import { importManagedReconstructionEvidence } from "../../../dist/application/managed/ManagedReconstructionService.js";
import { createEvidence } from "../../../dist/domain/evidence.js";
import { MANAGED_STATIC_PROVIDER } from "../../../dist/application/InvestigationProviders.js";
import { createManagedManifestVerifier } from "./manifest.mjs";
const execFileAsync = promisify(execFile);
@@ -2,9 +2,9 @@ import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
import { buildManagedPeFixture } from "./managed-pe-fixture.mjs";
import { createManagedConformanceOracleSupport } from "./managed-conformance-oracles.mjs";
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
import { buildManagedPeFixture } from "../../fixtures/managed/pe.mjs";
import { createManagedConformanceOracleSupport } from "./oracles.mjs";
export const createManagedConformanceSupport = (context) => {
const fixtureBytes = async (name, bytes) => {

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