Compare commits

...
Author SHA1 Message Date
Cursor Agent 5bd5d5fb46 chore: add Cloud Agent environment install
Save the tested Node 24.18.0 install so Cloud Agents read it from the repository.
2026-10-07 09:07:46 +00:00
Cursor Agent 51539a53ff docs: note Cloud Agent Node.js toolchain
Record the pinned Node 24.18.0 setup and that Hopper, Ghidra, and IDA stay optional for JavaScript analysis and deterministic tests.
2026-10-07 03:36:23 +00:00
morlutoandGitHub ef535b81fe refactor(domain): extract artifact path syntax helpers (#830)
Move hasScheme/stripQueryAndFragment/admitsCanonicalPathSyntax
plus looksExternal into domain/artifactPathSyntax with the
deliberate policy-divergence table (artifact root vs allowedOrigins
vs exact-origin fetch). Adopt in JavaScriptArtifactPathResolution
and the Electron native specifier check with characterization
tests. Policy unification tracked in #829.
2026-10-07 11:28:28 +08:00
morlutoandGitHub 64ebdf4501 refactor(domain): extract source-map envelope walk and disambiguate stringValue (#828)
Move the indexed sections walk shared by the browser fetcher and
the artifact reader into domain/sourceMapEnvelope with the
fetchers stricter offset validation as an option; both validators
keep their leaf strictness. Rename the browser CDP stringValue to
cdpStringValue across its 18 files, fold the fingerprint duplicate
into the static helper, and rename the version-keys shadow to
nonEmptyString.
2026-10-07 11:23:37 +08:00
N0zoM1z0andGitHub 6dd719eaf6 fix(browser): preserve form destinations and satisfies mutation uncertainty (#825)
Resolve empty form destinations from the captured document URL while preserving
base-relative links and Chrome's form/control whitespace distinction. Follow
satisfies wrappers when propagating explicit member mutation paths.

Add fail-before regressions and real-browser CLI/MCP verification.

Fixes #820

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-07 11:22:03 +08:00
N0zoM1z0andGitHub 3ed59b2812 refactor: share Android and firmware provider factories (#826) 2026-10-07 11:20:26 +08:00
morlutoandGitHub 764ff05231 refactor(domain): consolidate property-key extraction behind semanticStaticPropertyKey (#823)
Inline the private memberKey/propertyKey wrappers in semantic
MemberMutations/Projection/Values to direct canonical calls.
Migrate the electron Browser/Values member keys off legacy
propertyName (provably identical: explicit computed guard retained).
Delegate bundle calleeName/endpointArgument/stringArgument to the
static helpers, adding loadURL endpoint parity with static analysis.

Bundle regressions pin nested-dynamic-receiver and loadURL parity.
Remaining computed-identifier forks in electron Native/Ipc tracked
in #822.
2026-10-07 11:13:59 +08:00
N0zoM1z0andGitHub 75f50d86f6 refactor: add incremental capability migration safeguards (#819) 2026-10-07 10:56:06 +08:00
6caa38a0ca fix(javascript): keep mutated object initializer values unknown (#752)
* fix(javascript): keep mutated object initializer values unknown

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

* fix(javascript): retain copied slots while following nested mutations

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

* fix(javascript): retain facts for unchanged object properties

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

* docs(javascript): state untracked mutation channels

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

---------

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-07 10:51:44 +08:00
Rudy Mizrahi CelekliandGitHub 0374865836 fix(cli): honor the selected native plist path (#773)
* fix(cli): honor the selected native plist path

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

* ci: run native plist selection regressions on macOS

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

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-07 10:51:33 +08:00
Rudy Mizrahi CelekliandGitHub e9fac56232 fix(cli): align structured output guard with native flags (#777)
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-07 10:51:21 +08:00
morlutoandGitHub 5a0e0ba3b8 fix: address review nits from batch integration follow-ups (#818)
- browser: reject empty Referrer-Policy tokens structurally while
  still skipping them per W3 fallback semantics.
- browser: document that WebMCP keys cannot collapse since unknown
  frames are excluded before keying.
- ghidra: split NativeAOT non-recovery diagnostics into
  reason-missing vs payload-present variants with exact assertions.
- reference: lock secret-patterned exclusion before failure
  projection so failure messages cannot carry secret paths.
- doctor: lock direct-launcher Codex timeout ordering both ways.
- native: note plutil -n macOS 12+ requirement; add fat-output
  control guarding the thin pre-scan.
- reconstruction: multi-unknown limitation coverage via shared helper.

Tracks #817 for the remaining smol-toml advisory.
2026-10-07 10:49:23 +08:00
3af3811ab0 fix: integrate 24 reviewed browser/native/semantics corrections (#815)
* fix(reference): resolve emitted extensions to TypeScript source

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

* test(reference): retain legitimate declaration import counterparts

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

* fix(mcp): recognize implicit form elicitation capability

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

* fix(ghidra): reject contradictory NativeAOT reports

* fix(native): preserve thin architectures across echoed filename lines

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

* fix(native): preserve app bundle executable filename whitespace

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

* fix(doctor): validate Node-wrapped client policy

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

* fix(reference): preserve observed entry failure reasons

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

* fix(artifacts): verify opaque bytes before reusing inventory identities

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

* fix(browser): resolve DOM destinations against document base URL

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

* fix: recover serialized native NIB view hierarchy

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

* ci: exercise native compiled NIB hierarchy regressions

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

* fix(deps): remediate reachable archive-parsing advisories

Two high-severity advisories are reachable from untrusted target content:

- @xmldom/xmldom 0.9.10 (via plist) parses XML plist bytes from analyzed
  app bundles in the keyed-archive reader. Refreshed to 0.9.12.
- brace-expansion 5.0.7 (via @electron/asar -> minimatch) is reached while
  analyzing untrusted ASAR archives. Refreshed to 5.0.12.

Both are in-range patch updates (plist declares ^0.9.10, minimatch declares
^5.0.5), so no source or API change is required and no direct dependency
declaration moves. Lockfile-only change.

The remaining smol-toml advisory is not included: it parses operator-owned
client configuration rather than untrusted target content, and its fix moves
outside the declared range and changes leading-BOM handling.

* fix(semantics): link wrapped arrow Promise returns

* fix(reconstruction): preserve unknown graph limitations

* fix(native): preserve exact code signing identifiers

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

* fix(native): recognize only whole otool command headers

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

* fix(native): retain nm-proven literal export names

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

* fix(javascript): project decoded file URL paths

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

* fix: preserve explicit source-relative URL observation spelling

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

* fix(native): project Mach-O evidence into container file offsets

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

* fix(browser): normalize referrer policy fallback lists

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

---------

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: Rudy Celekli <rudy@gradiahq.com>
Co-authored-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: Kaoru0822 <aisakamimaki@gmail.com>
Co-authored-by: DeryFerd <deryferdikao125@gmail.com>
2026-10-07 10:33:10 +08:00
Rudy Mizrahi CelekliandGitHub 2572e1eb2f fix(javascript): project decoded file URL paths (#810)
* fix(javascript): project decoded file URL paths

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

* fix: preserve explicit source-relative URL observation spelling

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

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-07 10:28:48 +08:00
Rudy Mizrahi CelekliandGitHub 24a0bf9a61 fix(semantics): recognize Node builtin default namespaces (#769)
* fix(semantics): recognize Node builtin default namespaces

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

* style(semantics): keep Babel imports before local helpers

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

---------

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
2026-10-07 10:25:59 +08:00
Rudy Mizrahi CelekliandGitHub 630bdafcac fix(semantics): preserve empty string property keys (#764)
Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
2026-10-07 10:23:43 +08:00
Rudy Mizrahi CelekliandGitHub dd3640548f fix(browser): preserve Link header value delimiters (#805)
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-07 10:22:32 +08:00
Rudy Mizrahi CelekliandGitHub e84ad86a75 fix(browser): identify NodeNext TypeScript original source artifacts (#811)
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-07 10:20:51 +08:00
Rudy Mizrahi CelekliandGitHub b3b44d8a2d fix(browser): distinguish WebMCP registration frame owners (#813)
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-07 10:19:49 +08:00
Rudy Mizrahi CelekliandGitHub 02beb74535 fix(browser): distinguish computed call keys from identifiers (#808)
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-07 10:19:44 +08:00
Rudy Mizrahi CelekliandGitHub 21a8bd0ccd fix: capture IndexedDB object-store record fingerprints (#809)
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-07 10:18:03 +08:00
Rudy Mizrahi CelekliandGitHub 78014e3d14 fix(browser): fingerprint cookies at the selected page URL (#763)
Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
2026-10-07 10:11:57 +08:00
N0zoM1z0andGitHub 472b72a530 feat(web): export captured scripts for JavaScript application analysis (#801)
* feat(web): export captured scripts for JavaScript application analysis

Add capture adapters, conservative source path projection, verified exclusive publication and a shared CLI/MCP workflow. Retain source identities, missing content and competing URL versions in a complete manifest for existing static analysis.

Refs: #774
Closes: #799
Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* test(web): include script export in independent effect audit

Register the new contract family in the independent inventory audit and verify the filesystem, session, network and idempotence metadata of the named export contract.

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

* fix(web): preserve capture limitation values in export manifests

Align the export boundary with passive capture’s string contract instead of rejecting an admitted empty limitation. Cover the publisher boundary and regenerate canonical schemas and identities.

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-07 05:24:32 +08:00
N0zoM1z0andGitHub 894c30ef02 fix(ghidra): select exact packaged bridge scripts (#800) 2026-10-07 04:54:52 +08:00
N0zoM1z0andGitHub f9eaf056ef feat(browser): capture correlated network content and lifecycle evidence (#797)
* feat(browser): capture correlated network content and lifecycle evidence

Keep request identities and redirect relationships across ordered scenario events.
Capture selected browser-exposed bytes and duplicate headers through modular
readers, settle bounded reads before cleanup, and preserve unavailable states.
Keep content-selection mismatches and legacy network coverage unknown during
comparison. Verify real CLI/MCP, installed package, and browser lifecycle paths.

Refs #774

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

* fix(browser): align capture defaults and redact encoded media metadata

Correct the complete documented request and legacy default expectation caught by CI. Cover and redact encoded declared values in captured body media types.

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

* test(browser): verify polling capture cutoff completeness

Accept incomplete event coverage only when the continuously polling fixture has explicit unfinished requests; retain failures for other missing or truncated sections. Include actual completeness and unfinished transactions in diagnostics.

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-07 04:35:49 +08:00
N0zoM1z0andGitHub 50787b2faf fix(process): clarify Windows capture support and recovery (#798) 2026-10-07 04:20:53 +08:00
N0zoM1z0andGitHub 911d580634 fix(cli): preserve unsupported historical-source imports (#795) 2026-10-07 04:02:10 +08:00
N0zoM1z0andGitHub b62931f833 fix(windows): accept ordinary drive path separators (#791) 2026-10-07 03:51:59 +08:00
morluto d2aed172ab docs: fix stale IDA roadmap item, mermaid paths, toolchain wording 2026-10-07 02:49:43 +08:00
morluto 00a6ac3de9 docs: audit and improve localized readmes translations and wording 2026-10-07 02:42:32 +08:00
Rudy Mizrahi CelekliandGitHub 4bfd8038be fix(semantics): retain trailing rest argument flows (#756)
Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
2026-10-07 02:36:51 +08:00
Rudy Mizrahi CelekliandGitHub e1ca412757 fix(semantics): preserve direct promise ownership boundaries (#758)
Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
2026-10-07 02:36:41 +08:00
Rudy Mizrahi CelekliandGitHub 66c26f280b fix(mcp): preserve exact guided prompt context (#761)
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-07 02:36:31 +08:00
Rudy Mizrahi CelekliandGitHub cffc548cd3 fix(reference): retain partial imports for unreadable directories (#766)
Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
2026-10-07 02:36:21 +08:00
Rudy Mizrahi CelekliandGitHub ae81f64d2a fix(semantics): isolate named function expression bindings (#776)
Squash-merge: reviewed, no issues. Clean semantics fix with thorough binding-matrix coverage.
2026-10-07 02:36:06 +08:00
morluto c945931961 docs: clarify NativeAOT scope and regenerate conformance ledger
Documents NativeAOT as a native-analysis route with format/architecture limits, keeps the skill DOS-guide link inside the installed bundle convention, and regenerates the managed conformance manifest.
2026-10-07 02:34:44 +08:00
morluto 69be514c67 docs: update release boundary to npm 4.1.0 with Windows native bundle 2026-10-07 02:32:55 +08:00
morluto 554e9b1612 docs: move quick start up and mark skill install optional 2026-10-07 02:08:21 +08:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
9622b4b4cf chore(main): release rea-agents 4.1.0 (#635)
* chore(main): release rea-agents 4.1.0

* chore: normalize release product catalog

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-07 01:57:26 +08:00
N0zoM1z0andGitHub 8238232485 feat(ida): add read-only GUI and headless MCP providers (#754) 2026-10-07 01:47:05 +08:00
Dery FerdikaandGitHub c8e66c704a fix(artifacts): make extraction output tree work on Windows (#753)
SafeOutputTree.create() called fchmod(0o700) on a directory handle and
commit() fsynced a directory handle. Both are POSIX-only: Windows rejects
them with EPERM ("operation not permitted, fchmod"/"fsync"), so every
extract_artifact call failed with a generic io error and produced no output.

Skip the redundant directory chmod and directory fsync on Windows. mkdir
already applies the requested mode, and file contents are synced in write().
Add a Windows regression test and run it, plus the archive-safety test, in
the curated Windows CI lane so the behavior is covered.
2026-10-07 01:46:52 +08:00
91daaa68ae docs(browser): add an interaction example that captures the visible result (#750)
* docs(browser): add an interaction example that captures the visible result

Fixes #728

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(browser): wait for asynchronous search results before capture

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-07 01:24:22 +08:00
Kaoru0822andGitHub 9a4e6da5a9 fix(firmware): validate configured executables (#751) 2026-10-07 01:03:55 +08:00
N0zoM1z0andGitHub fb679b63c2 feat(ghidra): integrate optional NativeAOT metadata recovery (#748)
* feat(ghidra): integrate optional NativeAOT metadata recovery

* fix(ghidra): bind extension snapshots to configured artifacts

* test(ghidra): verify lazy NativeAOT startup failures

* fix(ghidra): preserve NativeAOT failure reasons and coverage

* fix(ghidra): preserve NativeAOT x64 calling conventions

* test(ghidra): guard recovered NativeAOT method conventions

* fix(ghidra): reject blocking extension file inputs

* fix(ghidra): diagnose incompatible NativeAOT extension classes

* test(ghidra): verify real extension initialization failures

* fix(ghidra): keep extension descriptor fields optional
2026-10-07 00:57:08 +08:00
Kaoru0822andGitHub 6d1f39b250 Merge pull request #749 from kaoru0822-kitauji/fix/javascript-schema-diagnostics
fix(javascript): expose result schema failures
2026-10-06 09:01:33 -07:00
Kaoru0822 99263a720d fix(javascript): expose result schema failures 2026-10-06 08:52:29 -07:00
N0zoM1z0andGitHub 5af1699103 docs: clarify onboarding and align provider guidance (#745) 2026-10-06 22:57:36 +08:00
d3f498ceec docs(hopper): document HopperTargetLease realpath fallback (#711)
* docs(hopper): document HopperTargetLease realpath fallback

Closes #642

* docs(hopper): clarify realpath fallback errors

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-06 22:27:57 +08:00
1d15bcb9ec fix(browser): classify CSP host sources by their own origin (#707)
* fix(browser): classify CSP host sources by their own origin

A Content-Security-Policy host-source without a scheme (for example
`cdn.example.com` or `*.trusted.test`) was resolved with
`new URL(token, baseUrl)`, so it became a page-relative path and was
reported as the observed page's own origin. Third-party script, style, and
connect sources were therefore misreported as approved origins, and a bare
`*` wildcard collapsed to the page origin as well.

Resolve host-sources per CSP3 \u00a72.3.1: a scheme-less source inherits the
protected resource's scheme, wildcard host/port sources stay classified as a
set of origins, and non-host tokens remain `other`. Keyword and scheme
sources are unchanged.

Adds boundary coverage for scheme-less host sources, wildcards, and scheme
inheritance against the approved-origin set.

* fix(browser): validate CSP host source syntax

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-06 22:26:57 +08:00
Kaoru0822andGitHub 739039092f fix(android): validate explicit Java runtime (#742) 2026-10-06 22:25:54 +08:00
morluto 3f6d6dc756 docs: update Trendshift repository badge 2026-10-06 22:18:21 +08:00
N0zoM1z0andGitHub 5a7c769692 feat(firmware): integrate Binwalk and Unblob analysis
* feat(firmware): integrate Binwalk and Unblob analysis

* fix(firmware): verify published files and namespace source references

* test(firmware): isolate extractor dependency failure from process cleanup

* fix(firmware): retain actionable report and cleanup diagnostics

* test: assert retained batch output failure diagnostics
2026-10-06 14:16:04 +00:00
Kaoru0822andGitHub 8d7071da4c Merge pull request #739 from kaoru0822-kitauji/fix/hopper-shutdown-request-diagnostics
fix(hopper): retain shutdown request diagnostics
2026-10-06 06:19:07 -07:00
Kaoru0822 72548712fe fix(hopper): retain shutdown request diagnostics 2026-10-06 05:49:26 -07:00
N0zoM1z0andGitHub 904d03e987 feat(android): integrate static APK analysis with headless JADX (#738)
* feat(android): integrate static APK analysis with headless JADX

* test(android): include APK tools in explicit effects audit
2026-10-06 12:46:48 +00:00
N0zoM1z0andGitHub d0f3480a3c feat(evidence): accept exact retained application references (#734) 2026-10-06 20:17:59 +08:00
Kaoru0822andGitHub 00eb03d0f2 Merge pull request #735 from kaoru0822-kitauji/fix/reference-import-cli-failure-status
fix(cli): report reference-source import failures
2026-10-06 05:09:11 -07:00
Kaoru0822andGitHub 75a75cbb20 Merge pull request #731 from kaoru0822-kitauji/fix/semantic-union-unknown-branches
fix(javascript): preserve unknown branches in primitive unions
2026-10-06 05:08:54 -07:00
Kaoru0822andGitHub 09c249deb8 Merge pull request #729 from kaoru0822-kitauji/fix/mcp-shutdown-diagnostics
fix(mcp): retain shutdown failure diagnostics
2026-10-06 05:08:40 -07:00
Kaoru0822andGitHub a5b970c6bf Merge pull request #716 from kaoru0822-kitauji/fix/client-configuration-leading-bom
fix(setup): accept leading BOM in JSON client configs
2026-10-06 05:08:25 -07:00
Kaoru0822andGitHub 65c879757e Merge pull request #715 from kaoru0822-kitauji/fix/typed-seed-array-members
fix(trace): match typed seeds against member arrays
2026-10-06 05:08:12 -07:00
Kaoru0822andGitHub 18c18290d0 Merge pull request #714 from kaoru0822-kitauji/fix/cli-json-file-failure-status
fix(cli): fail when JSON input files cannot be parsed
2026-10-06 05:07:58 -07:00
Kaoru0822andGitHub b1b687fb75 Merge pull request #713 from kaoru0822-kitauji/fix/ghidra-universal-macho-refusal
fix(ghidra): refuse ambiguous universal Mach-O imports
2026-10-06 05:07:38 -07:00
Kaoru0822andGitHub 863ed47de2 Merge pull request #712 from kaoru0822-kitauji/fix/javascript-resolve-path-semantics
fix(javascript): preserve path.resolve reset semantics
2026-10-06 05:07:23 -07:00
Kaoru0822andGitHub 1dccd0dee0 Merge pull request #701 from kaoru0822-kitauji/fix/mixed-signature-slices
fix(native): inspect mixed-signature Mach-O slices
2026-10-06 05:07:08 -07:00
Kaoru0822 6582b2433a fix(ghidra): refuse ambiguous universal Mach-O imports 2026-10-06 05:00:53 -07:00
Kaoru0822 66f3cfa594 fix(native): inspect mixed-signature Mach-O slices 2026-10-06 04:59:15 -07:00
N0zoM1z0andGitHub e2c23f3640 docs(ghidra): explain first-query deadlines and recovery (#736) 2026-10-06 19:53:44 +08:00
N0zoM1z0andGitHub 8d44b3edd3 fix(target): explain the JavaScript directory analysis route (#733) 2026-10-06 19:53:08 +08:00
Kaoru0822 431e8ebfcf fix(cli): report reference-source import failures 2026-10-06 04:27:34 -07:00
N0zoM1z0andGitHub 9dd6cef90a fix(process): scope comparison unknowns to their evidence (#732) 2026-10-06 19:16:49 +08:00
Kaoru0822 9a2f395028 fix(javascript): preserve unknown branches in primitive unions 2026-10-06 04:06:11 -07:00
Kaoru0822 fd008cd8e0 fix(mcp): retain shutdown failure diagnostics 2026-10-06 03:58:12 -07:00
Kaoru0822 fe843ec0b1 fix(setup): accept leading BOM in JSON client configs 2026-10-06 03:40:42 -07:00
Kaoru0822 38ebd54776 fix(trace): match typed seeds against member arrays 2026-10-06 03:24:18 -07:00
Kaoru0822 3201f0538d fix(cli): fail when JSON input files cannot be parsed 2026-10-06 03:02:22 -07:00
Kaoru0822andGitHub af48fe8791 Merge pull request #710 from kaoru0822-kitauji/fix/electron-option-property-keys
fix(electron): respect computed option keys and overrides
2026-10-06 02:44:14 -07:00
Kaoru0822 417737748a fix(javascript): preserve path.resolve reset semantics 2026-10-06 02:42:16 -07:00
Kaoru0822 98de3380e3 fix(electron): respect computed option keys and overrides 2026-10-06 02:26:28 -07:00
N0zoM1z0andGitHub 9bab55337a feat(ghidra): add atomic function annotation edits (#709)
* feat(ghidra): edit function annotations atomically

Return annotation readback and refreshed function analysis through CLI and MCP. Own each database transaction, invalidate caches and immutable snapshots, preserve executable bytes, and retain the Windows mutation boundary.

Assisted-by: Nitori (gensokyo-skills:nitori-reverse-engineering)

* fix(ghidra): preserve qualified function annotation identity

Preserve namespaces during local name writes and match qualified readback to the refreshed dossier. Add a producer-shaped regression and verify a real namespaced C++ function through the CLI.

Assisted-by: Nitori (gensokyo-skills:nitori-reverse-engineering)

* test(ghidra): align verification with annotation authority

Keep strict capability and profile checks while admitting the explicit session metadata operation. Verify packaged transport authority and atomic transaction commitments and preserve a read-only Windows expectation.

Assisted-by: Nitori (gensokyo-skills:nitori-reverse-engineering)
2026-10-06 17:24:33 +08:00
Kaoru0822andGitHub c3bc38bf6c Merge pull request #708 from kaoru0822-kitauji/fix/function-verification-source-parameters
fix(reconstruction): accept function comparison source parameters
2026-10-06 02:18:20 -07:00
Kaoru0822 7a96391001 fix(reconstruction): accept function comparison source parameters 2026-10-06 02:11:10 -07:00
Kaoru0822andGitHub ab5a1c61b1 Merge pull request #706 from kaoru0822-kitauji/fix/process-evidence-provider-identity
fix(evidence): admit current process provider identity
2026-10-06 02:00:47 -07:00
Kaoru0822andGitHub f04445c57c Merge pull request #704 from kaoru0822-kitauji/fix/arm64e-native-inspection
fix(native): select concrete arm64e Mach-O slices
2026-10-06 02:00:35 -07:00
Kaoru0822andGitHub 44b3627ab0 Merge pull request #703 from kaoru0822-kitauji/fix/process-pid-normalization
fix(process): honor disabled PID normalization in samples
2026-10-06 02:00:19 -07:00
N0zoM1z0andGitHub 26881da76a feat(ghidra): enable Windows x64 read-only analysis (#699)
* feat(ghidra): enable Windows x64 read-only analysis

* test(ghidra): exercise native Windows runtime boundaries

* test(ghidra): verify current Windows inventory and isolate ACL observer

* fix(ghidra): align Windows admission and address P0 review
2026-10-06 16:53:33 +08:00
Kaoru0822 d311940f24 fix(evidence): admit current process provider identity 2026-10-06 01:43:45 -07:00
cf061159ab fix: integrate 14 approved fix PRs (668,671,673,675,678-681,685-690) (#705)
* fix(javascript): preserve array index positions

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

* fix(javascript): respect object property overwrite order

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

* test(javascript): cover uncertain spread discriminants

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

* fix(javascript): keep nonfinite values outside JSON lattice

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

* fix(dotnet): keep non-CIL method implementations unknown

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

* test(package): separate CIL reconstruction from native imports

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

* fix(javascript): invalidate values on nontrivial binding writes

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

* fix(artifacts): align native DMG mount and detach lifecycle

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

* fix(browser): retain all literal worker import arguments

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

* fix(native): preserve final demangled symbol whitespace

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

* fix(dotnet): preserve C identifier underscores in native matching

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

* fix(browser): respect metadata key and overwrite semantics

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

* fix(javascript): keep optional parameter input unknown

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

* fix(dotnet): report observed method metadata changes

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

* docs(dotnet): synchronize generated conformance skill digest

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

* fix(browser): reject malformed indexed source-map offsets

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

* fix(native): preserve numeric-looking Mach-O identifiers

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

* docs: regenerate catalogs for integrated fix batch

* docs: regenerate catalogs after merging main (#702)

---------

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 16:31:00 +08:00
N0zoM1z0andGitHub 5bcb6645b5 feat(ghidra): admit explicit DOS COM analysis (#702)
* feat(ghidra): admit explicit DOS COM analysis

Carry caller-selected COM interpretation through CLI, MCP and target
identity, seed real-mode entry/context before Ghidra analysis, and verify
complete loaded bytes, source mappings and measured register context.
Keep original binaries unchanged and PSP/runtime coverage explicit.

Assisted-by: Nitori (gensokyo-skills:nitori-reverse-engineering)

* fix(verification): validate COM package selection before allocating fixtures
2026-10-06 16:26:40 +08:00
Kaoru0822 fd3282a87e fix(native): select concrete arm64e Mach-O slices 2026-10-06 01:25:11 -07:00
Kaoru0822 a4e57366dd fix(process): honor disabled PID normalization in samples 2026-10-06 01:22:45 -07:00
Rudy Mizrahi CelekliandGitHub d565ecdc25 fix(reference): read linked worktree commit provenance (#677)
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 16:18:42 +08:00
Rudy Mizrahi CelekliandGitHub 8189081511 fix(browser): keep bare module locations unresolved (#676)
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 16:18:36 +08:00
Kaoru0822andGitHub 7f1fa64019 fix(javascript): read source-map directives only from comments (#672) 2026-10-06 16:18:25 +08:00
Rudy Mizrahi CelekliandGitHub c8aed4740d fix(reference): preserve unresolved absolute module specifiers (#670)
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 16:18:17 +08:00
Rudy Mizrahi CelekliandGitHub fa02df9ad3 fix(native): retain spaced symbols in dyld inventory (#669)
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 16:18:09 +08:00
Rudy Mizrahi CelekliandGitHub ddea06360a fix(dotnet): preserve leading Unicode content in metadata strings (#667)
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 16:18:01 +08:00
Rudy Mizrahi CelekliandGitHub 94cc8475e0 fix(javascript): preserve primitive addition coercion (#666)
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 16:17:55 +08:00
Kaoru0822andGitHub 1a24be574d fix(reference): recognize CMake build manifests (#665) 2026-10-06 16:17:50 +08:00
Kaoru0822andGitHub 217e8cf59f Merge pull request #700 from kaoru0822-kitauji/fix/json-shape-collation
fix(comparison): break JSON shape collation ties
2026-10-06 01:10:26 -07:00
Kaoru0822andGitHub a55b5bb8ee Merge pull request #698 from kaoru0822-kitauji/fix/export-container-coverage
fix(comparison): retain container-only export shape paths
2026-10-06 01:10:13 -07:00
Kaoru0822andGitHub ba35b5509d Merge pull request #696 from kaoru0822-kitauji/fix/reference-computed-require
fix(reference): distinguish computed require member names
2026-10-06 01:09:56 -07:00
Kaoru0822andGitHub 11d0ef2e09 Merge pull request #695 from kaoru0822-kitauji/fix/reference-parser-recovery
fix(reference): retain recovered parser diagnostics
2026-10-06 01:09:40 -07:00
Kaoru0822 805c338f36 fix(comparison): break JSON shape collation ties 2026-10-06 01:01:16 -07:00
N0zoM1z0andGitHub a6a8f67649 feat(ghidra): verify DOS load images and expose memory evidence (#697)
* feat(ghidra): verify DOS load images and expose memory evidence

Verify measured source bytes, mapped regions, fixups and entries against
the immutable MZ snapshot, retaining independent checks and raw evidence.
Expose loaded byte reads and authoritative original file offsets through
both CLI and MCP, with focused and actual Ghidra DOS verification.

Assisted-by: Nitori (gensokyo-skills:nitori-reverse-engineering)

* fix(cli): describe memory inspection arguments

* test(ghidra): align bridge fixtures and CLI capability accounting
2026-10-06 15:48:48 +08:00
Kaoru0822 7864d665e1 fix(comparison): retain container-only export shape paths 2026-10-06 00:42:19 -07:00
Kaoru0822 df6e476d67 fix(reference): distinguish computed require member names 2026-10-06 00:26:08 -07:00
Kaoru0822 ed3a7f070c fix(reference): retain recovered parser diagnostics 2026-10-06 00:17:04 -07:00
Kaoru0822andGitHub 132a3a46be Merge pull request #694 from kaoru0822-kitauji/fix/dockerfile-language-boundary
fix(reference): bound Dockerfile language detection
2026-10-06 00:10:16 -07:00
Kaoru0822andGitHub e828acce0e Merge pull request #692 from kaoru0822-kitauji/fix/reference-test-stems
fix(reference): classify test suffixes before extensions
2026-10-06 00:07:24 -07:00
Kaoru0822 53fa55937f fix(reference): bound Dockerfile language detection 2026-10-06 00:03:29 -07:00
Kaoru0822andGitHub e60aa11108 Merge pull request #691 from kaoru0822-kitauji/fix/javascript-line-terminators
fix(javascript): count all source line terminators
2026-10-05 23:59:26 -07:00
Kaoru0822 8b228ae701 fix(reference): classify test suffixes before extensions 2026-10-05 23:56:24 -07:00
Kaoru0822 03bc6d36db fix(javascript): count all source line terminators 2026-10-05 23:49:27 -07:00
Kaoru0822andGitHub b6c21b1db5 Merge pull request #683 from morluto/docs/research-trendshift-badge
docs: add the official Trendshift badge to README headers
2026-10-05 23:32:36 -07:00
Kaoru0822andGitHub 433ebda8b4 Merge pull request #682 from kaoru0822-kitauji/fix/disable-unmanaged-skill-sync
fix(cli): disable unmanaged skill synchronization
2026-10-05 23:32:18 -07:00
Kaoru0822 2c1bf92010 fix(cli): disable unmanaged skill synchronization 2026-10-05 23:27:46 -07:00
N0zoM1z0 7d4ed59816 docs: add the official Trendshift badge to README headers 2026-10-06 06:27:12 +00:00
I Made Ocy Darma PutraandGitHub 7f1ae42f71 feat(setup): support Command Code MCP registration (#645)
Configure Command Code's user-scope MCP config (~/.commandcode/mcp.json)
with an explicit stdio registration ({ transport, enabled, command, args })
under mcpServers, matching the shape Command Code writes itself.

Adds the commandcode setup client and dialect, detection/alignment support,
README/docs entries, and regenerated product catalog.
2026-10-06 14:15:59 +08:00
Kaoru0822andGitHub 6ad62cec6a Merge pull request #663 from kaoru0822-kitauji/fix/semantic-candidate-relevance
fix(javascript): scope candidate ambiguity to query traversal
2026-10-05 23:06:33 -07:00
N0zoM1z0andGitHub 39db9fe424 docs: align MCP discovery and skill guidance with the stable catalog (#664) 2026-10-06 14:01:43 +08:00
Kaoru0822andGitHub c3d013b2fd Merge pull request #659 from kaoru0822-kitauji/fix/function-collation-ties
fix(comparison): break function collection collation ties
2026-10-05 22:58:54 -07:00
Kaoru0822andGitHub a5d2986b1e Merge pull request #657 from kaoru0822-kitauji/fix/reference-dynamic-imports
fix(reference): retain dynamic import relationships
2026-10-05 22:55:27 -07:00
Kaoru0822andGitHub 6a37346acb Merge pull request #652 from kaoru0822-kitauji/fix/dispatcher-build-recovery
fix(cli): include build step in missing-runtime recovery
2026-10-05 22:55:11 -07:00
Kaoru0822 f94fc7967b fix(javascript): scope candidate ambiguity to query traversal 2026-10-05 22:51:47 -07:00
N0zoM1z0andGitHub 88a3c3f8a4 test(hopper): synchronize startup cancellation with launcher readiness (#662) 2026-10-06 13:51:45 +08:00
Kaoru0822andGitHub 8dd1d84471 docs(installation): pin manual MCP registration to the release version (#658) 2026-10-06 13:50:08 +08:00
Kaoru0822andGitHub 5802ef8746 Merge pull request #651 from kaoru0822-kitauji/fix/node-runtime-engine-alignment
fix(setup): align Node runtime checks with package engines
2026-10-05 22:45:21 -07:00
Kaoru0822 3aaf7e2c54 fix(comparison): break function collection collation ties 2026-10-05 22:39:03 -07:00
N0zoM1z0andGitHub fc80ea88df docs: correct CLI examples and documentation checks (#655) 2026-10-06 13:34:22 +08:00
Kaoru0822 b89de8feab fix(reference): retain dynamic import relationships 2026-10-05 22:32:36 -07:00
Kaoru0822 ae72abd019 fix(cli): include build step in missing-runtime recovery 2026-10-05 22:19:35 -07:00
Kaoru0822 ea0b733687 fix(setup): align Node runtime checks with package engines 2026-10-05 22:13:22 -07:00
N0zoM1z0andGitHub 19f8cddfd6 fix(ghidra): preserve request and shutdown failure diagnostics (#647) 2026-10-06 12:13:52 +08:00
N0zoM1z0andGitHub 9f42495d11 fix(javascript): hash graph and evidence JSON incrementally (#646) 2026-10-06 11:54:55 +08:00
morlutoandGitHub 9b392bb2f8 chore(deps): finish toolchain upgrades and drop dead weight (#644)
* chore(deps): finish toolchain upgrades and drop dead weight

pino 9 to 10, @types/node 24 to 26, oxlint 1.73 to 1.87, knip 6.26
to 6.39, turbo 2.10 to 2.11. Narrow engines to supported majors
(22/24/26; vitest 5 refuses Node 25). Scope no-var-requires off for
Electron CJS hooks and fixtures.

Remove TypeDoc: incompatible with TypeScript 7 at install and
runtime, used by nothing in any gate. Drop the dep, docs:api
workflow, typedoc.json, CI artifact step, and related docs.

Replace graphology with a local call-graph adjacency plus BFS: the
only consumer is call-path search, and graphology-types ships no
types entry for TS7 NodeNext resolution. Delete both graphology
dependencies.

Remove dead re-exports newly flagged by knip 6.39.

* chore: drop stale typedoc and replay references

Remove the docs:api release-checklist step, the orphaned docs/api
ignore entry, and the nonexistent replay glob from the adapters
project.
2026-10-06 06:02:02 +08:00
morlutoandGitHub 5f27e02fae refactor(runtime): name all silent catch causes without changing diagnostics (#637)
* refactor(domain): split error taxonomy into thematic modules

Move each error family out of domain/errors.ts into its own module
(base, core, provider selection/adapter, browser observation,
artifact, evidence, unknown registry, hopper, configuration) and
relocate AnalysisErrorProjection to analysisErrorProjection.
Migrate all 162 importers to the owning modules and delete
errors.ts. No re-export shims; behavior is unchanged.

* refactor(runtime): name all silent catch causes without changing diagnostics

Bind cause in every bare catch and unhandled rejection across src/.
Real failures preserve the cause for logging; best-effort cleanup
carries an explicit comment. Caller-visible and Evidence strings are
byte-identical to before.
2026-10-06 05:58:46 +08:00
morlutoandGitHub 735c10059c refactor(domain): split error taxonomy into thematic modules (#636)
Move each error family out of domain/errors.ts into its own module
(base, core, provider selection/adapter, browser observation,
artifact, evidence, unknown registry, hopper, configuration) and
relocate AnalysisErrorProjection to analysisErrorProjection.
Migrate all 162 importers to the owning modules and delete
errors.ts. No re-export shims; behavior is unchanged.
2026-10-06 05:57:38 +08:00
morluto 77799e6c90 fix(ci): upload Vitest blob reports from current path 2026-10-06 05:54:40 +08:00
morluto 965d0b35f9 chore(deps): remove incompatible TypeDoc generator 2026-10-06 05:46:44 +08:00
morlutoandGitHub 5295d56b10 refactor(contracts): split tool contracts by family (#634)
Move official, enhanced, and session contract arrays with their
builders into officialToolContracts, enhancedToolContracts, and
sessionToolContracts. toolContracts keeps the ordered assembly and
resolver. Migrate all importers including catalog scripts. No
re-export shims; generated catalog output is unchanged.
2026-10-06 05:20:11 +08:00
morlutoandGitHub dee8853fde chore(deps): upgrade typescript, vitest, and MCP SDK to latest (#633)
typescript 5.9.3 to 7.0.2, vitest 4.1.10 to 5.0.3 (plus
@vitest/coverage-v8 and @fast-check/vitest 0.5.0 for peer compat),
@Modelcontextprotocol/client and server 2.0.0 to 2.3.1. Migrate the
removed describe.sequential to concurrent:false and regenerate
package metadata and product catalog.

Also restore byte-faithful reads in parseCliJsonInput: the safeJson
migration dropped the fatal UTF-8 check from #613. Vitest 5 requires
Node 22/24/26+, so verify on Node 24 (repo-pinned toolchain).
2026-10-06 05:15:17 +08:00
morlutoandGitHub 494d4db6d9 refactor(cli): collapse duplicate CLI layer into src/cli (#630)
Move root cli*Commands and cliSetup into src/cli, delete the
forwarding layer, point src/cli.ts and tests at the owners. No
forwarding shims.
2026-10-06 05:04:25 +08:00
morlutoandGitHub e7f54b3310 refactor(runtime): centralize safe JSON parsing and preserve error causes (#629)
* refactor(domain): consolidate canonical ordering and digest helpers (#624)

* refactor(domain): consolidate canonical ordering and digest helpers

Single owner for code-point ordering (domain/canonicalOrdering.ts)
and canonical JSON digests (domain/comparisonSemantics.ts). Delete
all local compareCodePoints/compareText/digestCanonical duplicates
and migrate every importer. No re-export shims.

* fix(ci): allow slower real browser startup

* chore(config): remove dead eslint, dedupe scripts, cacheable turbo tasks

Delete eslint.config.js (oxlint is the linter), drop lint:dead alias,
pin smol-toml, fold jscpd/scan:todos into check:pr, make test:run
cacheable, align oxlint ignores.

* fix(config): hash all test inputs for cached runs

* docs: fix architecture diagram and pretty-print tool catalog

Drop dangling REPLAY edges, add dotnet/reference/error/evaluation
nodes. Emit 2-space JSON from the catalog generator and regenerate.

* test: extract shared fixtures, drop knip carve-outs

Dedupe android/apple session tests, bundler adapter headers, and
reconstruction helpers into tests/support fixtures. Delete unused
type aliases and the knip ignoreIssues block. Lower jscpd threshold
2.7 to 2.5.

* refactor(app): split Setup and ProcessOwnership modules

Extract SetupTypes/SetupHost from Setup.ts and move observation
helpers into ProcessOwnershipObservation/ProcessTree. Migrate all
importers including CLI commands. No re-export shims.

* refactor(runtime): centralize safe JSON parsing and preserve error causes

Add domain/safeJson.safeParseJson and migrate all raw JSON.parse
call sites. Name catch causes and preserve failure reasons instead
of swallowing; annotate genuine best-effort cleanup.

* fix(runtime): retain JSON parse error causes
2026-10-06 05:00:54 +08:00
morlutoandGitHub 3c84080aab refactor(app): split Setup and ProcessOwnership modules (#628)
* refactor(domain): consolidate canonical ordering and digest helpers (#624)

* refactor(domain): consolidate canonical ordering and digest helpers

Single owner for code-point ordering (domain/canonicalOrdering.ts)
and canonical JSON digests (domain/comparisonSemantics.ts). Delete
all local compareCodePoints/compareText/digestCanonical duplicates
and migrate every importer. No re-export shims.

* fix(ci): allow slower real browser startup

* chore(config): remove dead eslint, dedupe scripts, cacheable turbo tasks

Delete eslint.config.js (oxlint is the linter), drop lint:dead alias,
pin smol-toml, fold jscpd/scan:todos into check:pr, make test:run
cacheable, align oxlint ignores.

* fix(config): hash all test inputs for cached runs

* docs: fix architecture diagram and pretty-print tool catalog

Drop dangling REPLAY edges, add dotnet/reference/error/evaluation
nodes. Emit 2-space JSON from the catalog generator and regenerate.

* test: extract shared fixtures, drop knip carve-outs

Dedupe android/apple session tests, bundler adapter headers, and
reconstruction helpers into tests/support fixtures. Delete unused
type aliases and the knip ignoreIssues block. Lower jscpd threshold
2.7 to 2.5.

* refactor(app): split Setup and ProcessOwnership modules

Extract SetupTypes/SetupHost from Setup.ts and move observation
helpers into ProcessOwnershipObservation/ProcessTree. Migrate all
importers including CLI commands. No re-export shims.
2026-10-06 05:00:32 +08:00
morlutoandGitHub 55e5eb090b test: extract shared fixtures, drop knip carve-outs (#627)
* refactor(domain): consolidate canonical ordering and digest helpers (#624)

* refactor(domain): consolidate canonical ordering and digest helpers

Single owner for code-point ordering (domain/canonicalOrdering.ts)
and canonical JSON digests (domain/comparisonSemantics.ts). Delete
all local compareCodePoints/compareText/digestCanonical duplicates
and migrate every importer. No re-export shims.

* fix(ci): allow slower real browser startup

* chore(config): remove dead eslint, dedupe scripts, cacheable turbo tasks

Delete eslint.config.js (oxlint is the linter), drop lint:dead alias,
pin smol-toml, fold jscpd/scan:todos into check:pr, make test:run
cacheable, align oxlint ignores.

* fix(config): hash all test inputs for cached runs

* docs: fix architecture diagram and pretty-print tool catalog

Drop dangling REPLAY edges, add dotnet/reference/error/evaluation
nodes. Emit 2-space JSON from the catalog generator and regenerate.

* test: extract shared fixtures, drop knip carve-outs

Dedupe android/apple session tests, bundler adapter headers, and
reconstruction helpers into tests/support fixtures. Delete unused
type aliases and the knip ignoreIssues block. Lower jscpd threshold
2.7 to 2.5.
2026-10-06 05:00:16 +08:00
morlutoandGitHub 5e7f35c7c3 docs: fix architecture diagram and pretty-print tool catalog (#626)
* refactor(domain): consolidate canonical ordering and digest helpers (#624)

* refactor(domain): consolidate canonical ordering and digest helpers

Single owner for code-point ordering (domain/canonicalOrdering.ts)
and canonical JSON digests (domain/comparisonSemantics.ts). Delete
all local compareCodePoints/compareText/digestCanonical duplicates
and migrate every importer. No re-export shims.

* fix(ci): allow slower real browser startup

* chore(config): remove dead eslint, dedupe scripts, cacheable turbo tasks

Delete eslint.config.js (oxlint is the linter), drop lint:dead alias,
pin smol-toml, fold jscpd/scan:todos into check:pr, make test:run
cacheable, align oxlint ignores.

* fix(config): hash all test inputs for cached runs

* docs: fix architecture diagram and pretty-print tool catalog

Drop dangling REPLAY edges, add dotnet/reference/error/evaluation
nodes. Emit 2-space JSON from the catalog generator and regenerate.
2026-10-06 05:00:01 +08:00
morlutoandGitHub 9c10a64f87 chore(config): remove dead eslint, dedupe scripts, cacheable turbo tasks (#625)
* refactor(domain): consolidate canonical ordering and digest helpers (#624)

* refactor(domain): consolidate canonical ordering and digest helpers

Single owner for code-point ordering (domain/canonicalOrdering.ts)
and canonical JSON digests (domain/comparisonSemantics.ts). Delete
all local compareCodePoints/compareText/digestCanonical duplicates
and migrate every importer. No re-export shims.

* fix(ci): allow slower real browser startup

* chore(config): remove dead eslint, dedupe scripts, cacheable turbo tasks

Delete eslint.config.js (oxlint is the linter), drop lint:dead alias,
pin smol-toml, fold jscpd/scan:todos into check:pr, make test:run
cacheable, align oxlint ignores.

* fix(config): hash all test inputs for cached runs
2026-10-06 04:59:49 +08:00
morlutoandGitHub ba3869c597 refactor(domain): consolidate canonical ordering and digest helpers (#624)
* refactor(domain): consolidate canonical ordering and digest helpers

Single owner for code-point ordering (domain/canonicalOrdering.ts)
and canonical JSON digests (domain/comparisonSemantics.ts). Delete
all local compareCodePoints/compareText/digestCanonical duplicates
and migrate every importer. No re-export shims.

* fix(ci): allow slower real browser startup
2026-10-06 04:55:26 +08:00
morlutoandGitHub 38b293329c refactor(native): extract FAT header format switch in Mach-O slice selection (#632)
Replace inline magic comparisons with an exhaustive fatHeaderFormat
switch and add a regression test locking in rejection of 32-bit
slice payloads inside little-endian containers. Behavior-preserving:
the new test passes on the old implementation.
2026-10-06 04:36:13 +08:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
feeb19b8b3 chore(main): release rea-agents 4.0.1 (#622)
* chore(main): release rea-agents 4.0.1

* chore: normalize release product catalog

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-06 02:54:44 +08:00
morluto 2ef02ced16 fix(release): allow npm propagation time 2026-10-06 02:51:43 +08:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1a936887b2 chore(main): release rea-agents 4.0.0 (#502)
* chore(main): release rea-agents 4.0.0

* chore: normalize release product catalog

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-06 02:29:30 +08:00
morluto 3fff74fb5b fix(process): align provider detach with configured platform 2026-10-06 02:26:15 +08:00
morluto a574b744b8 fix(ghidra): avoid nested quotes in Windows Java options 2026-10-06 02:22:18 +08:00
morluto bede3e3c60 fix(native): decode little-endian fat headers
Fixes #592
2026-10-06 02:22:18 +08:00
Rudy Mizrahi CelekliandGitHub de45f1dcb9 chore: remove tracked developer dependency symlink (#616)
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 02:14:07 +08:00
N0zoM1z0andGitHub b3223e3f61 fix(repo): keep dependency links out of version control (#617) 2026-10-06 02:10:40 +08:00
Rudy Mizrahi CelekliandGitHub 22acb0d5d7 fix(cli): reject invalid UTF-8 JSON file bytes (#613)
* fix(cli): reject invalid UTF-8 JSON file bytes

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

* test(browser): preserve unknown with-scope loader bindings

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

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 02:10:11 +08:00
Rudy Mizrahi CelekliandGitHub 1bd641d39f fix(browser): distinguish duplicate screenshot frame events (#612)
* fix(browser): distinguish duplicate screenshot frame events

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

* test(browser): preserve unknown with-scope loader bindings

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

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 02:10:00 +08:00
Rudy Mizrahi CelekliandGitHub 864205570a fix(browser): release discarded source-map response bodies (#611)
* fix(browser): release discarded source-map response bodies

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

* test(browser): preserve unknown with-scope loader bindings

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

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 02:09:50 +08:00
Rudy Mizrahi CelekliandGitHub b81d18eeee fix(native): reject signal-terminated command captures (#608)
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 02:09:41 +08:00
Rudy Mizrahi CelekliandGitHub c73742f300 fix(managed): distinguish metadata handles from member accesses (#607)
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 02:09:31 +08:00
Rudy Mizrahi CelekliandGitHub adc07f5eaa fix(javascript): preserve Node exports fallback stop reasons (#606)
* fix(javascript): preserve Node exports fallback stop reasons

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

* test(browser): preserve unknown with-scope loader bindings

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

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 02:09:12 +08:00
Rudy Mizrahi CelekliandGitHub 0ce529af4e fix(managed): recognize PE Thumb and ARMNT machines (#605)
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 02:09:00 +08:00
Rudy Mizrahi CelekliandGitHub 4a98c3ea03 fix(comparison): require decoded signatures for structural method matches (#604)
* fix(comparison): require decoded signatures for structural method matches

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

* test(browser): preserve unknown with-scope loader bindings

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

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 02:08:51 +08:00
Rudy Mizrahi CelekliandGitHub 12a2e3793a fix(comparison): account for unpaired records after explicit bundle pairing (#603)
* fix(comparison): account for unpaired records after explicit bundle pairing

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

* test(browser): preserve unknown with-scope loader bindings

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

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 02:08:42 +08:00
Rudy Mizrahi CelekliandGitHub 2b7bb3bb6a fix(native): inspect the selected universal Mach-O architecture (#601)
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 02:08:33 +08:00
Rudy Mizrahi CelekliandGitHub 1b6f05470d fix(managed): classify truncated signatures as malformed (#598)
* fix(managed): classify truncated signatures as malformed

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

* fix(managed): use typed signature parsing failures

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

* chore(managed): refresh signature schema commitments

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

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 02:08:02 +08:00
morlutoandGitHub 5daaa453ff chore: migrate formatting to Oxfmt (#615) 2026-10-06 01:59:19 +08:00
f557207887 fix(browser): capture popup network and report early event gaps (#614)
* fix(browser): capture selected events in popup pages

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

* fix(browser): capture popup network and report early event gaps

* fix(browser): deduplicate owned popup discovery

* test(browser): group popup selection coverage with the scenario event suite

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

* test(browser): verify popup evidence through CLI and MCP

* fix(browser): expose shared-context event coverage limits

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-06 01:58:54 +08:00
5f3ba3e688 fix(comparison): match generated names without swallowing symbol prefixes (#596)
* fix(comparison): match generated names without swallowing symbol prefixes

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

* test(comparison): cover symbol identity and callee dimensions

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

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-06 01:57:20 +08:00
Rudy Mizrahi CelekliandGitHub d1e5c0e1fd fix(comparison): normalize CFG identity by numeric addresses (#593)
Equivalent hexadecimal addresses no longer collapse a fully observed CFG to unknown, and duplicate numeric block starts are rejected even when spelled differently.
2026-10-06 01:51:59 +08:00
Rudy Mizrahi CelekliandGitHub 9997152c2d fix(browser): preserve RGB PNG transparent samples (#595)
Respect the RGB PNG tRNS transparent-color key when expanding 8-bit RGB pixels to RGBA, so equivalent RGB+tRNS and RGBA captures compare identically.
2026-10-06 01:51:32 +08:00
Rudy Mizrahi CelekliandGitHub 50f7e81432 fix(artifacts): interrupt ZIP extraction under backpressure (#594)
Cancel ZIP extraction promptly when an unread consumer applies backpressure, releasing the producer instead of leaving it parked on a drain that can never arrive.
2026-10-06 01:51:26 +08:00
Rudy Mizrahi CelekliandGitHub a3bf4a0944 fix(browser): capture selected events in popup pages (#600)
Selected page events from popups are retained even when popup lifecycle events are unselected; popup opened/closed events remain selection-controlled.
2026-10-06 01:51:10 +08:00
N0zoM1z0andGitHub 2919def828 fix(skill): remove obsolete grants and audit tool effects
## Changes

Related to #545 and the permission-system removal in #555.

- Upgrade the shipped skill to v24 and remove obsolete filesystem grants, per-call approval flags, Electron file-root configuration, and DMG approval flags. Calls follow the user-requested investigation directly. Keep setup plan confirmation, actual OS requirements, and process/archive lifecycle boundaries.
- Correct the passive browser guide: endpoint/scope come from requests; ordinary local URL queries are retained, while credentials remain redacted.
- Audit canonical tool effects: declare possible DMG process/temp-directory effects, mark fresh extraction non-idempotent, and mark comparisons of supplied captures/PNGs as closed-world. Align artifact-provider capability effects with temporary mount-directory writes.
- Retain non-read-only hints for calls that add Evidence to the session; document that annotations are hints and that clients control their confirmation UI. Do not mislabel session mutations as read-only to suppress prompts.
- Regenerate package, skill, MCP, and product metadata from source.

## Verification

- `npm run test:focused -- <11 affected test files>`: **40 tests passed**, including skill transaction/backup/idempotence, SDK tools/list annotations, actual local capture/PNG comparisons, artifact extraction/containment/cancellation, DMG launcher/cleanup seam, setup confirmation, browser MCP and scenario MCP, CLI catalog parity.
- `npm run check`: typecheck, lint, format, knip, package metadata.
- `npm run docs:check`: generated catalogs, skill metadata, documentation facts, completion ledger, error schema.

No local real Hopper/Ghidra run: their adapters and analysis operations are unchanged. Native macOS DMG behavior is covered locally through the launcher seam; no local macOS/hdiutil claim is made.

Real browser verification passed using existing Headless Chrome143.0.7499.4: actual CLI/stdio MCP,9,331,407-byte PNG, passive capture/source maps, and owned/external scenario cleanup. An initial full Chrome143 run failed the existing screenshot-height assertion (1397 versus1536); the dedicated headless-shell run passed the complete verifier without code changes. The verifier run-lineage field remained unavailable; no lineage-coverage claim is made. Browser download into `.codex` was blocked by CDN403, so the successful run used an existing browser without changing host configuration.
2026-10-06 01:40:58 +08:00
morlutoandGitHub d19686a2a5 fix(test): improve capture reliability and runner cleanup (#609)
* test: make capture fixtures readiness-aware and strengthen assertions

* fix(test): escalate child termination and report cleanup failures

* perf(test): use runner concurrency and lock shared build outputs
2026-10-06 01:32:46 +08:00
bae87b4ded fix(browser): derive source-map imports from syntax nodes (#588)
* fix(browser): derive source-map imports from syntax nodes

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

* fix(browser): recover source-map dependencies from decorated and aliased sources

Discarding a source's edges whenever it had parse errors cost more than it
protected. parseJavaScriptSource only enabled jsx and typescript, so a
decorated TypeScript source did not parse at all and contributed no edges at
all, even for the imports Babel could otherwise have read. That is the common
shape for the bundles this boundary inspects. "decorators-legacy" is enabled
instead of "decorators" because the standard form still rejects parameter
decorators such as constructor(@Inject() private s: S), which is the Angular
and Nest dependency-injection shape.

import x = require("m") parses cleanly but is a TSImportEqualsDeclaration
rather than a call expression, so it contributed no edge. It is now read as a
require dependency. It is only legal at module top level and always refers to
the host loader, so it needs no unbound-require check; the local-alias form
imports an entity name and still declares nothing.

A source whose syntax could not be recovered now yields status "partial" with
a limitation naming the affected sources, instead of an included item whose
empty edge list was indistinguishable from a source that genuinely imports
nothing. Edges that did parse are retained and the caller is told the list is
a subset, which matches how the static and semantic analysers already report
recovered syntax.

The source-map item status union gains "partial" and the coverage rollup now
counts it as retained, so a collection of only-partial items reports partial
rather than unavailable.

* Merge main into fix(browser): derive source-map imports from syntax nodes

Resolve the generated-file conflicts by taking main's generated output and
regenerating from the merged source contracts, rather than merging generated
text by hand.

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-06 01:03:41 +08:00
82895959d1 fix(javascript): honor package exports condition order (#591)
* fix(javascript): honor package exports condition order

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

* fix(javascript): resolve exports against Node's full condition set

The declared-order fix selected targets against {default, import|require},
but Node's active set for the resolver that owns installed packages also
includes "node" and "node-addons". Ordering only means something against the
full active set, so a package declaring {"node", "import"} resolved to the
wrong target, and a package declaring only {"import"} reported no target
where Node resolves ./node.cjs.

Nested condition objects and fallback arrays are also valid Node targets.
They are now flattened in Node's own order: a condition object stops at its
first active key, so a null or unmatched nested target ends the search, while
an array skips the entries it cannot resolve and concatenates the rest. Node
does not fall through a target whose file is absent, so the first flattened
target stands and its absence is reported rather than skipped.

A valid exports map that matches no active condition is reported as external
with the declared condition names, and one that selects an unsupported nested
or array target no longer borrows the "not valid package JSON" reason.

The differential test installs a real package and resolves the same bare
specifier through Node and through REA, mirroring file presence between the
filesystem and the inventory. 18 of the 25 cases fail without this change.

* style: apply Prettier to the conditional exports differential test

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-06 01:03:18 +08:00
ffbde92f9a fix(javascript): respect HTML base URL components (#589)
* fix(javascript): respect HTML base URL components

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

* fix(javascript): resolve HTML base href dot segments and gate base syntax

A relative HTML reference resolves against the base URL's directory, and that
directory drops a trailing "." or ".." segment rather than stepping through
it. Only a trailing "/" was treated as a directory, so <base href=".">
resolved against the artifact root instead of the document directory, and
<base href="sub/."> resolved against the parent of "sub".

The base href is also a second untrusted path input, but rejectDeclaration
only applied its admission rules to the declared path, so an encoded-traversal
or backslash base href relied on files-map confinement to stay safe. Both
inputs now share one admitsCanonicalPathSyntax predicate, and a local base
href is rejected with a reason naming the base href rather than a generic
declared-path message. External base hrefs are still reported as external,
so an encoded path on an unreachable origin keeps the more accurate reason.

7 of the new cases fail without this change.

* style: apply Prettier to the HTML base resolution changes

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-06 01:02:56 +08:00
N0zoM1z0andGitHub 82957c98bf chore(test): separate development feedback from full verification (#599) 2026-10-06 00:59:26 +08:00
morlutoandGitHub 9c25dc4ed2 fix!: repair boundary contracts across CLI and MCP (#597)
* fix(comparisons)!: preserve unknowns across partial observations

Share absence and digest semantics, keep unobserved managed bodies unknown, and match unique managed type/name/signatures before structural fallback.

BREAKING CHANGE: Managed comparison metadata adds exact-signature matching and its count, and name_matching now reports exact-signature-fallback.

* fix(javascript): preserve projected result binding semantics

Share syntax-aware property reads and assigned-result resolution. Retain destructuring paths and downgrade downstream whole-result relationships instead of resolving nested or wrapped calls as aliases.

* fix(native)!: admit complete parser values and preserve typed failures

Decode Mach headers by named columns, validate complete slice keys and lipo alignment, and preserve codesign runtime flags and tagged parser errors. Rename the Ghidra test dossier fixture and connect fake-backed suites to real verification lanes.

BREAKING CHANGE: Native load-command results no longer contain the unused, always-null file_offset field.

* fix(process): preserve host identity and actionable boundary failures

Thread selected platform and environment through process and Doctor seams, stamp observed producer identity in Evidence, and retain PTY, filesystem, lease and lazy-provider failure causes. Drain cancelled lazy loads and omit exited descendants from verified lineage.

* fix(browser): preserve cleanup and document-transition evidence

Make V8 and Playwright teardown cancellation-aware, expose primary failures alongside incomplete cleanup, share frame-tree and navigation rules, and clear stale WebMCP registrations without pruning unrelated child frames.

* fix(adapters): align managed targets and typed CLI outcomes

Inspect managed PE/CLI targets independently of native provider selection, reuse successful managed selections, and keep evidence bound to the actual inspected target. Classify canonical CLI error envelopes and exhaustive setup/update/uninstall outcomes, audit registered CLI coverage, and assert platform-specific runtime results.

* docs: refresh boundary contracts and shipped roadmap behavior
2026-10-06 00:50:38 +08:00
Rudy Mizrahi CelekliandGitHub f3e6582e9e fix(native): inspect Apple dispatch metadata in FAT64 binaries (#590)
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 00:32:31 +08:00
Kaoru0822andGitHub b37e47c9f7 fix(artifacts): break directory child collation ties (#587) 2026-10-06 00:24:15 +08:00
Rudy Mizrahi CelekliandGitHub 6aba6ae231 fix(native): decode Mach protection bits in their proper positions (#586)
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-06 00:14:37 +08:00
N0zoM1z0andGitHub ea9c3dab6b fix(mcp): return complete inline Evidence records (#585)
Return complete canonical Evidence records inline, including normalized_result, and preserve direct comparison and retained bundle identity.

Verified full deterministic gate, real Linux Ghidra CLI/MCP and independently installed artifact, generic package acceptance, private local samples, and targeted updates after syncing main.

Closes #551

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-06 00:10:31 +08:00
Kaoru0822andGitHub 205a977777 fix(package): separate agent and Hopper setup support (#584) 2026-10-06 00:00:15 +08:00
morlutoandGitHub 6fa6f1f9b1 fix(javascript): correct lexical and dynamic scope analysis (#538)
* fix(javascript): gate CommonJS exports on unshadowed globals and record dynamic scope

* fix(javascript): correct dynamic and switch scope boundaries
2026-10-05 23:42:48 +08:00
Kaoru0822andGitHub 00fa97422f fix(artifacts): retain expanded ASAR container parents (#583) 2026-10-05 23:39:12 +08:00
morlutoandGitHub ebb4c45b68 fix(artifacts): expand nested ASARs on Windows (#582) 2026-10-05 23:33:59 +08:00
morlutoandGitHub 28e0f4e9ec fix(artifacts): preserve archive entry types and host paths (#581) 2026-10-05 23:19:04 +08:00
morluto f73bb0fe11 test(setup): isolate client fixtures across host platforms 2026-10-05 23:13:56 +08:00
Kaoru0822andGitHub 348323dd23 fix(artifacts): retain relative paths in directory identities (#580) 2026-10-05 23:07:24 +08:00
Kaoru0822andGitHub 8eac290e8d fix(artifacts): resolve parents after archive enumeration (#578) 2026-10-05 23:06:46 +08:00
morlutoandGitHub 6256b68666 feat(setup)!: expand agent integrations and verify updates (#577)
* feat(setup)!: expand agent integrations and simplify onboarding

Add native configuration formats for OpenCode, Antigravity, GitHub Copilot
CLI, VS Code, and Devin. Preserve JSONC comments and unrelated settings
while making setup and uninstall ownership-aware and idempotent.

Select agent targets before planning, separate discovery from intent, and
keep agent-only setup independent of optional provider prerequisites.
Cover configuration boundaries and interactive behavior in packaged checks.

BREAKING CHANGE: unscoped setup refreshes only existing REA-owned
registrations. Use --all-detected to configure every detected client.
Setup JSON contains a compact doctor summary; use rea doctor for full
diagnostics. Cancellation and successful dry runs now exit successfully.

* feat(cli)!: replace upgrade with verified updates

Resolve an exact release through npm's configured registry, update only the
identified installation, and verify its new executable before reporting success.
Return a read-only maintenance plan for existing enabled REA integrations.

BREAKING CHANGE: replace rea upgrade with rea update; no alias is retained.
Successful update JSON uses status "updated", reports the verified installed
version, and returns an unapplied maintenance plan instead of opening setup.
2026-10-05 23:06:19 +08:00
N0zoM1z0andGitHub b225b0f64b fix(ghidra): recover evidenced switch cases and default targets (#579)
Correlate typed labels with recovered block entries and one indirect dispatch.
Preserve ambiguous and nonexact integer associations as unknown with raw diagnostics.
Expose defaults separately, collect backing load metadata, and invalidate old profiles.
Add independent compiler-byte CLI/MCP acceptance and actual Ghidra model controls.

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
Assisted-by: Suika (gensokyo-skills:suika-scatter-gather-planning)
2026-10-05 22:54:17 +08:00
Kaoru0822andGitHub bb91abd3df fix(javascript): prefer package entrypoints over directory indexes (#575) 2026-10-05 22:34:48 +08:00
Kaoru0822andGitHub 063cd25a93 fix(javascript): preserve literal CommonJS path punctuation (#576) 2026-10-05 22:28:51 +08:00
Kaoru0822andGitHub 52bf709bce fix(artifacts): exclude removed paths from unchanged counts (#569) 2026-10-05 21:42:56 +08:00
Kaoru0822andGitHub 3460eda9ca fix(conformance): preserve own JSON keys during comparison (#573) 2026-10-05 21:42:26 +08:00
Kaoru0822andGitHub f5013c50ca fix(artifacts): refresh cached ASAR headers for each inventory (#574) 2026-10-05 21:40:47 +08:00
morluto eba709e5c0 docs: align roadmap with direct tool use 2026-10-05 21:20:45 +08:00
morlutoandGitHub e07b81649e refactor!: simplify runtime tools and remove replay engines (#572)
* fix(hopper): report unavailable procedure body coverage

* refactor!: simplify runtime tools and remove replay engines

Keep direct inspection, capture, and comparison tools. Bind handlers to named contracts and let agents compose experiments with commands and local fixture servers.

BREAKING CHANGE: Remove controlled replay, Node characterization, finite replay-machine, managed runtime planning, and redundant string-tracing tools. Retire process replay, shims, reactive scenarios, custom checkpoints, and active browser replay and origin-scope inputs.
2026-10-05 21:15:04 +08:00
morluto c6d3bdf597 docs: place setup command above README image 2026-10-05 20:17:26 +08:00
morlutoandGitHub b5e98915c6 fix(tools)!: simplify direct invocation and harden boundary contracts (#555)
Remove REA permission grants, scope ceilings, approval confirmations, and inert settings. Preserve selected local evidence, accept caller-chosen commands and paths, and report actual runtime failures.

Harden provider results, schema serialization, snapshot evidence binding, archive paths, source maps, native screenshots, managed EH parsing, and cancellation cleanup. Regenerate tool metadata and document direct invocation.

BREAKING CHANGE: Removed permission configuration and policy commands, approval fields, filesystem scope inputs, and fixed confirmation options. Process capture inherits the host environment and accepts command names; callers must refresh tool schemas and migrate removed fields.
2026-10-05 20:16:44 +08:00
N0zoM1z0andGitHub 4f099a10f8 feat(ghidra): add DOS MZ analysis and complete function extents (#556)
Validate DOS MZ load modules and import them with Ghidra's 16-bit
real-mode profile. Preserve complete observed inclusive function body
ranges through inventory, function analysis, Evidence and snapshots.

Add a public source-generated DOS CLI/MCP verification lane and
regressions for malformed headers, body membership and cache profiles.

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
Assisted-by: Suika (gensokyo-skills:suika-scatter-gather-planning)
2026-10-05 20:09:45 +08:00
morlutoandGitHub 53fe7d6d3b refactor(config): take the environment as an input at every configuration read (#554)
* fix(platform): accept absolute paths for any host and stop blaming the ceiling for a missing grant

Addresses #548 and #545.

#548: processScenarioSchema required a leading "/" for executable,
working_directory and filesystem_roots, so no process scenario could be
expressed on Windows at all. The report suggested node:path's isAbsolute, but
that answers for the host running the check: on Linux `C:\tools` is not
absolute, so a scenario captured on Windows would fail validation when it is
replayed, compared, or asserted in a test on another host. A boundary schema
must not vary with its evaluator, so the check accepts the POSIX form, a
Windows drive path and a UNC share everywhere. The rejection message now names
the constraint instead of leaving the caller to infer it from "invalid request".
The HTTP `path` field inside `checkpoints` is an URL path and is deliberately
left alone.

I checked whether this merely moved the failure downstream: containment is
enforced by isPathWithinRoot, which already uses host-aware path.relative, so
validation really was the only blocker.

#545: when a scope is inside the administrator ceiling but no grant was ever
issued, the denial still pointed at widening the ceiling -- advice that cannot
help, since the ceiling already permits the operation. The `missing` payload is
computed against candidate *grants*, so it describes what a grant must cover,
not a ceiling gap. `PermissionRemediation` gains a `grant` variant saying
exactly that, and PermissionAuthority selects it when the denial is not a
ceiling violation. An explicitly requested remediation is still honoured, and a
genuine ceiling violation still says "configure".

* refactor(config): take the environment as an input at every configuration read

Completes B-2. Every `parseConfig(process.env)` site now reads from an
injected environment, so configuration-driven behaviour is a function of an
argument rather than of the machine running it.

The twelve sites were not equivalent, and treating them as one number would
have hidden the difference:

- **src/application/ (3 sites) were genuine defects.** `runSessionStatus`
  already injected its `logger` but hardcoded `process.env` for config, so the
  MCP path's `dependencies.env` never reached it; `captureProcessScenarioFile`
  and `runAnalysis` had the same problem. `runAnalysis` gained `environment` on
  the options object it already had, which is where its other injected
  dependencies live.

- **The CLI command layer (9 sites) is a documented boundary,** where
  `process.env` is legitimate. Rather than 9 independent ambient reads, they
  now share one seam: `createCli(environment)` passes it to the seven
  registrars that need configuration, and the nested helpers (`electronContext`,
  `runtimeContext`, `loadConfiguredAuthority`, `inspectJavaScriptReplay`) take
  it explicitly.

Every new parameter defaults to `process.env`, so all existing call sites are
unchanged and production behaviour is identical. The point is that the ambient
read is now a documented default at a boundary rather than a hidden dependency
deep inside behaviour.

`process.env` now survives in `src/` only as those default parameters —
nothing reads it at the point of use.

* style(cli): apply prettier formatting to the runtime observation command
2026-10-05 20:00:16 +08:00
morluto 778e464c20 docs: place Discord community below setup command 2026-10-05 19:57:30 +08:00
morluto ad86d39d2c docs: move README image above Discord community 2026-10-05 19:38:13 +08:00
morlutoandGitHub e793e124a2 fix(platform): accept absolute paths for any host and stop blaming the ceiling for a missing grant (#552)
Addresses #548 and #545.

#548: processScenarioSchema required a leading "/" for executable,
working_directory and filesystem_roots, so no process scenario could be
expressed on Windows at all. The report suggested node:path's isAbsolute, but
that answers for the host running the check: on Linux `C:\tools` is not
absolute, so a scenario captured on Windows would fail validation when it is
replayed, compared, or asserted in a test on another host. A boundary schema
must not vary with its evaluator, so the check accepts the POSIX form, a
Windows drive path and a UNC share everywhere. The rejection message now names
the constraint instead of leaving the caller to infer it from "invalid request".
The HTTP `path` field inside `checkpoints` is an URL path and is deliberately
left alone.

I checked whether this merely moved the failure downstream: containment is
enforced by isPathWithinRoot, which already uses host-aware path.relative, so
validation really was the only blocker.

#545: when a scope is inside the administrator ceiling but no grant was ever
issued, the denial still pointed at widening the ceiling -- advice that cannot
help, since the ceiling already permits the operation. The `missing` payload is
computed against candidate *grants*, so it describes what a grant must cover,
not a ceiling gap. `PermissionRemediation` gains a `grant` variant saying
exactly that, and PermissionAuthority selects it when the denial is not a
ceiling violation. An explicitly requested remediation is still honoured, and a
genuine ceiling violation still says "configure".
2026-10-05 18:50:10 +08:00
morluto 5b383b2ddd docs: align translated READMEs and clarify support guides 2026-10-05 18:39:28 +08:00
morluto 92617f9657 fix(docs): allow README installation wording to vary 2026-10-05 18:07:46 +08:00
morluto 42e6a8a6f6 docs: add boundary contract guidance 2026-10-05 17:55:33 +08:00
morluto 97822f233b docs: clarify README wording and analysis tool support 2026-10-05 17:52:22 +08:00
morlutoandGitHub 09830dad05 refactor(domain): consolidate duplicated semantic member and literal helpers (#550) 2026-10-05 17:40:20 +08:00
morlutoandGitHub 5b59b87726 refactor(host): inject platform and environment instead of reading them ambiently (#549) 2026-10-05 17:39:26 +08:00
morluto 69a45e0bbc docs: place community invitation below README navigation 2026-10-05 17:38:13 +08:00
morluto 79bf26f33f docs: add Discord community links to README 2026-10-05 17:36:29 +08:00
morlutoandGitHub ea58834940 refactor(domain): own digest and prefixed identifier shapes in one module (#547) 2026-10-05 17:29:20 +08:00
morlutoandGitHub 828395d510 fix(browser): ignore duplicate frame navigations and read block-form source map directives (#546) 2026-10-05 17:25:16 +08:00
N0zoM1z0andGitHub 84e4361c09 fix(inspector): preserve lossy Node discovery locations (#542)
Keep explicitly selected Node Inspector targets observable when discovery
cannot verify their file location. Resolve literal POSIX discovery paths
without URI decoding, and retain lossy Windows or underscore paths as
unresolved metadata. Validate scriptParsed file URLs independently.

Preserve unknown target identity through runtime reconciliation, regenerate
contracts, and verify real CLI/MCP behavior on Linux and Windows.

Closes #530

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-05 16:55:42 +08:00
morlutoandGitHub 05bb630a00 fix(replay): keep __proto__ own properties and reject symbol-keyed results (#540) 2026-10-05 16:54:06 +08:00
morlutoandGitHub 5b51fa01e2 fix(replay): share one seeded generator and present builtins with real identity (#539) 2026-10-05 16:47:20 +08:00
morlutoandGitHub 333d118c0f fix(javascript): classify assignment targets and retain compound-assignment reads (#537) 2026-10-05 16:16:57 +08:00
morlutoandGitHub 4c8dc39f43 fix(javascript): honor computed keys when naming callables, exports, requests and options (#536) 2026-10-05 15:52:06 +08:00
morlutoandGitHub d40502aa3b fix(native): match compound otool keys exactly and badge unknown thread entrypoints (#534) 2026-10-05 15:43:20 +08:00
N0zoM1z0andGitHub 4007b20fc2 fix(mcp): emit valid schemas for empty arrays (#535)
Replace empty tuples with a shared empty-array schema. Validate all input
and output schemas against Draft 2020-12 during catalog generation and
packaged MCP verification, with runtime and catalog regression coverage.

Closes #528

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-05 15:32:58 +08:00
morlutoandGitHub 3e4d4d93fd fix(native): normalize otool line endings and strip plist JSON BOM (#533) 2026-10-05 15:28:31 +08:00
morlutoandGitHub 0f77ada55e fix(native): retain lazy dylibs, normalize version-min builds, limit thread entrypoints (#532) 2026-10-05 15:23:08 +08:00
morlutoandGitHub 9e30d93af3 fix(native): reject trailing text in lipo integers and keep multi-word otool keys (#531) 2026-10-05 15:23:02 +08:00
c30878f616 fix(javascript): isolate lexical loop bindings (#511)
* fix(javascript): isolate lexical loop bindings

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

* test(javascript): isolate loop lexical names regression cases

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

---------

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-05 15:11:24 +08:00
6b7923cdd2 fix(javascript): retain property reads during updates (#508)
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-05 15:09:58 +08:00
2df6b51ebb fix(replay): preserve CommonJS object as ESM default export (#519)
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-05 15:05:43 +08:00
4200a425d5 fix(replay): link the complete ESM graph before evaluating (#515)
* fix(replay): link the complete ESM graph before evaluating

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

* fix(replay): rename shadowed module bindings in ESM link graph

---------

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-05 15:03:59 +08:00
9975bd23cd fix(replay): preserve Date call and explicit constructor semantics (#512)
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-05 15:02:47 +08:00
5b28a8b331 fix(workflows): preserve punctuation in filesystem source paths (#521)
* fix(workflows): preserve punctuation in filesystem source paths

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

* fix(workflows): share scheme URL pattern in path normalization

---------

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-05 15:01:12 +08:00
Rudy Mizrahi CelekliandGitHub 056c123759 fix(browser): resolve redirected source map paths from final URL (#518)
* fix(browser): resolve redirected source map paths from final URL

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

* test(browser): separate redirected source map boundary cases

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

---------

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-05 15:01:06 +08:00
Rudy Mizrahi CelekliandGitHub 4a5673e1dc fix(native): retain re-exported Mach-O library dependencies (#516)
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-05 14:59:48 +08:00
Rudy Mizrahi CelekliandGitHub 385445804a fix(workflows): retain container presence in export shape comparisons (#523)
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-05 14:59:35 +08:00
bcfb4072f2 fix(native): preserve text-valued load command metadata (#520)
* fix(native): preserve text-valued load command metadata

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

* fix(native): share numeric literal pattern between parsers

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-05 14:59:30 +08:00
Rudy Mizrahi CelekliandGitHub 23c4eba5b3 fix(artifacts): respect XML plist byte-order marks (#514)
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-05 14:59:21 +08:00
Rudy Mizrahi CelekliandGitHub 80777d96da fix(javascript): avoid invented computed destructuring properties (#507)
Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
2026-10-05 14:59:10 +08:00
Rudy Mizrahi CelekliandGitHub 2fd11961cf fix(cli): fail commands returning projected analysis errors (#506)
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-05 14:59:05 +08:00
Rudy Mizrahi CelekliandGitHub 55f0267c40 fix(javascript): bind named class expressions in their class scope (#510)
* fix(javascript): bind named class expressions in their class scope

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

* test(javascript): isolate class expression lexical names regression cases

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

---------

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-05 14:59:01 +08:00
Rudy Mizrahi CelekliandGitHub 488fd5f60b fix(browser): release cancelled CDP cleanup without waiting for responses (#517)
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-05 14:58:33 +08:00
Rudy Mizrahi CelekliandGitHub d8c18f604f fix(web): retain uncertainty for incomplete capture inventories (#513)
* fix(web): retain uncertainty for incomplete capture inventories

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

* test(web): keep incomplete capture cases in a focused suite

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

---------

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-05 14:58:24 +08:00
Rudy Mizrahi CelekliandGitHub 9c28f3c4c0 fix(browser): keep subresource redirects out of page navigation scope (#524)
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-05 14:58:14 +08:00
a8195ba2cc fix(native): capture Mach-O header metadata alongside load commands (#525)
* fix(native): capture Mach-O header metadata alongside load commands

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

* style(test): format native Mach-O header regression

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

* fix(native): drop dead bare otool override in header test

---------

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-05 14:50:58 +08:00
31ed005183 fix(win32): probe .cmd/.bat runtime shims via cmd.exe to avoid spawn EINVAL (#505)
* fix(win32): probe .cmd/.bat runtime shims via cmd.exe to avoid spawn EINVAL

Node >= 18.20 refuses to spawn .cmd/.bat files without a shell
(CVE-2024-27980 mitigation) and throws spawn EINVAL. On Windows, the
npm and npx PATH shims are .CMD files, so doctor's runtime toolchain
probes always failed even on a healthy install.

Route batch-file candidates through cmd.exe /d /s /c with the command
line as a single quoted argument and windowsVerbatimArguments.

* fix(win32): thread platform through version probe and harden batch quoting

- windowsBatchCommand takes platform/systemRoot params instead of reading
  process.platform directly, so it is unit-testable off Windows
- double embedded quotes in the cmd.exe /c string to block breakout
- replace spawn-based win32 tests with pure helper units plus a
  win32-only temp .bat probe

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-05 14:50:48 +08:00
Rudy Mizrahi CelekliandGitHub 6a431d806a fix(javascript): respect lexical shadowing of require (#509)
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-05 14:50:16 +08:00
Rudy Mizrahi CelekliandGitHub 9d1c3631b8 fix(replay): preserve sparse array positions in result projection (#522)
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-05 14:50:05 +08:00
morlutoandGitHub 405732a7f5 fix: correct native inventories and bridge error classification (#503)
* fix(hopper): preserve normalized inventory addresses

* test(hopper): isolate real inventory verification

* fix(native): preserve otool addresses and section alignment

* fix: preserve address spaces in call path evidence

* fix(hopper): distinguish request validation from provider faults

* fix(native): retain tool inventory facts and reject failed signatures
2026-10-04 17:25:09 +08:00
morlutoandGitHub 4fbc501121 feat(native): add inspection primitives, dispatch traces and approved UI observation (#500)
* fix(hopper): normalize string-backed addresses and update verification

* feat(native): add inspection primitives, dispatch traces and approved UI observation

* test: prioritize real artifact E2E and captured golden regressions

* fix(process): queue observed output before reactive rendering actions

* test: prune redundant cases and strengthen boundary assertions

* fix(cli): route native value tracing through shared workflows

* test: replace synthetic success with real workflows and captured data

* fix(catalog): separate portable metadata from native host availability
2026-10-04 16:02:36 +08:00
1414 changed files with 83590 additions and 42590 deletions
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
NODE_VERSION="24.18.0"
NODE_SHA256="55aa7153f9d88f28d765fcdad5ae6945b5c0f98a36881703817e4c450fa76742"
export PATH="/usr/local/bin:${PATH}"
if [[ "$(/usr/local/bin/node -v 2>/dev/null || true)" != "v${NODE_VERSION}" ]]; then
tmp="$(mktemp -d)"
curl -fsSL -o "${tmp}/node.tar.xz" "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz"
echo "${NODE_SHA256} ${tmp}/node.tar.xz" | sha256sum -c -
sudo tar -xJf "${tmp}/node.tar.xz" -C /usr/local --strip-components=1
rm -rf "${tmp}"
fi
hash -r
for cmd in node npm npx corepack; do
if [[ -x "/usr/local/bin/${cmd}" ]]; then
sudo ln -sfn "/usr/local/bin/${cmd}" "/usr/local/cargo/bin/${cmd}"
fi
done
if [[ -f "${HOME}/.bashrc" ]] && ! grep -qF '# rea-dev-node' "${HOME}/.bashrc"; then
printf '\n# rea-dev-node\nexport PATH="/usr/local/bin:$PATH"\n' >> "${HOME}/.bashrc"
fi
npm ci
+4
View File
@@ -0,0 +1,4 @@
{
"name": "REA",
"install": "bash .cursor/cloud-agent-install.sh"
}
+82 -20
View File
@@ -77,6 +77,8 @@ jobs:
- run: npm ci
- if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
run: npm run check:ci
- if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
run: npm run verify:test-discovery
- if: github.event_name == 'pull_request' && needs.changes.outputs.code != 'true'
run: npm run format:check
@@ -105,7 +107,7 @@ jobs:
if: ${{ !cancelled() }}
with:
name: vitest-report-${{ strategy.job-index }}
path: .vitest-reports/*
path: .vitest/blob/*
include-hidden-files: true
if-no-files-found: error
overwrite: true
@@ -157,7 +159,7 @@ jobs:
- uses: actions/download-artifact@v4
if: needs.changes.outputs.code == 'true' && success()
with:
path: .vitest-reports
path: .vitest/blob
pattern: vitest-report-*
merge-multiple: true
- name: Merge coverage, JUnit, and timing reports
@@ -215,10 +217,31 @@ jobs:
- name: Verify fixtures from a clean checkout
run: npm run verify:fixtures
package-e2e:
name: Package E2E (${{ matrix.os }})
windows-native-build:
name: Build bundled Windows x64 native controls
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Prepare cross compiler
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
- run: npm run build:windows-native
- run: npm run verify:windows-native:artifact
- uses: actions/upload-artifact@v4
with:
name: windows-native-controls
path: native/windows/build/
if-no-files-found: error
package-e2e:
name: Package/install integration (${{ matrix.os }})
needs: [changes, windows-native-build]
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
strategy:
fail-fast: false
matrix:
@@ -231,8 +254,35 @@ jobs:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- uses: actions/download-artifact@v4
with:
name: windows-native-controls
path: native/windows/build/
- run: npm run verify:windows-native:artifact
- run: npm run verify:package
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
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run verify:keyed-archive
- run: npm run verify:asset-catalog
- name: Native compiled NIB hierarchy regressions
run: npm run test:focused -- src/application/NibNativeHierarchy.test.ts
- name: Real binary metadata and Interface Builder integration
run: npm run verify:apple-dispatch && npm run verify:interface-builder
- name: Native plist CLI selection regressions
run: npm run test:focused -- tests/boundary/cli/nativePlistCli.test.ts
reconstruction-readiness:
name: Reconstruction readiness
needs: changes
@@ -263,10 +313,14 @@ jobs:
retention-days: 30
javascript-runtime-observation:
name: JavaScript runtime observation
name: JavaScript runtime observation (${{ matrix.os }})
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
@@ -280,7 +334,7 @@ jobs:
windows-curated:
name: Windows capability and package lane
needs: changes
needs: [changes, windows-native-build]
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
runs-on: windows-latest
timeout-minutes: 25
@@ -293,6 +347,11 @@ jobs:
- run: npm ci
- run: npm run build:cached
- run: npm run typecheck
- uses: actions/download-artifact@v4
with:
name: windows-native-controls
path: native/windows/build/
- run: npm run verify:windows-native:artifact
- name: Record named Windows host capabilities
run: node scripts/windows-capabilities.mjs | Out-File -Encoding utf8NoBOM windows-capabilities.json
- name: Validate Windows host capability report
@@ -305,29 +364,37 @@ jobs:
# ignores unmatched paths, so a stale entry would otherwise report green
# while skipping coverage.
#
# Every path here must pass on windows-latest WITHOUT verified Windows
# native authority. src/ghidra/GhidraProvider.test.ts,
# src/ghidra/GhidraProvider.behavior.test.ts successors, and
# tests/boundary/process/providerProcess.test.ts are deliberately absent:
# they require Job Object process ownership and private-runtime DACL
# enforcement, which this runner does not provide by design. Those claims
# belong behind the same approval boundary as the real Ghidra Windows
# workflow, not in the default lane.
# The matching native artifact establishes real Job Object and private
# runtime boundaries for these fixtures. Ghidra and Java are not needed;
# real engine claims remain in the separate controlled-provider workflow.
- name: Run curated Windows capability tests
run: >-
npx vitest run --coverage.enabled=false
tests/boundary/filesystem/binaryTarget.test.ts
tests/boundary/filesystem/safeOutputTree.test.ts
tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts
tests/boundary/filesystem/setupUnavailableProvider.test.ts
tests/boundary/cli/referenceSourceImportStatus.test.ts
src/cliReferenceSourceImportStatus.test.ts
tests/boundary/filesystem/asarInventoryFreshness.test.ts
tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts
tests/conformance/providers/ghidra/windowsCapabilities.test.ts
tests/conformance/providers/ghidra/ghidraWindowsFixture.test.ts
src/ghidra/GhidraInstallation.test.ts
tests/boundary/providers/ghidra/ghidraTransport.test.ts
tests/boundary/providers/ghidra/ghidraTargetSnapshot.test.ts
tests/boundary/providers/ghidra/ghidraLauncher.test.ts
src/windows/WindowsNativeLoader.test.ts
src/windows/WindowsOwnedProcess.test.ts
src/ghidra/GhidraLauncherEnvironment.test.ts
src/process/ProcessOwnership.test.ts
src/process/ProcessOwnership.part2.test.ts
src/process/ProcessOwnership.part3.test.ts
src/process/ProcessOwnership.validation.test.ts
src/application/Update.test.ts
tests/process-global/updateMaintenance.test.ts
tests/conformance/providers/ghidra/realGhidraWindowsWorkflow.test.ts
- run: npm run verify:windows-native
- run: npm run verify:package:windows
- uses: actions/upload-artifact@v4
if: always()
@@ -348,8 +415,3 @@ jobs:
cache: npm
- run: npm ci
- run: npm run docs:check
- uses: actions/upload-artifact@v4
with:
name: api-docs
path: build/api-docs
retention-days: 14
+44
View File
@@ -0,0 +1,44 @@
name: Source-built NativeAOT fixtures
on:
workflow_dispatch:
pull_request:
paths:
- "bridge/ghidra/extensions/nativeaot/**"
- "scripts/generate-nativeaot-fixtures.mjs"
- "tests/fixtures/nativeaot/**"
- ".github/workflows/nativeaot-fixtures.yml"
permissions:
contents: read
jobs:
windows-fixture:
name: Build Windows x64 NativeAOT fixture
# .NET 8.0.22's bundled linker discovery supports VS 2019/2022.
runs-on: windows-2022
timeout-minutes: 15
env:
DOTNET_CLI_TELEMETRY_OPTOUT: "1"
DOTNET_PROCESSOR_COUNT: "1"
COMPlus_GCHeapHardLimit: "30000000"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.416"
- name: Build benign fixture and independent byte oracle
run: node scripts/generate-nativeaot-fixtures.mjs
- uses: actions/upload-artifact@v4
with:
name: nativeaot-win-x64-${{ github.event.pull_request.head.sha || github.sha }}
path: |
_reference/nativeaot-integration/generated/win-x64/symbols/NativeAotFixture.exe
_reference/nativeaot-integration/generated/win-x64/NativeAotFixture.map
_reference/nativeaot-integration/generated/win-x64/symbols.txt
_reference/nativeaot-integration/generated/win-x64/oracle.json
if-no-files-found: error
retention-days: 7
+10
View File
@@ -5,10 +5,20 @@ on:
paths:
- ".github/workflows/real-browser.yml"
- "scripts/verify-real-browser.mjs"
- "scripts/verify-browser-network.mjs"
- "scripts/verify-browser-scripts.mjs"
- "scripts/lib/browser-*.mjs"
- "scripts/fixtures/browser-network-site.mjs"
- "scripts/fixtures/browser-script-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/application/WebScriptExportService.ts"
- "src/contracts/webScriptToolContracts.ts"
- "src/server/registerWebScriptTool.ts"
- "src/cli/webScriptCommands.ts"
- "package.json"
- "package-lock.json"
workflow_dispatch:
+33 -1
View File
@@ -11,9 +11,29 @@ permissions:
contents: read
jobs:
native-artifact:
name: Build matching Windows native artifact
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Prepare cross compiler
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
- run: npm run build:windows-native
- run: npm run verify:windows-native:artifact
- uses: actions/upload-artifact@v4
with:
name: real-ghidra-windows-native
path: native/windows/build/
if-no-files-found: error
verify:
name: Real Ghidra 12.1.4 Windows x64 PE proof
if: github.ref == 'refs/heads/main'
needs: native-artifact
environment: real-ghidra-windows
runs-on: [self-hosted, Windows, x64, ghidra-12-1-2]
timeout-minutes: 20
@@ -23,13 +43,25 @@ jobs:
with:
node-version-file: .nvmrc
- run: npm ci
- uses: actions/download-artifact@v4
with:
name: real-ghidra-windows-native
path: native/windows/build/
- run: npm run verify:windows-native:artifact
- run: npm run verify:windows-native
- name: Verify all admitted Windows Ghidra operations
env:
REA_ANALYSIS_PROVIDER: ghidra
run: npm run verify:ghidra:windows | Tee-Object -FilePath windows-ghidra-proof.log
- name: Verify ordinary-user CLI and MCP operations
env:
REA_ANALYSIS_PROVIDER: ghidra
run: npm run verify:ghidra:windows:package | Tee-Object -FilePath windows-ghidra-package-proof.log
- uses: actions/upload-artifact@v4
if: always()
with:
name: windows-ghidra-proof
path: windows-ghidra-proof.log
path: |
windows-ghidra-proof.log
windows-ghidra-package-proof.log
retention-days: 30
@@ -1,40 +0,0 @@
name: Real JavaScript replay verification
on:
pull_request:
paths:
- ".github/workflows/real-javascript-replay.yml"
- "scripts/verify-real-javascript-replay.mjs"
- "src/replay/**"
- "src/application/JavaScriptReplay*"
- "src/domain/javascriptReplay.ts"
- "tests/fixtures/replay/**"
- "package.json"
- "package-lock.json"
workflow_dispatch:
concurrency:
group: real-replay-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
HUSKY: "0"
permissions:
contents: read
jobs:
real-replay:
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: sudo apt-get update && sudo apt-get install -y bubblewrap
- run: npm run verify:replay
env:
REA_REPLAY_ALLOW_UNAVAILABLE: "true"
+16 -11
View File
@@ -108,6 +108,12 @@ jobs:
- run: npm ci
- name: Prepare Windows native build toolchain
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
- name: Build and verify bundled Windows native controls
run: npm run build:windows-native && npm run verify:windows-native:artifact
- name: Verify packaged release before publishing
run: npm run verify:package
@@ -129,23 +135,22 @@ jobs:
verification_root="$(mktemp -d)"
trap 'rm -rf -- "${verification_root}"' EXIT
cd "${verification_root}"
for attempt in $(seq 1 20); do
published_version="$(npm view "rea-agents@${version}" version --json 2>/dev/null || true)"
for attempt in $(seq 1 80); do
published_version="$(npm view --prefer-online "rea-agents@${version}" version --json 2>/dev/null || true)"
if [ "${published_version}" = "\"${version}\"" ]; then
if npm exec --yes --package="rea-agents@${version}" -- rea --help \
&& npm exec --yes --package="rea-agents@${version}" -- rea --llms \
&& node "${GITHUB_WORKSPACE}/scripts/verify-published-package.mjs" "${version}"; then
exit 0
fi
echo "Published rea-agents@${version} is visible, but CLI verification failed on attempt ${attempt}" >&2
echo "rea-agents@${version} is visible on npm after attempt ${attempt}; verifying the published CLI."
npm exec --yes --package="rea-agents@${version}" -- rea --help
npm exec --yes --package="rea-agents@${version}" -- rea --llms
node "${GITHUB_WORKSPACE}/scripts/verify-published-package.mjs" "${version}"
exit 0
else
echo "rea-agents@${version} is not visible on npm yet (attempt ${attempt}/20)" >&2
echo "rea-agents@${version} is not visible on npm yet (attempt ${attempt}/80; up to 20 minutes)" >&2
fi
if [ "${attempt}" -lt 20 ]; then
if [ "${attempt}" -lt 80 ]; then
sleep 15
fi
done
echo "Published rea-agents@${version} failed registry and CLI verification" >&2
echo "rea-agents@${version} did not become visible on npm within 20 minutes" >&2
exit 1
publish-mcp:
+4 -2
View File
@@ -6,8 +6,7 @@ __pycache__/
# Distribution
dist/
build/
docs/api/
node_modules/
node_modules
*.egg-info/
*.egg
@@ -42,3 +41,6 @@ test-results.xml
# OS
.DS_Store
Thumbs.db
# Local reference sources and analysis artifacts
/_reference/
+12
View File
@@ -0,0 +1,12 @@
[submodule "third_party/jadx-headless-mcp"]
path = third_party/jadx-headless-mcp
url = https://github.com/1013503897/jadx-headless-mcp.git
[submodule "third_party/binwalk"]
path = third_party/binwalk
url = https://github.com/ReFirmLabs/binwalk.git
[submodule "third_party/unblob"]
path = third_party/unblob
url = https://github.com/onekey-sec/unblob.git
[submodule "third_party/ghidra-nativeaot"]
path = third_party/ghidra-nativeaot
url = https://github.com/Washi1337/ghidra-nativeaot.git
+3 -2
View File
@@ -1,6 +1,6 @@
{
"_comment": "Test files are intentionally NOT excluded: the gate used to report zero clones because it skipped **/*.test.ts, hiding all test-tree duplication. Current actual is ~2.6% lines across src+ and tests/, down from 3.4% when tests were included. Lower this as duplication is removed.",
"threshold": 2.7,
"_comment": "Test files are intentionally NOT excluded: the gate used to report zero clones because it skipped **/*.test.ts, hiding all test-tree duplication. Current actual is ~2.4% lines across src+ and tests/, down from 3.4% when tests were included. Lower this as duplication is removed.",
"threshold": 2.5,
"reporters": ["consoleFull"],
"ignore": [
"node_modules/**",
@@ -9,6 +9,7 @@
".codex/**",
".devin/**",
"tests/fixtures/**",
"third_party/**",
"tests/conformance/**/*.c",
"tests/conformance/**/*.m",
"tests/conformance/**/*.swift",
+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 80,
"sortImports": false,
"sortPackageJson": false,
"ignorePatterns": [
"dist/**",
"build/**",
"docs/api/**",
"node_modules/**",
"CHANGELOG.md",
"package-lock.json",
"_reference/**",
"third_party/**"
]
}
+110 -7
View File
@@ -6,17 +6,34 @@
"max-depth": ["warn", 4],
"max-lines": [
"warn",
{ "max": 500, "skipBlankLines": true, "skipComments": true }
{
"max": 500,
"skipBlankLines": true,
"skipComments": true
}
],
"max-lines-per-function": [
"warn",
{ "max": 100, "skipBlankLines": true, "skipComments": true }
{
"max": 100,
"skipBlankLines": true,
"skipComments": true
}
],
"max-params": ["warn", 4],
"no-var": "warn",
"no-var-requires": "error"
},
"overrides": [
{
"files": [
"scripts/electron-active-hook*.cjs",
"tests/conformance/readiness/electron/*.js"
],
"rules": {
"no-var-requires": "off"
}
},
{
"files": [
"src/{contracts,domain,server}/**/*.ts",
@@ -29,8 +46,74 @@
{
"patterns": [
{
"group": ["**/ghidra/**", "**/hopper/**"],
"message": "Deep-provider imports belong behind adapters or in the application composition root."
"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."
}
]
}
@@ -43,7 +126,11 @@
"max-lines": "off",
"max-lines-per-function": [
"error",
{ "max": 150, "skipBlankLines": true, "skipComments": true }
{
"max": 150,
"skipBlankLines": true,
"skipComments": true
}
]
}
},
@@ -62,9 +149,13 @@
"**/browser/**",
"**/ghidra/**",
"**/hopper/**",
"**/ida/**",
"**/replay/**",
"**/cli.*",
"**/main.*"
"**/main.*",
"**/composition/**",
"**/android/**",
"**/firmware/**"
],
"message": "Domain tests may import only their owning domain and inward dependencies."
}
@@ -120,5 +211,17 @@
"node": true,
"es2023": true
},
"ignorePatterns": ["dist", "node_modules", "docs/api", ".codex", ".devin"]
"ignorePatterns": [
"dist",
"build",
"node_modules",
"docs/api",
"coverage",
".turbo",
".cache",
".codex",
".devin",
".factory",
"third_party"
]
}
-5
View File
@@ -1,5 +0,0 @@
dist
docs/api
node_modules
CHANGELOG.md
package-lock.json
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "3.2.1"
".": "4.1.0"
}
+44 -8
View File
@@ -2,7 +2,7 @@
## Product Direction
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper and the bring-your-own Ghidra adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and has an experimental Windows x64 P0 boundary for approved native x86-64 PE applications; it supplies admitted read-only inventory and function-analysis operations but no GUI or mutation authority. Keep provider-specific code out of the domain and application layers.
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper, the bring-your-own Ghidra adapter, and the bring-your-own IDA MCP adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86-64 PE applications; on Linux and macOS it supplies inventory, function analysis, and atomic function annotation edits in an ephemeral database, without modifying executable bytes or controlling a GUI. Windows P0 has no mutation authority. IDA adapts upstream legacy attached GUI and modern headless database-supervisor profiles for live read-only analysis; initial real verification covers Windows, and an attached GUI database is never saved or closed. Keep provider-specific code out of the domain and application layers.
Prioritize:
@@ -13,14 +13,14 @@ Prioritize:
Installers must not install or upgrade Homebrew, Node.js, npm, Java, Ghidra, or other unrelated software. Ghidra is bring-your-own. `rea setup` must print its planned changes and require approval before writing files or installing Hopper.
REA is a local-only tool; do not sanitize actionable local diagnostics such as artifact paths, digests, mismatch locations, or analysis metadata, while continuing to redact genuine secrets such as credentials and authorization headers.
REA is a local-only tool. Preserve caller-selected inputs, captured output, URLs, paths, digests, mismatch locations, and analysis metadata. Do not guess that local evidence is secret from environment-variable names, argument names, or text patterns. Redact transport authentication credentials and values the caller explicitly marks sensitive; do not persist the entire ambient environment merely because a child inherits it.
## Project Structure & Module Organization
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/browser/`, `src/replay/`, `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/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.
@@ -30,9 +30,12 @@ REA is a layered ESM TypeScript application. Dependencies flow inward from pure
- `npm ci`: install the locked dependencies.
- `npm run build:cached`: build the CLI and MCP server.
- `npm run test:local`: run focused tests for changed code.
- `npm run test:local`: run changed source tests without building; pass exact source test paths to run them regardless of Git status.
- `npm run test:focused -- PATH...`: run exact test files; build first for boundary, acceptance, or process-global tests.
- `npm run check:changed`: run cached static checks and source tests affected since the branch merge base (default `origin/main`).
- `npm run check:fast`: run cached typecheck and lint checks.
- `npm run check:pr`: run the deterministic PR gate and generated-document checks.
- `npm run check:pr`: opt into the complete local deterministic gate and generated-document checks for broad changes; CI owns full coverage. Routine iterations need focused tests and relevant checks, not the whole gate each time.
- `npm run docs:check`: check committed generated documents; `npm run docs:generate` regenerates them.
- For provider-dependent changes, see [docs/testing.md](docs/testing.md) and run the matching real-provider verification.
- Keep each verification lane's prerequisites limited to the claim it checks. Use host-native fixtures for host/provider acceptance; put optional cross-target formats and their external toolchains in a separate lane. Preflight required commands and report the missing dependency and lane clearly.
@@ -44,12 +47,28 @@ Configuration is parsed and validated by `src/config.ts` and `src/config/`. Keep
## Coding Style & Naming Conventions
Use ESM TypeScript, two-space indentation, and Prettier defaults. Keep compiler strictness intact (`strict`, `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`, `verbatimModuleSyntax`). Use `camelCase` for values/functions, `PascalCase` for classes/types, and `UPPER_SNAKE_CASE` for constants. Parse unknown values at every MCP, environment, and subprocess boundary. Avoid `any`, unchecked casts, non-null assertions, import-time I/O, and floating promises. Exported APIs require concise JSDoc. Model expected failures with the tagged error algebra and `Result`, not broad exception wrappers.
Use ESM TypeScript, two-space indentation, and the committed Oxfmt configuration. Keep compiler strictness intact (`strict`, `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`, `verbatimModuleSyntax`). Use `camelCase` for values/functions, `PascalCase` for classes/types, and `UPPER_SNAKE_CASE` for constants. Parse unknown values at every MCP, environment, and subprocess boundary. Avoid `any`, unchecked casts, non-null assertions, import-time I/O, and floating promises. Exported APIs require concise JSDoc. Model expected failures with the tagged error algebra and `Result`, not broad exception wrappers.
## Boundary Contracts
Treat a boundary as a contract between the producer's actual representation and the consumer's required meaning. When implementing or auditing a boundary, trace the value through parsing, normalization, authorization, serialization, and the CLI/MCP result. Establish affected callers from their code paths; similar tool names or workflows do not prove that they share a schema or failure mode.
Keep portable evidence and scenario validation distinct from host-native execution checks. Absolute filesystem paths, file URLs, and HTTP paths have different semantics; do not substitute one platform's syntax for the domain concept. Interpret provider metadata according to its documented or observed producer behavior. When a transformation loses information, preserve the reported value and an explicit unknown rather than guessing a canonical identity.
Preserve meaningful failure reasons through application and adapter layers. Malformed input is distinct from an unsupported target, unavailable provider, or host operating-system permission denial. Diagnostics should identify the failed constraint and the target or lifecycle request it applies to. Recovery advice must address that reason and point to an available workflow; generic catches must not erase actionable validation details.
Leave meaningful target, action, capture, and output choices to the agent. A selected operation already expresses intent; do not require approval booleans or repeated permission declarations. Trace each setting to its consumer: remove ignored options and single-value confirmations, derive built-in lifecycle behavior, and supply defaults for omitted optional metadata. Report actual effects and limitations where they help interpret results rather than asking callers to restate them.
Verify the representation consumed at the next boundary. Internal validator success does not establish JSON Schema validity or client compatibility: validate advertised input and output schemas against their declared dialect after SDK conversion, and keep generated contracts aligned. Use representative producer data and regressions that exercise the failed behavior. Platform and provider support claims require the corresponding real workflow; package startup, capability probes, and mock transport tests establish only their narrower claims. Report unverified coverage explicitly.
Bind tool handlers to named contracts, preserving their exact input and output types. Catalog ordering is presentation metadata; changing that order must not select a different schema or operation for an existing handler.
## Designing MCP Tools
Start from the analyst question and desired result, not a provider API. Before adding a tool, inspect the existing contract and its nearest alternative.
Let agents compose experiments with ordinary commands, scripts, and local fixture servers. REA tools should perform the requested inspection or capture and return evidence. A custom orchestration language, replay engine, or separate prepare/execute plan needs an observed requirement that those primitives cannot satisfy. A plan-only tool without an executor does not establish runtime behavior; expose the useful inspection directly.
- Prefer reusable, composable primitives: inspect one explicit object or
relationship and return evidence that can support different analyst
questions. Compose primitives into a workflow only for a recurring outcome
@@ -57,7 +76,7 @@ Start from the analyst question and desired result, not a provider API. Before a
business-domain interpretations out of general tool contracts.
- Use **inspect/search** tools for facts about a target or candidate set; use **trace** tools for relationships; use **compare** tools for explicitly paired inputs.
- Add a **workflow** when observed agent use shows a repeated sequence that REA can compose without losing analyst control. Return useful results inline so callers can choose their next action.
- Use **observe/capture/replay** only when runtime activity is required, and declare authority and lifecycle effects in the contract.
- Use **observe/capture** only when runtime activity is required, and declare authority and lifecycle effects in the contract.
- Extend an existing tool when intent and result contract are unchanged. Add a tool for a distinct analyst outcome or materially different authority.
- Keep caller-facing names and results provider-neutral. Put engine-specific behavior in provider adapters and report each provider's exact coverage.
- Keep results complete by default. Add a limit only when it follows from a real format, protocol, authority, or resource-safety constraint; explain truncation and unsupported facets. Keep observed, derived, inferred, and unknown results distinct.
@@ -68,10 +87,27 @@ See [docs/tool-design.md](docs/tool-design.md) for the design checklist. When us
## Testing Guidelines
Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use fake launcher/socket seams; MCP tests connect with the client SDK version pinned in `package.json`. Preserve the canonical tool inventory defined by `TOOL_CONTRACTS` and verified through `CATALOG_IDENTITY` and generated product metadata. Cover malformed input, cancellation, lifecycle cleanup, and actual format, protocol, and permission boundaries. Do not add tests that merely freeze arbitrary caps or prescribed call sequences. Real Hopper, Ghidra, browser, JavaScript replay, managed conformance, and any real managed-tool claims cannot be replaced by mocks; use the corresponding `verify:*` command.
Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use fake launcher/socket seams; MCP tests connect with the client SDK version pinned in `package.json`. Preserve the canonical tool inventory defined by `TOOL_CONTRACTS` and verified through `CATALOG_IDENTITY` and generated product metadata. Cover malformed input, cancellation, lifecycle cleanup, and actual format, protocol, host-permission, and target-identity boundaries. Do not add tests that merely freeze arbitrary caps or prescribed call sequences. Real Hopper, Ghidra, browser, managed conformance, and any real managed-tool claims cannot be replaced by mocks; use the corresponding `verify:*` command.
Keep tool catalogs complete and self-describing; prefer capability- and session-scoped availability over schema truncation. Serialized bytes alone do not measure agent usability or model context cost.
## 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. `.cursor/cloud-agent-install.sh` installs that 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/`).
<!-- BEGIN:turborepo-agent-rules -->
# This is NOT the Turborepo you know
Turborepo configuration, task behavior, and CLI commands can vary between installed versions and may differ from your training data. Resolve the `turbo` package from this file's directory or relevant workspace; in monorepos, it may not be visible from the repository root. For example, run `node -p "require.resolve('turbo/package.json')"` from a workspace that depends on `turbo`.
Read `docs/README.md` inside that installed package first, then read the relevant pages from its `docs/` directory before changing Turborepo configuration or commands. Heed deprecation notices. These bundled docs match the installed package version and are available without network access.
This block is written and re-added by `turbo` before repository-scoped commands when an AI agent is detected. In the Turborepo source repository, its template is defined in `crates/turborepo-cli/src/cli/agent_guidance.rs`. Removing the managed block while updates are enabled means a later qualifying invocation will add it again. Set `"agentGuidance": false` in the root `turbo.json` or `turbo.jsonc` to opt out; this does not remove an existing block. Keep the block committed with your work to avoid an uncommitted change on the next agent invocation.
<!-- END:turborepo-agent-rules -->
+263
View File
@@ -1,5 +1,268 @@
# Changelog
## [4.1.0](https://github.com/morluto/rea/compare/rea-agents-4.0.1...rea-agents-4.1.0) (2026-10-06)
### Features
* **android:** integrate static APK analysis with headless JADX ([#738](https://github.com/morluto/rea/issues/738)) ([904d03e](https://github.com/morluto/rea/commit/904d03e987a6c498561b314a266202dee84b13ab))
* **evidence:** accept exact retained application references ([#734](https://github.com/morluto/rea/issues/734)) ([d0f3480](https://github.com/morluto/rea/commit/d0f3480a3cb9887499c2b188c783dd1c4db09787))
* **firmware:** integrate Binwalk and Unblob analysis ([5a7c769](https://github.com/morluto/rea/commit/5a7c769692c21efc4cc0e5ebf06a9f2534dc75ed))
* **ghidra:** add atomic function annotation edits ([#709](https://github.com/morluto/rea/issues/709)) ([9bab553](https://github.com/morluto/rea/commit/9bab55337ab10616be6929afddb08fb19fbb8ab7))
* **ghidra:** admit explicit DOS COM analysis ([#702](https://github.com/morluto/rea/issues/702)) ([5bcb664](https://github.com/morluto/rea/commit/5bcb6645b561e666b7e07dd8f38d2296fa7cbba8))
* **ghidra:** enable Windows x64 read-only analysis ([#699](https://github.com/morluto/rea/issues/699)) ([26881da](https://github.com/morluto/rea/commit/26881da76aa320c8529fcc741df0fc9898d97bdc))
* **ghidra:** integrate optional NativeAOT metadata recovery ([#748](https://github.com/morluto/rea/issues/748)) ([fb679b6](https://github.com/morluto/rea/commit/fb679b63c27e6c5f137532953cfa24a2ffb1aaf2))
* **ghidra:** verify DOS load images and expose memory evidence ([#697](https://github.com/morluto/rea/issues/697)) ([a6a8f67](https://github.com/morluto/rea/commit/a6a8f67649cab558b0de11601e65425f280c7170))
* **ida:** add read-only GUI and headless MCP providers ([#754](https://github.com/morluto/rea/issues/754)) ([8238232](https://github.com/morluto/rea/commit/82382324855a3a4ed51d2cdc3b2b455c9e8419e2))
* **setup:** support Command Code MCP registration ([#645](https://github.com/morluto/rea/issues/645)) ([7f1ae42](https://github.com/morluto/rea/commit/7f1ae42f71ce85547d317eed5e62fa330d9876bb))
### Bug Fixes
* **android:** validate explicit Java runtime ([#742](https://github.com/morluto/rea/issues/742)) ([7390390](https://github.com/morluto/rea/commit/739039092f98c67e8e6ccfb1f8445e8f13ac8f15))
* **artifacts:** make extraction output tree work on Windows ([#753](https://github.com/morluto/rea/issues/753)) ([c8e66c7](https://github.com/morluto/rea/commit/c8e66c704a70f5133922aa1bd1a3ba42959093ba))
* **browser:** classify CSP host sources by their own origin ([#707](https://github.com/morluto/rea/issues/707)) ([1d15bcb](https://github.com/morluto/rea/commit/1d15bcb9ecd7efa487a8fd3f0d9755bd60051722))
* **browser:** keep bare module locations unresolved ([#676](https://github.com/morluto/rea/issues/676)) ([8189081](https://github.com/morluto/rea/commit/8189081511441f0e3909b21b80c21c658ec9a453))
* **ci:** upload Vitest blob reports from current path ([77799e6](https://github.com/morluto/rea/commit/77799e6c90ce36cd52e45b62c9cba2d46641c4cf))
* **cli:** disable unmanaged skill synchronization ([433ebda](https://github.com/morluto/rea/commit/433ebda8b492f388faf6e6cac63ccc6f9c2d8d7e))
* **cli:** disable unmanaged skill synchronization ([2c1bf92](https://github.com/morluto/rea/commit/2c1bf9201040ed3d60f475af9129b7bf85a508fa))
* **cli:** fail when JSON input files cannot be parsed ([18c1829](https://github.com/morluto/rea/commit/18c18290d048b78a435f6864dfc778a9fea0af2e))
* **cli:** fail when JSON input files cannot be parsed ([3201f05](https://github.com/morluto/rea/commit/3201f0538dc88e54bce00decf689bf9b5603bfd3))
* **cli:** include build step in missing-runtime recovery ([6a37346](https://github.com/morluto/rea/commit/6a37346acb9829acaab0e2042624e6ea0d03e2a9))
* **cli:** include build step in missing-runtime recovery ([ae72abd](https://github.com/morluto/rea/commit/ae72abd019b25347bfc7593b5dcadc5a2cbc3786))
* **cli:** report reference-source import failures ([00eb03d](https://github.com/morluto/rea/commit/00eb03d0f2247da95dfa7bda0f41a0f366a25dd9))
* **cli:** report reference-source import failures ([431e8eb](https://github.com/morluto/rea/commit/431e8ebfcfba2925904c0deb4888bbd3755b17bf))
* **comparison:** break function collection collation ties ([c3d013b](https://github.com/morluto/rea/commit/c3d013b2fd5c3a32dfa73ef40dafdaeaf935f37e))
* **comparison:** break function collection collation ties ([3aaf7e2](https://github.com/morluto/rea/commit/3aaf7e2c54029a456145d6e3287fd9122f3abbe3))
* **comparison:** break JSON shape collation ties ([217e8cf](https://github.com/morluto/rea/commit/217e8cf59f49e2f68b394fbcbc87f914dbee97ee))
* **comparison:** break JSON shape collation ties ([805c338](https://github.com/morluto/rea/commit/805c338f360184d73ae335a723fca656830d6328))
* **comparison:** retain container-only export shape paths ([a55b5bb](https://github.com/morluto/rea/commit/a55b5bb8ee400ef5565954d57916e8aae7dac760))
* **comparison:** retain container-only export shape paths ([7864d66](https://github.com/morluto/rea/commit/7864d665e15edec29ade0f3fb7ac374b8579c2c0))
* **dotnet:** preserve leading Unicode content in metadata strings ([#667](https://github.com/morluto/rea/issues/667)) ([ddea063](https://github.com/morluto/rea/commit/ddea06360a09508a0ff695ab6e141eff19534619))
* **electron:** respect computed option keys and overrides ([af48fe8](https://github.com/morluto/rea/commit/af48fe879161ee749692751b3e29ba06b63b7409))
* **electron:** respect computed option keys and overrides ([98de338](https://github.com/morluto/rea/commit/98de3380e3062cb25778263bfea07b215a09b39c))
* **evidence:** admit current process provider identity ([ab5a1c6](https://github.com/morluto/rea/commit/ab5a1c61b12475c554a5e4d60b48b642b2464bbb))
* **evidence:** admit current process provider identity ([d311940](https://github.com/morluto/rea/commit/d311940f2414fe5cceb750de4464b681bb44ae1e))
* **firmware:** validate configured executables ([#751](https://github.com/morluto/rea/issues/751)) ([9a4e6da](https://github.com/morluto/rea/commit/9a4e6da5a96ed2708a0f628b436dcd7415999327))
* **ghidra:** preserve request and shutdown failure diagnostics ([#647](https://github.com/morluto/rea/issues/647)) ([19f8cdd](https://github.com/morluto/rea/commit/19f8cddfd6f283188567b8ff1e49b810d79952ee))
* **ghidra:** refuse ambiguous universal Mach-O imports ([b1b687f](https://github.com/morluto/rea/commit/b1b687fb75325c0275a0cf161843fc5969ba1d47))
* **ghidra:** refuse ambiguous universal Mach-O imports ([6582b24](https://github.com/morluto/rea/commit/6582b2433a955167e33246ad07de1a40bcb20144))
* **hopper:** retain shutdown request diagnostics ([8d7071d](https://github.com/morluto/rea/commit/8d7071da4c61c5cfb4894be6536405affd90f276))
* **hopper:** retain shutdown request diagnostics ([7254871](https://github.com/morluto/rea/commit/72548712fea972b920834663a5e2ceea4738ce1e))
* integrate 14 approved fix PRs (668,671,673,675,678-681,685-690) ([#705](https://github.com/morluto/rea/issues/705)) ([cf06115](https://github.com/morluto/rea/commit/cf061159ab69a746675e48896fa953d6990c05f9))
* **javascript:** count all source line terminators ([e60aa11](https://github.com/morluto/rea/commit/e60aa1110800e1309b482e7a44ecd1762bade992))
* **javascript:** count all source line terminators ([03bc6d3](https://github.com/morluto/rea/commit/03bc6d36db94e70c709557cb7302d40af9e99e6b))
* **javascript:** expose result schema failures ([6d1f39b](https://github.com/morluto/rea/commit/6d1f39b250f8a1c6f924b1a6b9d26d56dff4beee))
* **javascript:** expose result schema failures ([99263a7](https://github.com/morluto/rea/commit/99263a720d7106aff059ad7fa43849151a94c963))
* **javascript:** hash graph and evidence JSON incrementally ([#646](https://github.com/morluto/rea/issues/646)) ([9f42495](https://github.com/morluto/rea/commit/9f42495d11100adc8646de5c8b62762ab9715ae6))
* **javascript:** preserve path.resolve reset semantics ([863ed47](https://github.com/morluto/rea/commit/863ed47de2e35ef9b734e45e3bb44ea9e9dc8ff6))
* **javascript:** preserve path.resolve reset semantics ([4177377](https://github.com/morluto/rea/commit/417737748aa96ba38ced7b0b22dbb972187f2ee0))
* **javascript:** preserve primitive addition coercion ([#666](https://github.com/morluto/rea/issues/666)) ([94cc847](https://github.com/morluto/rea/commit/94cc8475e0e9e24cd2e8345bd36a743a979e3620))
* **javascript:** preserve unknown branches in primitive unions ([75a75cb](https://github.com/morluto/rea/commit/75a75cbb20aa9b2148f9fd0547f613eadda68edd))
* **javascript:** preserve unknown branches in primitive unions ([9a2f395](https://github.com/morluto/rea/commit/9a2f3950286a01922be0b07b29c4aefee466ec1e))
* **javascript:** read source-map directives only from comments ([#672](https://github.com/morluto/rea/issues/672)) ([7f1fa64](https://github.com/morluto/rea/commit/7f1fa64019e01a654ad0eb80b47d268a58f04e57))
* **javascript:** scope candidate ambiguity to query traversal ([6ad62ce](https://github.com/morluto/rea/commit/6ad62cec6abf182a52cf9844145e147fb6399c8e))
* **javascript:** scope candidate ambiguity to query traversal ([f94fc79](https://github.com/morluto/rea/commit/f94fc7967b2e5604c144d3638515caad6273a0c6))
* **mcp:** retain shutdown failure diagnostics ([09c249d](https://github.com/morluto/rea/commit/09c249deb86068503611fc474e76018d5c2a4d0b))
* **mcp:** retain shutdown failure diagnostics ([fd008cd](https://github.com/morluto/rea/commit/fd008cd8e0b8277e691efd8e69dc96ac8e188d9e))
* **native:** inspect mixed-signature Mach-O slices ([1dccd0d](https://github.com/morluto/rea/commit/1dccd0dee04abfdc80e8b48e3f05fa39cccf0feb))
* **native:** inspect mixed-signature Mach-O slices ([66f3cfa](https://github.com/morluto/rea/commit/66f3cfa594a86cbf1f21fc6d2eca1a82e236c896))
* **native:** retain spaced symbols in dyld inventory ([#669](https://github.com/morluto/rea/issues/669)) ([fa02df9](https://github.com/morluto/rea/commit/fa02df9ad3bc3139ef10422dedffac88c4d51d36))
* **native:** select concrete arm64e Mach-O slices ([f04445c](https://github.com/morluto/rea/commit/f04445c57c597a8312211139b75da706cb0c2a96))
* **native:** select concrete arm64e Mach-O slices ([fd3282a](https://github.com/morluto/rea/commit/fd3282a87e40417cf3d7d6211743809f9bad2dc8))
* **process:** honor disabled PID normalization in samples ([44b3627](https://github.com/morluto/rea/commit/44b3627ab0b32c42e545dd2fd095006d249a77f6))
* **process:** honor disabled PID normalization in samples ([a4e5736](https://github.com/morluto/rea/commit/a4e57366dd75077bde07f25c7966204cedad929e))
* **process:** scope comparison unknowns to their evidence ([#732](https://github.com/morluto/rea/issues/732)) ([9dd6cef](https://github.com/morluto/rea/commit/9dd6cef90abf89f9191bce942e31900870231968))
* **reconstruction:** accept function comparison source parameters ([c3bc38b](https://github.com/morluto/rea/commit/c3bc38bf6c74d2175a4680530dcadd6cab0e7791))
* **reconstruction:** accept function comparison source parameters ([7a96391](https://github.com/morluto/rea/commit/7a96391001780012f704f4ef5cb6597baa590b7d))
* **reference:** bound Dockerfile language detection ([132a3a4](https://github.com/morluto/rea/commit/132a3a46be648b036ed36372a50d6e32a9e7f2e5))
* **reference:** bound Dockerfile language detection ([53fa559](https://github.com/morluto/rea/commit/53fa55937f241fc9ef3091eb22314cc370c749b9))
* **reference:** classify test suffixes before extensions ([e828acc](https://github.com/morluto/rea/commit/e828acce0e0bf3808773590cf2dbb0bc8227df7b))
* **reference:** classify test suffixes before extensions ([8b228ae](https://github.com/morluto/rea/commit/8b228ae7010144b69a243c9682aeb0a1dc4249a3))
* **reference:** distinguish computed require member names ([ba35b55](https://github.com/morluto/rea/commit/ba35b5509db60c18dd43e350e0dd4a0b5f229c69))
* **reference:** distinguish computed require member names ([df6e476](https://github.com/morluto/rea/commit/df6e476d67b2719c4b998daa3e9ad8a4b157ace2))
* **reference:** preserve unresolved absolute module specifiers ([#670](https://github.com/morluto/rea/issues/670)) ([c8aed47](https://github.com/morluto/rea/commit/c8aed4740d52b1eecaa93b3a527e8b3814dc88a4))
* **reference:** read linked worktree commit provenance ([#677](https://github.com/morluto/rea/issues/677)) ([d565ecd](https://github.com/morluto/rea/commit/d565ecdc257514f5d0c5a412081e322fc66d4215))
* **reference:** recognize CMake build manifests ([#665](https://github.com/morluto/rea/issues/665)) ([1a24be5](https://github.com/morluto/rea/commit/1a24be574d1860931ff8d4c8ec07f3968fff9fd5))
* **reference:** retain dynamic import relationships ([a5d2986](https://github.com/morluto/rea/commit/a5d2986b1e517b71a1ed51be433c95e975eafe32))
* **reference:** retain dynamic import relationships ([b89de8f](https://github.com/morluto/rea/commit/b89de8feab389477a468f0b89b8dfa35d7ddc102))
* **reference:** retain recovered parser diagnostics ([11d0ef2](https://github.com/morluto/rea/commit/11d0ef2e09308ac06889865b3fcf356b8ffa647f))
* **reference:** retain recovered parser diagnostics ([ed3a7f0](https://github.com/morluto/rea/commit/ed3a7f070cf0793e22b9a0bbfd1f2ebe9dabcfd0))
* **setup:** accept leading BOM in JSON client configs ([a5b970c](https://github.com/morluto/rea/commit/a5b970c6bf6848145a82de51c001410aa4d50f6f))
* **setup:** accept leading BOM in JSON client configs ([fe843ec](https://github.com/morluto/rea/commit/fe843ec0b101544e22d7599d93b36377d3e3bc34))
* **setup:** align Node runtime checks with package engines ([5802ef8](https://github.com/morluto/rea/commit/5802ef8746412649bb641332f2fa7e69bb1a86a3))
* **setup:** align Node runtime checks with package engines ([ea0b733](https://github.com/morluto/rea/commit/ea0b733687582d6cc28b42beaf2fc7a48be07b6a))
* **target:** explain the JavaScript directory analysis route ([#733](https://github.com/morluto/rea/issues/733)) ([8d44b3e](https://github.com/morluto/rea/commit/8d44b3edd36b528c308e32328552ca386dc43e91))
* **trace:** match typed seeds against member arrays ([65c8797](https://github.com/morluto/rea/commit/65c879757ea09fabc5fc036d6e3395fd2939995c))
* **trace:** match typed seeds against member arrays ([38ebd54](https://github.com/morluto/rea/commit/38ebd54776d661ced2e6cc2c5e4b260653a086d9))
### Code Refactoring
* **app:** split Setup and ProcessOwnership modules ([#628](https://github.com/morluto/rea/issues/628)) ([3c84080](https://github.com/morluto/rea/commit/3c84080aab5e5aa6c9fc14bafa07c635b537b64d))
* **cli:** collapse duplicate CLI layer into src/cli ([#630](https://github.com/morluto/rea/issues/630)) ([494d4db](https://github.com/morluto/rea/commit/494d4db6d9d519f9258a25504cf29b07d61fbb9f))
* **contracts:** split tool contracts by family ([#634](https://github.com/morluto/rea/issues/634)) ([5295d56](https://github.com/morluto/rea/commit/5295d56b105074b290e0d881df43cd13e34a2f8b))
* **domain:** consolidate canonical ordering and digest helpers ([#624](https://github.com/morluto/rea/issues/624)) ([ba3869c](https://github.com/morluto/rea/commit/ba3869c597133b63fdcc0077c558923ae2e17a3f))
* **domain:** split error taxonomy into thematic modules ([#636](https://github.com/morluto/rea/issues/636)) ([735c100](https://github.com/morluto/rea/commit/735c10059ca161967d71ccf8fa3f62011d2a22ca))
* **native:** extract FAT header format switch in Mach-O slice selection ([#632](https://github.com/morluto/rea/issues/632)) ([38b2933](https://github.com/morluto/rea/commit/38b293329ca06c424a58bb59a28728f19015ce22))
* **runtime:** centralize safe JSON parsing and preserve error causes ([#629](https://github.com/morluto/rea/issues/629)) ([e7f54b3](https://github.com/morluto/rea/commit/e7f54b3310b7f45af11c1754e468d4d3804191db))
* **runtime:** name all silent catch causes without changing diagnostics ([#637](https://github.com/morluto/rea/issues/637)) ([5f27e02](https://github.com/morluto/rea/commit/5f27e02fae670774667de3618c88ba2ab0e7b661))
### Documentation
* add the official Trendshift badge to README headers ([b6c21b1](https://github.com/morluto/rea/commit/b6c21b1db5d4ff4be0968aa26693916537622a06))
* add the official Trendshift badge to README headers ([7d4ed59](https://github.com/morluto/rea/commit/7d4ed59816388409885310f40c12dfdcdd2d9246))
* align MCP discovery and skill guidance with the stable catalog ([#664](https://github.com/morluto/rea/issues/664)) ([39db9fe](https://github.com/morluto/rea/commit/39db9fe4245cc8142f57f4feeadaff64cb894e31))
* **browser:** add an interaction example that captures the visible result ([#750](https://github.com/morluto/rea/issues/750)) ([91daaa6](https://github.com/morluto/rea/commit/91daaa68aea3bbee02b837ac64d3ad13127d601e))
* clarify onboarding and align provider guidance ([#745](https://github.com/morluto/rea/issues/745)) ([5af1699](https://github.com/morluto/rea/commit/5af169910391f6b95492865247fabf3c4735a9d6))
* correct CLI examples and documentation checks ([#655](https://github.com/morluto/rea/issues/655)) ([fc80ea8](https://github.com/morluto/rea/commit/fc80ea88dfb7bde43e89db0f341c99d7b53c1a7c))
* fix architecture diagram and pretty-print tool catalog ([#626](https://github.com/morluto/rea/issues/626)) ([5e7f35c](https://github.com/morluto/rea/commit/5e7f35c7c3ea8d504421a2623eafd1d73fea08a0))
* **ghidra:** explain first-query deadlines and recovery ([#736](https://github.com/morluto/rea/issues/736)) ([e2c23f3](https://github.com/morluto/rea/commit/e2c23f3640f83c13587603c763dbd7b5ef1bbbb0))
* **hopper:** document HopperTargetLease realpath fallback ([#711](https://github.com/morluto/rea/issues/711)) ([d3f498c](https://github.com/morluto/rea/commit/d3f498ceec65e4f61f39036ab943ace7ef1b9de5))
* **installation:** pin manual MCP registration to the release version ([#658](https://github.com/morluto/rea/issues/658)) ([8dd1d84](https://github.com/morluto/rea/commit/8dd1d8447146b9962ec7ab2f245b0fe47d3ddf44))
* update Trendshift repository badge ([3f6d6dc](https://github.com/morluto/rea/commit/3f6d6dc756d99ca6e7cdc1737522842fcbaee839))
### Tests
* extract shared fixtures, drop knip carve-outs ([#627](https://github.com/morluto/rea/issues/627)) ([55e5eb0](https://github.com/morluto/rea/commit/55e5eb090bb3d6cf347d7cc2f8f028b35b7461ba))
* **hopper:** synchronize startup cancellation with launcher readiness ([#662](https://github.com/morluto/rea/issues/662)) ([88a3c3f](https://github.com/morluto/rea/commit/88a3c3f8a4bf5cb31f60bca4696431c4e4512b30))
## [4.0.1](https://github.com/morluto/rea/compare/rea-agents-4.0.0...rea-agents-4.0.1) (2026-10-05)
### Bug Fixes
* **release:** allow npm propagation time ([2ef02ce](https://github.com/morluto/rea/commit/2ef02ced1603809f0fc6984612de31dce1801dd8))
## [4.0.0](https://github.com/morluto/rea/compare/rea-agents-3.2.1...rea-agents-4.0.0) (2026-10-05)
### ⚠ BREAKING CHANGES
* Managed comparison metadata adds exact-signature matching and its count, and name_matching now reports exact-signature-fallback.
* **setup:** unscoped setup refreshes only existing REA-owned registrations. Use --all-detected to configure every detected client. Setup JSON contains a compact doctor summary; use rea doctor for full diagnostics. Cancellation and successful dry runs now exit successfully.
* Remove controlled replay, Node characterization, finite replay-machine, managed runtime planning, and redundant string-tracing tools. Retire process replay, shims, reactive scenarios, custom checkpoints, and active browser replay and origin-scope inputs.
* **tools:** Removed permission configuration and policy commands, approval fields, filesystem scope inputs, and fixed confirmation options. Process capture inherits the host environment and accepts command names; callers must refresh tool schemas and migrate removed fields.
### Features
* **ghidra:** add DOS MZ analysis and complete function extents ([#556](https://github.com/morluto/rea/issues/556)) ([4f099a1](https://github.com/morluto/rea/commit/4f099a10f819125d52bd8baed9a9018496212eab))
* **native:** add inspection primitives, dispatch traces and approved UI observation ([#500](https://github.com/morluto/rea/issues/500)) ([4fbc501](https://github.com/morluto/rea/commit/4fbc501121e96b33665a76cad2e5c3e8ac2e2c6d))
* **setup:** expand agent integrations and verify updates ([#577](https://github.com/morluto/rea/issues/577)) ([6256b68](https://github.com/morluto/rea/commit/6256b6866616fa0c3137a77ebd31cf34155a642f))
### Bug Fixes
* **artifacts:** break directory child collation ties ([#587](https://github.com/morluto/rea/issues/587)) ([b37e47c](https://github.com/morluto/rea/commit/b37e47c9f729a66008b1dc8b0a1bb266e79c2776))
* **artifacts:** exclude removed paths from unchanged counts ([#569](https://github.com/morluto/rea/issues/569)) ([52bf709](https://github.com/morluto/rea/commit/52bf709bcefb17ab07e1f9c47c5fadf5eca9bd4e))
* **artifacts:** expand nested ASARs on Windows ([#582](https://github.com/morluto/rea/issues/582)) ([ebb4c45](https://github.com/morluto/rea/commit/ebb4c45b6892ab60f2c81e05fbb4cb528ffbffc4))
* **artifacts:** interrupt ZIP extraction under backpressure ([#594](https://github.com/morluto/rea/issues/594)) ([50f7e81](https://github.com/morluto/rea/commit/50f7e81432001239ca8b5de3a9cfe8f5980d2711))
* **artifacts:** preserve archive entry types and host paths ([#581](https://github.com/morluto/rea/issues/581)) ([28e0f4e](https://github.com/morluto/rea/commit/28e0f4e9ec09ae35b26920e4c6925f48770bf5ed))
* **artifacts:** refresh cached ASAR headers for each inventory ([#574](https://github.com/morluto/rea/issues/574)) ([f5013c5](https://github.com/morluto/rea/commit/f5013c50ca81795f91fd945fae2fb76f9dbe0f97))
* **artifacts:** resolve parents after archive enumeration ([#578](https://github.com/morluto/rea/issues/578)) ([8eac290](https://github.com/morluto/rea/commit/8eac290e8d716817526ab7f628e83c75fb33d06b))
* **artifacts:** respect XML plist byte-order marks ([#514](https://github.com/morluto/rea/issues/514)) ([23c4eba](https://github.com/morluto/rea/commit/23c4eba5b38b8de4f9547d879f2c3e54f741b1e1))
* **artifacts:** retain expanded ASAR container parents ([#583](https://github.com/morluto/rea/issues/583)) ([00fa974](https://github.com/morluto/rea/commit/00fa97422f585db878ffe5d1613c56608916f972))
* **artifacts:** retain relative paths in directory identities ([#580](https://github.com/morluto/rea/issues/580)) ([348323d](https://github.com/morluto/rea/commit/348323dd23360bbac3841a9632ffcd08290566d2))
* **browser:** capture popup network and report early event gaps ([#614](https://github.com/morluto/rea/issues/614)) ([f557207](https://github.com/morluto/rea/commit/f557207887ca9336f3d3c5fc02c76c354c1b2945))
* **browser:** capture selected events in popup pages ([#600](https://github.com/morluto/rea/issues/600)) ([a3bf4a0](https://github.com/morluto/rea/commit/a3bf4a0944ce5a52e100470273b91fdd48ebce49))
* **browser:** derive source-map imports from syntax nodes ([#588](https://github.com/morluto/rea/issues/588)) ([bae87b4](https://github.com/morluto/rea/commit/bae87b4ded343b34c4226b5e5d491145db7f85ff))
* **browser:** distinguish duplicate screenshot frame events ([#612](https://github.com/morluto/rea/issues/612)) ([1bd641d](https://github.com/morluto/rea/commit/1bd641d39f58461566e4a3f07fa00f84f34e87ad))
* **browser:** ignore duplicate frame navigations and read block-form source map directives ([#546](https://github.com/morluto/rea/issues/546)) ([828395d](https://github.com/morluto/rea/commit/828395d510bd4f29525e5a0807a550a2daad885c))
* **browser:** keep subresource redirects out of page navigation scope ([#524](https://github.com/morluto/rea/issues/524)) ([9c28f3c](https://github.com/morluto/rea/commit/9c28f3c4c0840fd7424dc669db1fb9baa1dde5d7))
* **browser:** preserve RGB PNG transparent samples ([#595](https://github.com/morluto/rea/issues/595)) ([9997152](https://github.com/morluto/rea/commit/9997152c2d3127162161a2b945f506f0e834be38))
* **browser:** release cancelled CDP cleanup without waiting for responses ([#517](https://github.com/morluto/rea/issues/517)) ([488fd5f](https://github.com/morluto/rea/commit/488fd5f60be3b2900c27a915c73322b9cb5eabb5))
* **browser:** release discarded source-map response bodies ([#611](https://github.com/morluto/rea/issues/611)) ([8642055](https://github.com/morluto/rea/commit/864205570a0a9452f96c11feacd00338d026f17b))
* **browser:** resolve redirected source map paths from final URL ([#518](https://github.com/morluto/rea/issues/518)) ([056c123](https://github.com/morluto/rea/commit/056c1237591de3710cb96a51ddd71fa94317b906))
* **cli:** fail commands returning projected analysis errors ([#506](https://github.com/morluto/rea/issues/506)) ([2fd1196](https://github.com/morluto/rea/commit/2fd11961cfba0c2d211a1aa643cbf05fa4e8c20d))
* **cli:** reject invalid UTF-8 JSON file bytes ([#613](https://github.com/morluto/rea/issues/613)) ([22acb0d](https://github.com/morluto/rea/commit/22acb0d5d730379fbe49e9e491f446e11425190e))
* **comparison:** account for unpaired records after explicit bundle pairing ([#603](https://github.com/morluto/rea/issues/603)) ([12a2e37](https://github.com/morluto/rea/commit/12a2e3793af63542448124311e17eacbd5235fc0))
* **comparison:** match generated names without swallowing symbol prefixes ([#596](https://github.com/morluto/rea/issues/596)) ([5f3ba3e](https://github.com/morluto/rea/commit/5f3ba3e6886b6b3186711e025da14875dc3c9684))
* **comparison:** normalize CFG identity by numeric addresses ([#593](https://github.com/morluto/rea/issues/593)) ([d1e5c0e](https://github.com/morluto/rea/commit/d1e5c0e1fd04d2d5a0d663cbe43787a010e3d783))
* **comparison:** require decoded signatures for structural method matches ([#604](https://github.com/morluto/rea/issues/604)) ([4a98c3e](https://github.com/morluto/rea/commit/4a98c3ea035480561d138d79288974a860c2aa22))
* **conformance:** preserve own JSON keys during comparison ([#573](https://github.com/morluto/rea/issues/573)) ([3460eda](https://github.com/morluto/rea/commit/3460eda9ca01ccf41e3b9895f3cbe975c0eaed51))
* correct native inventories and bridge error classification ([#503](https://github.com/morluto/rea/issues/503)) ([405732a](https://github.com/morluto/rea/commit/405732a7f55e3033c29533b18f7d8313dbd28570))
* **docs:** allow README installation wording to vary ([92617f9](https://github.com/morluto/rea/commit/92617f965720aaa74ec20ad67291adc5e83677a1))
* **ghidra:** avoid nested quotes in Windows Java options ([a574b74](https://github.com/morluto/rea/commit/a574b744b8fe95e906524684f6a3b28cdc656499))
* **ghidra:** recover evidenced switch cases and default targets ([#579](https://github.com/morluto/rea/issues/579)) ([b225b0f](https://github.com/morluto/rea/commit/b225b0f64bfa86586c17f193d30e278d8124b14e))
* **inspector:** preserve lossy Node discovery locations ([#542](https://github.com/morluto/rea/issues/542)) ([84e4361](https://github.com/morluto/rea/commit/84e4361c0936a4bb30951699fb68970d4d7a2327)), closes [#530](https://github.com/morluto/rea/issues/530)
* **javascript:** avoid invented computed destructuring properties ([#507](https://github.com/morluto/rea/issues/507)) ([80777d9](https://github.com/morluto/rea/commit/80777d96daf64d0266e5cf3f9aad9338b97be4ff))
* **javascript:** bind named class expressions in their class scope ([#510](https://github.com/morluto/rea/issues/510)) ([55f0267](https://github.com/morluto/rea/commit/55f0267c409cbf096bcdbd07d7604c44bcde3fbe))
* **javascript:** classify assignment targets and retain compound-assignment reads ([#537](https://github.com/morluto/rea/issues/537)) ([333d118](https://github.com/morluto/rea/commit/333d118c0fa65c8c418baaa337270c0ad25cd7cc))
* **javascript:** correct lexical and dynamic scope analysis ([#538](https://github.com/morluto/rea/issues/538)) ([6fa6f1f](https://github.com/morluto/rea/commit/6fa6f1f9b17318220abb2683c327146785dcc5d3))
* **javascript:** honor computed keys when naming callables, exports, requests and options ([#536](https://github.com/morluto/rea/issues/536)) ([4c8dc39](https://github.com/morluto/rea/commit/4c8dc39f439b5283b077c6aa47f262714d7c4bbd))
* **javascript:** honor package exports condition order ([#591](https://github.com/morluto/rea/issues/591)) ([8289595](https://github.com/morluto/rea/commit/82895959d1494f16bc59909a2575facf3ab19d79))
* **javascript:** isolate lexical loop bindings ([#511](https://github.com/morluto/rea/issues/511)) ([c30878f](https://github.com/morluto/rea/commit/c30878f616c4496c6cd1993d677aa019b2412b7e))
* **javascript:** prefer package entrypoints over directory indexes ([#575](https://github.com/morluto/rea/issues/575)) ([bb91abd](https://github.com/morluto/rea/commit/bb91abd3df52802ec8f9d3d1fb215dce5eb6ea49))
* **javascript:** preserve literal CommonJS path punctuation ([#576](https://github.com/morluto/rea/issues/576)) ([063cd25](https://github.com/morluto/rea/commit/063cd25a93ee9f16b45635fea196dde0d9309ab9))
* **javascript:** preserve Node exports fallback stop reasons ([#606](https://github.com/morluto/rea/issues/606)) ([adc07f5](https://github.com/morluto/rea/commit/adc07f5eaab005f013c88617aa09b6f2b4af4111))
* **javascript:** respect HTML base URL components ([#589](https://github.com/morluto/rea/issues/589)) ([ffbde92](https://github.com/morluto/rea/commit/ffbde92f9ab65a868b660abf17ce9d3c4577d125))
* **javascript:** respect lexical shadowing of require ([#509](https://github.com/morluto/rea/issues/509)) ([6a431d8](https://github.com/morluto/rea/commit/6a431d806a1574a660b96ba49412b24df9e93b50))
* **javascript:** retain property reads during updates ([#508](https://github.com/morluto/rea/issues/508)) ([6b7923c](https://github.com/morluto/rea/commit/6b7923cdd2a099d1481783f34970a3d1a71a8206))
* **managed:** classify truncated signatures as malformed ([#598](https://github.com/morluto/rea/issues/598)) ([1b6f054](https://github.com/morluto/rea/commit/1b6f05470d6fb1903346ef7a25fb5f9d23079cb7))
* **managed:** distinguish metadata handles from member accesses ([#607](https://github.com/morluto/rea/issues/607)) ([c73742f](https://github.com/morluto/rea/commit/c73742f3003ec6c1469f77ecc9ccb093df636b4a))
* **managed:** recognize PE Thumb and ARMNT machines ([#605](https://github.com/morluto/rea/issues/605)) ([0ce529a](https://github.com/morluto/rea/commit/0ce529af4e0280fcbd4e647d0fcb0a9ca9f3496e))
* **mcp:** emit valid schemas for empty arrays ([#535](https://github.com/morluto/rea/issues/535)) ([4007b20](https://github.com/morluto/rea/commit/4007b20fc277b4d20b52cae328dea58ae033be63)), closes [#528](https://github.com/morluto/rea/issues/528)
* **mcp:** return complete inline Evidence records ([#585](https://github.com/morluto/rea/issues/585)) ([ea9c3da](https://github.com/morluto/rea/commit/ea9c3dab6b2ff4fa943c8ca4fec7ae8b304dbd93)), closes [#551](https://github.com/morluto/rea/issues/551)
* **native:** capture Mach-O header metadata alongside load commands ([#525](https://github.com/morluto/rea/issues/525)) ([a8195ba](https://github.com/morluto/rea/commit/a8195ba2cc34a37f68d7bb8a8087d73ace40984b))
* **native:** decode little-endian fat headers ([bede3e3](https://github.com/morluto/rea/commit/bede3e3c60e53d445209096b360a81eaa0c446cc)), closes [#592](https://github.com/morluto/rea/issues/592)
* **native:** decode Mach protection bits in their proper positions ([#586](https://github.com/morluto/rea/issues/586)) ([6aba6ae](https://github.com/morluto/rea/commit/6aba6ae2316fe6299c855a3db52b221fff63c00c))
* **native:** inspect Apple dispatch metadata in FAT64 binaries ([#590](https://github.com/morluto/rea/issues/590)) ([f3e6582](https://github.com/morluto/rea/commit/f3e6582e9ed327c5accb8f9068ea0eb9bac49b1f))
* **native:** inspect the selected universal Mach-O architecture ([#601](https://github.com/morluto/rea/issues/601)) ([2b7bb3b](https://github.com/morluto/rea/commit/2b7bb3bb6a87704c18469768bc82d87e97b5c99c))
* **native:** match compound otool keys exactly and badge unknown thread entrypoints ([#534](https://github.com/morluto/rea/issues/534)) ([d40502a](https://github.com/morluto/rea/commit/d40502aa3b9a8c87fde49e5887547066128f19ba))
* **native:** normalize otool line endings and strip plist JSON BOM ([#533](https://github.com/morluto/rea/issues/533)) ([3e4d4d9](https://github.com/morluto/rea/commit/3e4d4d93fdc3eb5fdffda3a8dc8bec43201cdd98))
* **native:** preserve text-valued load command metadata ([#520](https://github.com/morluto/rea/issues/520)) ([bcfb407](https://github.com/morluto/rea/commit/bcfb4072f2ff665e93477206e14ce4b0581f97b9))
* **native:** reject signal-terminated command captures ([#608](https://github.com/morluto/rea/issues/608)) ([b81d18e](https://github.com/morluto/rea/commit/b81d18eeee9d93dafcb15e3d723de951509c2612))
* **native:** reject trailing text in lipo integers and keep multi-word otool keys ([#531](https://github.com/morluto/rea/issues/531)) ([9e30d93](https://github.com/morluto/rea/commit/9e30d93af3d2c0cc1a7536a10e3141f9f2fb499c))
* **native:** retain lazy dylibs, normalize version-min builds, limit thread entrypoints ([#532](https://github.com/morluto/rea/issues/532)) ([0f77ada](https://github.com/morluto/rea/commit/0f77ada55e6d1d01cb925993d871bf4a94f94bb2))
* **native:** retain re-exported Mach-O library dependencies ([#516](https://github.com/morluto/rea/issues/516)) ([4a5673e](https://github.com/morluto/rea/commit/4a5673e1dc6f2213cf94121384cff405e390677a))
* **package:** separate agent and Hopper setup support ([#584](https://github.com/morluto/rea/issues/584)) ([205a977](https://github.com/morluto/rea/commit/205a9777776476b4f361e38f25d1c75baaccd420))
* **platform:** accept absolute paths for any host and stop blaming the ceiling for a missing grant ([#552](https://github.com/morluto/rea/issues/552)) ([e793e12](https://github.com/morluto/rea/commit/e793e124a27b8087057f58f39028144ea6f6e173))
* **process:** align provider detach with configured platform ([3fff74f](https://github.com/morluto/rea/commit/3fff74fb5bb9725be2456e8c39bc0b98181d0687))
* repair boundary contracts across CLI and MCP ([#597](https://github.com/morluto/rea/issues/597)) ([9c25dc4](https://github.com/morluto/rea/commit/9c25dc4ed2915a81fc83a1d4dc22e7ba814b8dff))
* **replay:** keep __proto__ own properties and reject symbol-keyed results ([#540](https://github.com/morluto/rea/issues/540)) ([05bb630](https://github.com/morluto/rea/commit/05bb630a00aa521229cde53ded9a5345b8bd1a2b))
* **replay:** link the complete ESM graph before evaluating ([#515](https://github.com/morluto/rea/issues/515)) ([4200a42](https://github.com/morluto/rea/commit/4200a425d50eab80847edf3baa9ea4c5244e8be8))
* **replay:** preserve CommonJS object as ESM default export ([#519](https://github.com/morluto/rea/issues/519)) ([2df6b51](https://github.com/morluto/rea/commit/2df6b51ebb87e8d406d6201d8f3158aefefa7cc5))
* **replay:** preserve Date call and explicit constructor semantics ([#512](https://github.com/morluto/rea/issues/512)) ([9975bd2](https://github.com/morluto/rea/commit/9975bd23cdba7e196ca97c3fcec41a6eee925e57))
* **replay:** preserve sparse array positions in result projection ([#522](https://github.com/morluto/rea/issues/522)) ([9d1c363](https://github.com/morluto/rea/commit/9d1c3631b8deccb5fb91906130f38e3f24aa254e))
* **replay:** share one seeded generator and present builtins with real identity ([#539](https://github.com/morluto/rea/issues/539)) ([5b51fa0](https://github.com/morluto/rea/commit/5b51fa01e2391cd4b6d5e9ba7b6aa7615de891b1))
* **repo:** keep dependency links out of version control ([#617](https://github.com/morluto/rea/issues/617)) ([b3223e3](https://github.com/morluto/rea/commit/b3223e3f61a23b79152939ef755aca95e7640f09))
* **skill:** remove obsolete grants and audit tool effects ([2919def](https://github.com/morluto/rea/commit/2919def828b3c8fc7947989f1de3f4f4ac4cdd36))
* **test:** improve capture reliability and runner cleanup ([#609](https://github.com/morluto/rea/issues/609)) ([d19686a](https://github.com/morluto/rea/commit/d19686a2a5122c691f7885997906ecb31dd9827d))
* **tools:** simplify direct invocation and harden boundary contracts ([#555](https://github.com/morluto/rea/issues/555)) ([b5e9891](https://github.com/morluto/rea/commit/b5e98915c60968213141f33839e0c5c4b34df4db))
* **web:** retain uncertainty for incomplete capture inventories ([#513](https://github.com/morluto/rea/issues/513)) ([d8c18f6](https://github.com/morluto/rea/commit/d8c18f604f292c95b6b0b185d23517cb6881c5fb))
* **win32:** probe .cmd/.bat runtime shims via cmd.exe to avoid spawn EINVAL ([#505](https://github.com/morluto/rea/issues/505)) ([31ed005](https://github.com/morluto/rea/commit/31ed005183c315f1bc72f6e10730a47be73d7f1c))
* **workflows:** preserve punctuation in filesystem source paths ([#521](https://github.com/morluto/rea/issues/521)) ([5b28a8b](https://github.com/morluto/rea/commit/5b28a8b331259e026678e681210e9c3c4f48f2fa))
* **workflows:** retain container presence in export shape comparisons ([#523](https://github.com/morluto/rea/issues/523)) ([3854458](https://github.com/morluto/rea/commit/385445804ab45605a47edcb996ebbc026e2f0a43))
### Code Refactoring
* **config:** take the environment as an input at every configuration read ([#554](https://github.com/morluto/rea/issues/554)) ([53fe7d6](https://github.com/morluto/rea/commit/53fe7d6d3b3630472124563ccafdad3f4964b4bc))
* **domain:** consolidate duplicated semantic member and literal helpers ([#550](https://github.com/morluto/rea/issues/550)) ([09830da](https://github.com/morluto/rea/commit/09830dad053f2b1440e0b1cde63747606684be4b))
* **domain:** own digest and prefixed identifier shapes in one module ([#547](https://github.com/morluto/rea/issues/547)) ([ea58834](https://github.com/morluto/rea/commit/ea5883494036ddb30d138fdb381d54b2d9808b49))
* **host:** inject platform and environment instead of reading them ambiently ([#549](https://github.com/morluto/rea/issues/549)) ([5b59b87](https://github.com/morluto/rea/commit/5b59b87726b459cf9887dd4edc176981ef4e6afc))
* simplify runtime tools and remove replay engines ([#572](https://github.com/morluto/rea/issues/572)) ([e07b816](https://github.com/morluto/rea/commit/e07b81649e96668e5ce166cfc6db3b59a7777697))
### Documentation
* add boundary contract guidance ([42e6a8a](https://github.com/morluto/rea/commit/42e6a8a6f6da4c3aebb10235a27d471dc2590954))
* add Discord community links to README ([79bf26f](https://github.com/morluto/rea/commit/79bf26f33f980c20367986080a16d4a9f7952048))
* align roadmap with direct tool use ([eba709e](https://github.com/morluto/rea/commit/eba709e5c096796b0afe3ce9203659df9eaf0a7d))
* align translated READMEs and clarify support guides ([5b383b2](https://github.com/morluto/rea/commit/5b383b2ddd4195cb15e412b81853955b38419721))
* clarify README wording and analysis tool support ([97822f2](https://github.com/morluto/rea/commit/97822f233be49a8b582b4c2aa982af3854ca0bb2))
* move README image above Discord community ([ad86d39](https://github.com/morluto/rea/commit/ad86d39d2cb4f2d1ce0da08a17d5f7948c0797ec))
* place community invitation below README navigation ([69a45e0](https://github.com/morluto/rea/commit/69a45e0bbc03d87624de866a821b976e4aaca069))
* place Discord community below setup command ([778e464](https://github.com/morluto/rea/commit/778e464c206ba75a2f94edaad3c05e8bf63bed10))
* place setup command above README image ([c6d3bdf](https://github.com/morluto/rea/commit/c6d3bdf597d9cd75a6f7396236e1a47f5ee4288d))
### Tests
* **setup:** isolate client fixtures across host platforms ([f73bb0f](https://github.com/morluto/rea/commit/f73bb0fe116678543a73d13f58eaa3b48133afc5))
## [3.2.1](https://github.com/morluto/rea/compare/rea-agents-3.2.0...rea-agents-3.2.1) (2026-10-03)
+50 -30
View File
@@ -2,15 +2,19 @@
REA welcomes focused bug fixes, documentation improvements, tests, and reverse-engineering workflow enhancements. Open an issue before a large contract or architecture change so its scope can be agreed before implementation.
For capability organization and provider composition, follow the incremental
[migration guide](docs/capability-migration.md). Run `npm run verify:test-discovery`
after adding or moving tests.
When adding or changing an MCP tool, follow the [tool design guide](docs/tool-design.md) and preserve the canonical contracts and generated catalog.
## Development setup
REA development requires Node.js 24.18.x and npm 11.16.x. 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+, 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.
```bash
npm ci
npm test
npm run check:fast
```
`npm ci` installs the exact dependencies and prepares the Husky hooks without
@@ -20,38 +24,46 @@ Turbo. Turbo caches deterministic builds and static checks across Git
worktrees. After a package, lockfile, or managed-skill version change, run
`npm run metadata:generate` before building.
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. Prefer capability- and session-scoped tool advertisement over schema truncation.
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`.
Before submitting a pull request, run:
## Development feedback and PR verification
For an ordinary edit, run the relevant regression and cached static checks:
```bash
npm run check:pr
npm run verify:package
npm pack --dry-run
npm run test:focused -- src/config.test.ts
npm run check:fast
```
`npm run check:fast` runs cached typecheck and lint for rapid local feedback.
`npm run check` adds formatting, dead-code, and package-metadata freshness
checks; use `npm run check:test` when the complete test suite is relevant.
Pre-commit formats then lints staged source files, and pre-push runs
`check:fast`. `check:pr` additionally renders API documentation and checks all
committed generated metadata. Real
provider and replay execution remains uncached, but their deterministic build
prerequisite uses Turbo. CI uploads the rendered TypeDoc site as an `api-docs`
artifact; the generated HTML is not committed.
`test:local` selects dirty source tests without building; explicit source test
paths run even on a clean tree. `check:changed` adds source tests affected since
the branch merge base with `origin/main`. Use `npm run test:changed -- --base
REVISION` to choose another base. Changed-test selection follows the import
graph and is feedback, not complete correctness evidence. Inspect relevant
boundary and provider behavior explicitly; see [docs/testing.md](docs/testing.md).
Local `npm test` runs every deterministic Vitest project without coverage,
retries, or verbose output. Focused boundary, acceptance, changed-test, and
watch commands are documented in [docs/testing.md](docs/testing.md), together
with the path-based behavioral-depth rules. Changed-test selection follows the
import graph and is feedback, not the release gate. CI splits the complete suite
across four native Vitest shards, then merges coverage, JUnit, and JSON timing
reports. Coverage thresholds remain in `vitest.config.ts`.
Before handing off a PR, run the relevant tests, `npm run check`, and
`npm run docs:check` when contracts or generated metadata change. Record which
checks ran. CI owns complete deterministic tests and aggregate coverage. Use
`npm run check:pr` for a deliberate full local gate on broad changes or when
investigating CI failures; it is not required after each edit, rebase, or push.
Packaging, setup, installation, or distribution changes also require
`npm run verify:package` and `npm pack --dry-run`. Provider behavior changes
require the matching real-provider `verify:*` lane.
CI installs dependencies once for all static checks, cancels superseded PR
runs, and skips package, Windows, and full test lanes for documentation-only
pull requests. TypeDoc renders only in pull-request CI; it does not run in local
commit or pre-push hooks, or in post-merge `main` CI.
`check:fast` runs cached typecheck and lint, reporting diagnostics on failure.
`check` adds formatting, dead-code, and package-metadata freshness checks.
Formatting uses Oxfmt and the committed `.oxfmtrc.json`; generated sources use
the same configuration. Pre-commit formats and lints staged files; pre-push runs
`check:fast`.
`docs:check` checks committed generated metadata. `docs:generate` regenerates
those files, and the docs CI lane checks them.
Real-provider execution remains uncached; deterministic builds use Turbo.
Local `npm test` runs every deterministic Vitest project without coverage or
retries. CI runs four coverage shards and merges JUnit and timing reports.
CI cancels superseded PR runs and skips package, Windows, and full test lanes
for documentation-only PRs. Coverage thresholds remain in `vitest.config.ts`.
Tests that need a temporary directory must use
`createTestTempDirectory` from `tests/fixtures/temporaryDirectory.ts`. The
@@ -92,13 +104,21 @@ Describe the behavior change and verification performed in the pull request. Nev
## Maintainer release checklist
Run the full checks, isolated package verifier, package dry run, and two-target real-Hopper verifier described above. Build a local tarball and exercise the executable through the package boundary:
Run `npm run check:pr`, the isolated package verifier, package dry run, and two-target real-Hopper verifier described above. Build a local tarball and exercise the executable through the package boundary:
```bash
npm pack
npm exec --yes --package ./morluto-rea-0.1.0.tgz -- rea --help
```
Use the exact filename printed by `npm pack` to run the packaged executable:
```bash
npm exec --yes --package ./rea-agents-VERSION.tgz -- rea --help
```
Replace `VERSION` with the packed version; do not use a tarball from an earlier
build.
Publish the public package:
```bash
@@ -110,6 +130,6 @@ After npm registry propagation, verify the published CLI and connect the client
```bash
npx -y rea-agents@latest --help
npx -y rea-agents@latest doctor
npx -y rea-agents@latest setup --yes
npx -y rea-agents@latest setup --yes --all-detected
npx -y rea-agents@latest mcp
```
+391 -462
View File
File diff suppressed because it is too large Load Diff
+134 -81
View File
@@ -2,9 +2,9 @@
[English](README.md) · [简体中文](README_zh.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · **العربية**
# REA: هندسة أي شيء عكسيًا
# REA: اعكس هندسة أي شيء
### واجهة CLI وخادم MCP واحدان يمكّنان وكلاء البرمجة من إجراء هندسة عكسية لأي شيء
### وسيط MCP واحد للهندسة العكسية عبر الملفات التنفيذية والتطبيقات وسلوك وقت التشغيل.
**اعثر على ميزة تعجبك. افهم آلية عملها. ابنها بالطريقة التي تريدها.**
@@ -13,109 +13,161 @@
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#منصة-أدوات-التحقيق)
[![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
[![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM)
[البدء السريع](#البدء-السريع) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [دليل الأدوات](#منصة-أدوات-التحقيق) · [كيف يعمل](#كيف-يعمل) · [الأسئلة الشائعة](#الأسئلة-الشائعة)
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
[البدء السريع](#البدء-السريع) · [حالة الدعم](#حالة-الدعم) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [منصة أدوات التحقيق](#منصة-أدوات-التحقيق) · [خطة العمل](#خطة-العمل) · [كيف يعمل؟](#كيف-يعمل)
<table aria-label="REA community">
<tr>
<td align="center" width="360">
<a href="https://discord.gg/GkcryMnJDM">
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
<strong>انضم إلى مجتمع الهندسة العكسية</strong>
</a><br />
<sub>Discord · أسئلة وأجوبة · شارك أعمالك</sub>
</td>
</tr>
</table>
<br />
<code>curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash</code>
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="REA يشغّل جسر التحليل داخل Hopper لفحص ملف تنفيذي أصلي" width="1200" />
</div>
هل وجدت في تطبيق ما ميزة تريدها في منتجك؟ أعط التطبيق إلى وكيل البرمجة حتى من دون شيفرته المصدرية. باستخدام REA، يستطيع الوكيل استقصاء الميزة وفهم آلية عملها ثم بناء نسخة ملائمة لتقنياتك وتصميمك ومتطلباتك.
---
يوفر REA هذه العملية عبر CLI وخادم MCP واحدين. يستطيع الوكيل قراءة شيفرة التطبيق وتتبع طريقة عمل الميزات ثم استخدام الأدلة مباشرة في عمله البرمجي المعتاد. ويتولى REA أدوات الهندسة العكسية المعقدة خلف واجهة واحدة.
هل وجدت في تطبيق ميزة تريدها في منتجك؟ اطلب من وكيلك استقصاءها باستخدام REA. يستطيع فحص التطبيق من دون شيفرته المصدرية، وشرح طريقة عمل الميزة وعرض الأدلة، ثم بناء نسخة تناسب مشروعك.
يوفر REA أدوات لتحليل الملفات التنفيذية الأصلية وتطبيقات JavaScript وElectron وتجميعات .NET والمواقع. يمكنك استخدامها من وكيلك أو من الطرفية. يعمل التحليل محليًا، وتتضمن النتائج الأدلة والقيود.
يضبط Setup إعدادات الوكيل ويربطه بتثبيت موجود من Hopper أو Ghidra، ويثبّت سير عمل REA الموجّه. ويمكنه تثبيت Hopper بعد موافقتك. ولا يحتاج التحليل الثابت لتطبيقات JavaScript إلى أي محرك.
## اطلب من وكيلك مباشرة
```bash
npx rea-agents setup
```
ثم اطلب:
بعد [الإعداد](#البدء-السريع)، أعد تشغيل الوكيل ثم اطلب:
```text
أعد REA وأجر هندسة عكسية لتطبيق Notes. اشرح طريقة عمل البحث وكيف توصلت إلى ذلك،
ثم ابنِ ميزة مشابهة لمشروعي.
استقصِ طريقة عمل البحث في تطبيق Notes، واعرض الأدلة، ثم ابنِ ميزة مشابهة لمشروعي.
```
تطبيق Notes مجرد مثال. يمكنك تسمية أي تطبيق تريد فهمه أو طلب البدء بنظرة عامة.
استبدل Notes بالتطبيق الذي تريد فهمه، أو اطلب نظرة عامة أولًا.
## من الملف التنفيذي إلى السلوك
| فك الترجمة | الفهم | إعادة الإنشاء |
| ----------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------- |
| فك الترجمة | الفهم | إعادة الإنشاء |
| ---------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------ |
| استخرج الشيفرة شبه المصدرية، والتعليمات، والسلاسل النصية، والرموز | تتبع تدفق التحكم والمراجع المتبادلة والاستدعاءات والأنواع | حوّل ما تعلمه الوكيل إلى ميزة تناسب تقنياتك وواجهتك ومتطلباتك |
يوضح REA كيف وصل إلى نتائجه. وهو لا يدّعي استعادة الشيفرة المصدرية الأصلية أو استنساخ التطبيق تلقائيًا.
## لماذا REA؟
| | |
| -------------------- | ----------------------------------------------------------------------------------------- |
| **مصمم للوكلاء** | اسأل عما يفعله تطبيق مترجم ودع الوكيل يجمع الأدلة بدلًا من التخمين. |
| **CLI وMCP** | استخدم قدرات الهندسة العكسية نفسها من الطرفية أو وكيل البرمجة. |
| **يتولى التعقيد** | يدير REA إعداد الأدوات وفتح التطبيق واستمرار الاستقصاء والتنظيف بعد الانتهاء. |
| **سير عمل كامل** | انتقل من النظرة الأولى إلى الشيفرة شبه المصدرية وعلاقات الاستدعاء والأنواع وأدلة التنفيذ. |
| **محلي حسب التصميم** | يجري التحليل على جهاز Mac ولا يرفع REA الملف التنفيذي إلى خدمة تحليل مستضافة. |
| **يحافظ على السياق** | استقصِ عدة ملفات تنفيذية من دون بدء التحليل من جديد عند كل سؤال. |
| | |
| ------------------------ | ---------------------------------------------------------------------------- |
| **مصمم للوكلاء** | اسأل عما يفعله التطبيق ودع الوكيل يفحصه بدلًا من التخمين. |
| **CLI وMCP** | استخدم قدرات الهندسة العكسية نفسها من الطرفية أو الوكيل. |
| **إعداد موجّه** | اضبط وكيلك، واربط أداة تحليل موجودة، أو ثبّت Hopper بموافقتك. |
| **من الفهم إلى الشيفرة** | افهم الميزة ثم ابنِ نسختك الخاصة في جلسة البرمجة نفسها. |
| **محلي حسب التصميم** | يجري التحليل على مضيفك المحلي المدعوم، ولا يرفع REA التطبيق إلى خدمة مستضافة. |
| **يحافظ على السياق** | استقصِ عدة تطبيقات دون بدء التحليل من جديد عند كل سؤال. |
## البدء السريع
### باستخدام وكيل برمجة — موصى به
### تشغيل الإعداد (موصى به)
اربط REA بوكيلك:
```bash
npx --yes rea-agents@latest setup
npx rea-agents setup
```
اطلب من الوكيل إعداد REA. سيفحص جهاز Mac ويشرح ما يحتاج إلى تثبيته ويطلب الموافقة ويرشدك خلال رسائل النظام. أعد تشغيل الوكيل بعد الإعداد إذا طلب ذلك لتحميل مجموعة الأدوات الكاملة.
يعرض Setup أولاً قائمة متعددة الاختيار للوكلاء الذين تريد ربطهم. تكون تسجيلات REA الموجودة محددة افتراضياً، ولا يؤدي اكتشاف عميل جديد وحده إلى تحديده؛ ويمكنك اختيار عميل غير مهيأ. راجع المسارات والتغييرات المحددة قبل الموافقة. يثبت Setup سير عمل REA للوكلاء المحددين افتراضياً. Hopper خيار منفصل يتطلب موافقة مستقلة، ويمكن أيضاً حفظ مسارات تثبيت Ghidra الموجودة.
### قبل البدء
يعرض التغييرات قبل تطبيقها وينشئ نسخة احتياطية من الإعدادات الموجودة. راجع [دليل التثبيت والإعداد](docs/installation.md) للمتطلبات والخيارات الإضافية.
- macOS 12 أو أحدث
- Ubuntu 24.04+ أو Fedora 41+ أو Arch Linux ‏64 بت
- Node.js 22.19+ أو 24.11+ وnpm المرفق مع Node
### الاستخدام مع وكيل
يعرض `rea setup` خطة التغييرات كاملة ويطلب التأكيد قبل تطبيقها. لا يثبت أو يحدّث Homebrew أو Node.js أو npm. إذا كان [Hopper](https://www.hopperapp.com/) مفقودًا، يقترح Setup الحزمة الرسمية. Hopper برنامج منفصل يحتاج إلى ترخيص خاص به.
بعد الإعداد، أعد تشغيل الوكيل واشرح التطبيق أو الميزة التي تريد استقصاءها. يدعم REA Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin وOpenCode وAntigravity وGitHub Copilot CLI وCommand Code وVS Code. تكون تسجيلات REA الموجودة محددة افتراضياً، ويجب اختيار العملاء المكتشفين الآخرين يدوياً. يمكن للوكلاء الآخرين استخدام إعداد MCP أدناه.
إذا كان Ghidra 12.1.4 PUBLIC وJDK 21 الكامل بنواة 64 بت مثبتين مسبقًا على Linux ‏64 بت، فيمكن لـ Setup أيضًا تسجيل `GHIDRA_INSTALL_DIR` و`JAVA_HOME` الاختياري بعد الموافقة. لا ينزّل REA أو يثبت أو يعدّل Ghidra أو Java. يوفّر مزوّد Ghidra ضمن جلسة headless معزولة وللقراءة فقط الجرد، وفك الترجمة، والتجميع، وعلاقات الاستدعاء، والمراجع ذات الأنواع، وxref، وCFG، وملفات تحليل الدوال؛ وتبقى حالة الواجهة الرسومية وعمليات التعديل غير متاحة.
يمكن تشغيل Hopper في الوضع التجريبي. إذا ظهرت رسالة عند التشغيل الأول، فاختر التجربة أو أدخل ترخيصًا موجودًا.
#### التثبيت على Linux واستكشاف الأخطاء
### مهارة مساعد البرمجة (اختياري)
على Ubuntu 24.04+ وFedora 41+ وArch Linux ‏64 بت، ينزّل REA حزمة DEB أو RPM أو Arch الرسمية، ويتحقق من الحجم وقيمة التحقق المنشورين، ثم يستخدم `apt-get` أو `dnf` أو `pacman` لحل الاعتماديات. عند التشغيل دون root يعرض `pkexec` طلب تفويض النظام. لا يستدعي REA الأمر `sudo`.
أضف المهارة إلى مساعد البرمجة للحصول على سياق أغنى:
مسار المشغّل الافتراضي هو `/opt/hopper/bin/Hopper`. استخدم `HOPPER_LAUNCHER_PATH` للمسارات الأخرى. إذا أبلغ Doctor عن غياب محرك التحليل، شغّل `ldd /opt/hopper/bin/Hopper | grep 'not found'`، وثبّت المكتبات الناقصة ثم أعد `rea setup`. يشغّل REA إصدار Hopper التجريبي المدعوم على شاشة Xvfb خاصة ويختار وضع التجربة الذي يقدمه Hopper لكل جلسة تحليل؛ لا يلزم `DISPLAY` لسطح المكتب ولا ترخيص مدفوع، مع بقاء حدود الإصدار التجريبي التي يحددها المورّد. أضف `~/.local/bin` إلى `PATH` أيضًا.
```bash
npx skills add morluto/rea --skill reverse-engineer-anything
```
### 1. إعداد REA
توفر المهارة سير عمل التحقيق في REA. نفّذ الإعداد أعلاه لربط REA بالوكيل وضبط أدوات التحليل. يثبّت الإعداد افتراضيًا مهارة مطابقة للإصدار؛ وهذا الأمر يثبّت نسخة المستودع.
لشجرة JavaScript/Electron المستخرجة أو ملف ASAR، لا يلزم إعداد MCP ولا محرك أصلي:
```bash
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
استبدل المسار بهدفك (مثل `"D:/apps/example"` على Windows). يدعم سير العمل الثابت هذه المسارات، بينما يتطلب التحليل الأصلي محركه الخاص.
### تثبيت أمر rea
ثبت واجهة سطر الأوامر:
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx --yes rea-agents@latest setup
```
إذا طلب macOS أو برنامج التثبيت تأكيدًا، فأكمل الخطوة ثم شغّل الأمر نفسه مرة أخرى.
يجب تثبيت Node.js وnpm مسبقًا. عند تشغيله من طرفية، يثبت البرنامج `rea` ويبدأ الإعداد.
### 2. إعادة تشغيل وكيل البرمجة
يكتشف Setup كلاً من Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin. ويهيئ تلقائيًا أول ستة عملاء عند اكتشافهم؛ أما Devin فيُبلّغ عن اكتشافه فقط ولا يُعدّله لعدم وجود حد موثق لإعداد MCP المحلي. أعد تشغيل العملاء الذين جرى إعدادهم كي يحمّلوا REA.
### 3. اطلب من الوكيل
يمكنك ذكر التطبيق باسمه. سيعثر وكيل البرمجة عليه ويمرر ملف البرنامج الذي يحتاجه REA.
```text
استخدم REA لإجراء هندسة عكسية لتطبيق Notes. اشرح طريقة عمل البحث واعرض الأدلة،
ثم ابنِ ميزة مشابهة لمشروعي باستخدام SQLite.
```
إذا واجهت مشكلة، فشغّل:
يمكنك بدلًا من ذلك التثبيت باستخدام npm ثم تشغيل الإعداد:
```bash
npx -y rea-agents@latest doctor
rea uninstall
rea uninstall --purge-data
npm install --global rea-agents
rea setup
```
### المتطلبات
- macOS 12 أو أحدث
- Ubuntu 24.04+ أو Fedora 41+ أو Arch Linux بنواة 64 بت
- 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 أيضًا إلى أداة فك الترجمة الأصلية المطابقة لمعمارية الجهاز.
يستطيع Setup التحقق من التثبيت وحفظ المسارات؛ ولا يثبت أو يحدّث Ghidra أو Java أو Node.js أو npm أو Homebrew.
يتضمن فرع main والحزمة npm 4.1.0 دعمًا تجريبيًا لـ Windows x64 Ghidra P0 لتطبيقات PE الأصلية x86-64 (غير المُدارة وغير DLL) على NTFS محلي، مع ضوابط Job Object وقوائم DACL الخاصة وقبول المسارات المجمّعة. تحقق من [حد الإصدار](docs/installation.md#released-package-and-main) قبل توقع ذلك من حزمة npm أقدم. راجع [دليل Windows Ghidra P0](docs/windows-ghidra-p0.md) للمتطلبات والنطاق الموثق.
### تشخيص المشكلات
يشخّص `npx -y rea-agents@latest doctor` الجهاز والاعتماديات وأدوات التحليل وإعدادات الوكيل من دون تغييرها. أضف `--json` للحصول على تشخيص منظم.
مسار Hopper الافتراضي على Linux هو `/opt/hopper/bin/Hopper`. استخدم `HOPPER_LAUNCHER_PATH` للمسارات الأخرى. إذا كان الملف موجودًا لكن محرك التحليل لا يظهر، فشغّل `ldd /opt/hopper/bin/Hopper | grep 'not found'` للتحقق من المكتبات الناقصة. راجع [دليل Hopper](docs/installation.md#hopper).
### التحديث والإزالة
- يحدّث `rea update` تثبيت REA الحالي.
- يزيل `rea uninstall` تسجيلات الوكلاء وملفات سير العمل التي يديرها REA، ويترك Hopper مثبتًا.
- يزيل `rea uninstall --purge-data` أيضًا ذاكرة REA المؤقتة وحالته. استخدمه فقط إذا أردت حذف هذه البيانات.
## حالة الدعم
تُوصف قدرات المستودع الحالية ومتطلبات الأنظمة في [دليل الدعم الإنجليزي](README.md#current-status). قد يسبق فرع main [إصدار npm](docs/installation.md#released-package-and-main).
- يوفر Ghidra 25 عملية للقراءة فقط على Linux x64 وmacOS x64/arm64 وحدود Windows x64 P0 التجريبية. يدعم Linux/macOS أيضًا تعليقات الدوال الذرية على مستوى الجلسة. نظام Windows P0 للقراءة فقط، ولا يوفر Ghidra أي تحكم في الواجهة.
- يتطلب الفحص الثابت لنظام Android تجهيز JADX/Java بشكل منفصل؛ ويغطي التحقق بالموفر الحقيقي نظام Linux. راجع [تحليل Android](docs/android-analysis.md).
- تحدد طلبات المتصفح وElectron والعمليات هدفها وإجراءاتها ودورة حياتها مباشرة، وتعتمد على وصول المضيف الفعلي دون منح أذونات منفصلة من REA. ما زالت كتابة إعدادات Setup وتثبيت Hopper تتطلب الموافقة على خطتها الدقيقة.
- يصف `rea capabilities` عمليات جلسة الملف التنفيذي لا كل أدوات التطبيقات ووقت التشغيل. راجع قائمة أدوات MCP الكاملة والدليل ذا الصلة.
## استقصاء كامل بطلب واحد
بعد الإعداد، يمكنك أن تطلب من وكيلك:
@@ -159,22 +211,28 @@ rea uninstall --purge-data
## منصة أدوات التحقيق
| عائلة الأدوات | العدد | أمثلة |
| --------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| فحص الملفات التنفيذية | 36 | الدوال، والشيفرة شبه المصدرية، والتعليمات، والسلاسل، والأسماء، والمراجع، والتعليقات |
| التحليل المركب | 14 | النظرة العامة، و`inspect_native_api`، وفك الترجمة الدفعي، ومخططات الاستدعاء، والمراجع، واكتشاف الأنواع |
| أدوات macOS الأصلية | 5 | بيانات Mach-O والتوقيعات وملفات plist والمعماريات وفك رموز Swift دون تشغيل Hopper |
| رسم القطع الأثرية | 3 | فحص كامل مباشر للدلائل والحزم المدعومة، واستخراج معاملاتي محدد صراحة |
| Managed PE/CLI | 8 | هوية PE/CLI، أعضاء البيانات الوصفية، بصمات CIL، تصريحات P/Invoke/الحدود الأصلية والتحقق منها، إسقاط رسم التطبيق، استيراد إعادة البناء من فك الترجمة، إعادة ربط الرموز، خطط ترابط وقت التشغيل ومقارنة الإصدارات |
| مراقبة المتصفح | 9 | التقاط CDP محدود الأصل وتحليل الحزم وخرائط المصدر واكتشاف WebMCP والفروق والأدلة المرئية |
| تحليل Electron | 5 | مراقبة سلبية ضمن جذور canonical، ورسم ثابت ومحدود للتطبيق، ومطابقة ثابتة/وقت تشغيل قائمة على Evidence |
| وقت تشغيل JavaScript | 2 | اكتشاف أهداف Node/Electron Inspector المعتمدة للاتصال فقط، ومراقبة محدودة للبرامج النصية وسياقات التنفيذ دون تقييم أو تجهيز |
| سير عمل التطبيقات | 10 | تتبع محدود ومقارنة إصدارات فريدة ومقارنة ثابتة لأشكال قيم إرجاع الصادرات وإعادة تشغيل وحدات معزولة ومعتمدة على Linux وتوصيف وقت التشغيل المُدار وإغلاق تغطية إعادة البناء ودفاتر التزامات حتمية |
| جلسة الملف التنفيذي | 22 | فتح الأهداف وحزم الأدلة وتشغيل آلة إعادة محدودة ومقارنة العمليات والقطع والدوال وسجل المجهولات المتبقية |
| عائلة الأدوات | العدد | الاستخدام |
| -------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| الفحص الأصلي | 41 | الدوال والشيفرة شبه المصدرية والتعليمات والسلاسل والرموز والاستدعاءات والمراجع والتعليقات وقراءة البايتات ومواضع الملفات |
| سير عمل التحقيق | 14 | نظرات عامة على التطبيقات وملفات الدوال وواجهات API الأصلية والتوزيع وفك الترجمة الدفعي وتتبع الميزات ومسارات الاستدعاء ومخططاته واكتشاف Swift وObjective-C |
| أدوات macOS الأصلية | 7 | بيانات Mach-O والتوقيعات وplist والمعماريات واستعادة أسماء Swift دون تشغيل Hopper |
| مخطط المخرجات | 5 | جرد الدلائل والحزم وملفات Interface Builder المترجمة وكتالوجات موارد Apple والاستخراج |
| PE/CLI المُدار | 7 | هوية تجميعات .NET وبياناتها الوصفية وتعليمات CIL والاعتماديات الأصلية واستيراد إعادة البناء ومقارنة الإصدارات |
| البرامج الثابتة | 2 | فحص مناطق البرامج الثابتة على Linux واستخراجها الصريح |
| Android APK | 5 | بيانات الحزمة وmanifest والبحث عن الأصناف وقوائم الأعضاء وفك ترجمة الأساليب والمراجع الثابتة |
| مراقبة المتصفح | 9 | بنية الصفحات وبيانات الشبكة والبرامج النصية وخرائط المصدر واكتشاف WebMCP والصور ومقارنة الالتقاط |
| تحليل Electron | 5 | مراقبة العارضين وخريطة التطبيق الثابتة والمواءمة بين نتائج الثبات والتشغيل |
| وقت تشغيل JavaScript | 2 | اكتشاف أهداف Node/Electron Inspector ومواضع البرامج وأحداث سياقات التنفيذ |
| سير عمل التطبيقات | 8 | تصدير النصوص البرمجية الملتقطة، تتبع الميزات عبر الطبقات ومقارنة الإصدارات ومواءمة المصدر التاريخي ومقارنة أشكال القيم المعادة والتحقق من إعادة الإنشاء |
| مساحة العمل والملاحظة | 21 | الجلسات وحزم الأدلة وسياق التنقل ومقارنة العمليات والمخرجات والدوال وتسجيل الأسئلة غير المحسومة |
## خطة العمل
يشمل العمل التالي توسيع التحقق من الأهداف الأصلية، ومقارنة إصدارات JavaScript و.NET، ومراقبة وقت التشغيل والتحقق من إعادة الإنشاء. يتيح Setup بالفعل اختيار تكامل الوكيل وتثبيت Hopper. تثبيت أدوات تحليل أخرى ما زال عملًا لاحقًا. راجع [خطة التثبيت](docs/roadmap.md) و[تقييم أدوات التحليل](docs/provider-evaluation.md).
## استخدام REA مع وكلاء برمجة آخرين
يكتشف Setup كلاً من Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin، ويهيئ تلقائيًا أول ستة عملاء. أما Devin فيُبلّغ عن اكتشافه فقط ولا يُعدّله. يمكن لأي وكيل برمجة يدعم خوادم MCP المحلية استخدام REA بالإعداد التالي:
يدعم Setup Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin وOpenCode وAntigravity وGitHub Copilot CLI وCommand Code وVS Code. تكون تسجيلات REA الموجودة محددة افتراضياً، ويجب اختيار العملاء المكتشفين الآخرين يدوياً. يمكن لأي وكيل يدعم خوادم MCP المحلية الاتصال باستخدام الإعداد التالي:
<!-- x-release-please-start-version -->
@@ -183,7 +241,7 @@ rea uninstall --purge-data
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@3.2.1", "mcp"]
"args": ["-y", "rea-agents@4.1.0", "mcp"]
}
}
}
@@ -204,7 +262,7 @@ flowchart LR
Ghidra --> App
```
يستخدم CLI وخادم MCP محرك التحليل نفسه. تغلق أوامر الطرفية التطبيق عند انتهائها، بينما تبقيه جلسة الوكيل مفتوحًا أثناء الاستقصاء.
يستخدم CLI وخادم MCP سير عمل التحليل نفسه. تحرر أوامر الطرفية جلسة الجسر الخاصة بها عند انتهائها، بينما يمكن لجلسة الوكيل الحفاظ على الاتصال أثناء الاستقصاء. إغلاق جلسة REA لا يغلق تطبيق Hopper الذي يستخدمه المستخدم.
## أوامر CLI
@@ -221,20 +279,15 @@ npx -y rea-agents@latest compare /absolute/path/to/left-evidence.json /absolute/
## سلوك Hopper
يستطيع REA تشغيل Hopper تلقائيًا؛ لا حاجة إلى افتراض أنه يعمل مسبقًا. لكنه تطبيق macOS رسومي، لذلك قد يطلب التركيز أو يعرض حوارًا عندما يفرض Hopper قرارًا تفاعليًا. لا يمكن لـ REA ضمان تشغيل غير مرئي تمامًا لأن هذه الواجهة يملكها Hopper وmacOS.
يشغّل REA برنامج Hopper عند الحاجة. على macOS قد تظهر نافذته في المقدمة أو يحتاج أحد الحوارات إلى استجابة منك. لا يمكن ضمان بقاء النافذة في الخلفية.
لتجنب مربع اختيار معمارية ملف شامل، حدد معطيات المحمل مسبقًا:
```bash
export HOPPER_LOADER_ARGS_JSON='["-l", "Mach-O", "--aarch64"]'
```
يدير REA دورة حياة Hopper ويحاول إغلاق العمليات التي بدأها عند انتهاء الأمر أو جلسة MCP.
يحدد REA تنسيق الهدف ومعماريته لتجنب حوارات الاختيار المعتادة. عند إغلاق الجلسة، يوقف الجسر وينظف موارده الخاصة، لكنه لا يغلق تطبيق Hopper الذي تستخدمه.
## الأمان والخصوصية
- يبقى تحليل الملف التنفيذي محليًا بين REA ومزوّد التحليل المختار.
- يبقى التحليل محليًا بين REA وموفر التحليل المختار عبر مقابس محلية خاصة، ومع IDA عبر تسجيل MCP المحلي المضبوط.
- لا يرفع REA الأهداف إلى خدمة مستضافة.
- تعمل أدوات التحليل والأهداف المطلقة بصلاحيات المستخدم نفسه؛ وما زال التقاط واجهة النظام على macOS يعتمد على الوصول إلى تسهيلات الاستخدام وتسجيل الشاشة. ولا ينفّذ التحليل الثابت للـ JavaScript الوحدات المستخرجة.
- تستخدم جلسات Ghidra مشروعًا مؤقتًا معزولًا ولا تفتح مشاريع Ghidra التي يملكها المستخدم أو تعدّلها.
- راجع الملفات التنفيذية غير الموثوقة واعزلها كما تفعل مع أي مدخل أصلي قد يكون ضارًا.
- أبلغ عن الثغرات وفق [سياسة الأمان](SECURITY.md)، وليس عبر قضية عامة.
@@ -258,7 +311,7 @@ export HOPPER_LOADER_ARGS_JSON='["-l", "Mach-O", "--aarch64"]'
<details>
<summary><strong>هل يعمل REA على Linux أو Windows؟</strong></summary>
نعم، يعمل REA على macOS 12+ وUbuntu 24.04+ وFedora 41+ وArch Linux ‏64 بت؛ Windows غير مدعوم حاليًا. يستخدم التحليل العميق Hopper أو مزوّد Ghidra للقراءة فقط الذي يختاره المستدعي؛ ويتطلب Ghidra على Linux الإصدار 12.1.4 وJDK 21 الكامل.
يدعم 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) للنطاق الموثق.
</details>
+126 -69
View File
@@ -4,7 +4,7 @@
# REA:あらゆるものをリバースエンジニアリング
### コーディングエージェントがあらゆるものをリバースエンジニアリングするための、ひとつの CLI / MCP サーバー
### エージェントでアプリの動作からネイティブバイナリまで調べる。
**気になる機能を見つけ、仕組みを理解し、自分の形で実装する。**
@@ -13,35 +13,51 @@
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#調査ツールカタログ)
[![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
[![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM)
[クイックスタート](#クイックスタート) · [バイナリから動作へ](#バイナリから動作へ) · [ツールカタログ](#調査ツールカタログ) · [仕組み](#仕組み) · [FAQ](#faq)
<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>
[クイックスタート](#クイックスタート) · [現在の対応状況](#現在の対応状況) · [バイナリから動作へ](#バイナリから動作へ) · [調査ツールカタログ](#調査ツールカタログ) · [ロードマップ](#ロードマップ) · [仕組み](#仕組み)
<table aria-label="REA community">
<tr>
<td align="center" width="360">
<a href="https://discord.gg/GkcryMnJDM">
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
<strong>リバースエンジニアリングのコミュニティに参加</strong>
</a><br />
<sub>Discord · 質問と回答 · 成果の共有</sub>
</td>
</tr>
</table>
<br />
<code>curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash</code>
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="Hopper 内で解析ブリッジを起動し、ネイティブバイナリを調べる REA" width="1200" />
</div>
---
アプリの気になる機能を自分のプロダクトにも取り入れたいと思ったことはありませんか。ソースコードがなくても、そのアプリをコーディングエージェントに渡せます。REA を使えば、エージェントが機能を調査して仕組みを理解し、あなたの技術スタック、デザイン、要件に合わせた形で実装できます。
アプリの機能を自分のプロダクトにも取り入れたいときは、REA を使ってエージェントに調査を依頼できます。ソースコードがなくても、アプリを調べ、仕組みと根拠を示し、あなたのプロジェクト向けに同様の機能を実装できます。
REA はこの流れをひとつの CLI / MCP サーバーで実現します。エージェントはコンパイル済みのアプリを調べ、機能の流れを追い、分かったことを通常のコーディング作業に活用できます。複雑なリバースエンジニアリングツールは REA がひとつのインターフェースの裏側で扱います。
REA はネイティブバイナリ、JavaScript/Electron アプリ、.NET アセンブリ、Web サイトの解析ツールを提供します。エージェントからもターミナルからも使えます。解析はローカルで実行され、結果には根拠と制限が含まれます。
Setup はエージェントを設定し、既存の Hopper または Ghidra に接続します。承認後に Hopper をインストールすることもできます。
## エージェントに頼むだけ
```bash
npx rea-agents setup
```
次のように依頼します:
[セットアップ](#クイックスタート)後にエージェントを再起動し、次のように依頼します:
```text
メモアプリをリバースエンジニアリングしてください。
検索の仕組みと判断の理由を示し、私のプロジェクト向けに同様の機能を実装してください。
メモアプリの検索機能を調べて根拠を示し、私のプロジェクト向けに同様の機能を実装してください。
```
メモアプリは例にすぎません。調べたいアプリを指定するか、まず概要から始めるよう依頼できます。
メモアプリを調べたいアプリに置き換えるか、まず概要を依頼してください。
## バイナリから動作へ
@@ -53,69 +69,104 @@ REA は調査をバイナリ上の根拠に結び付けます。元のソース
## REA を選ぶ理由
| | |
| ---------------------- | ------------------------------------------------------------------------------------- |
| **エージェント向け** | コンパイル済みアプリについて質問し、推測ではなく根拠を集めさせることができます。 |
| **CLI と MCP** | ターミナルとコーディングエージェントから同じリバースエンジニアリング機能を使えます。 |
| **複雑さを処理** | ツール設定、アプリの読み込み、調査の維持、終了後のクリーンアップを REA が担います。 |
| **一連の調査に対応** | 最初の概要から疑似コード、呼び出し関係、型、実装の手掛かりまで掘り下げられます。 |
| **ローカルで解析** | 解析は Mac 上で実行され、REA がバイナリをホスト型解析サービスへ送ることはありません。 |
| **コンテキストを維持** | 質問ごとに解析を最初からやり直さず、複数のバイナリを続けて調査できます。 |
| | |
| -------------------------- | ---------------------------------------------------------------------------------------------------- |
| **エージェント向け** | アプリの動作について質問し、推測ではなくエージェントに調査させることができます。 |
| **CLI と MCP** | ターミナルとエージェントから同じリバースエンジニアリング機能を使えます。 |
| **ガイド付きセットアップ** | エージェントの設定、既存の解析ツールへの接続、承認後の Hopper インストールに対応します。 |
| **洞察からコードへ** | 機能を理解したら、同じコーディングセッションで自分のプロダクト向けに実装できます。 |
| **ローカルで解析** | 解析は対応するローカルホストで実行され、REA がバイナリをホスト型解析サービスへ送ることはありません。 |
| **コンテキストを維持** | 質問ごとに解析を最初からやり直さず、複数のアプリを続けて調査できます。 |
## クイックスタート
### コーディングエージェントから(推奨)
### セットアップを実行(推奨)
REA をエージェントで使うための設定を行います:
```bash
npx --yes rea-agents@latest setup
npx rea-agents setup
```
エージェントに REA のセットアップを依頼してください。Mac を確認し、必要なインストールを説明して承認を求め、システムプロンプトを案内します。完全なツールを読み込むため再起動を求められた場合は、セットアップ後に再起動してください。
Setup は最初に連携するエージェントを複数選択できるようにします。既存の REA 登録は初期選択されますが、検出されただけのクライアントは自動選択されず、未設定のクライアントも選べます。具体的なパスと変更内容を確認してから承認してください。選択したエージェントには通常 REA のワークフローをインストールします。Hopper は別の任意操作で、個別の承認が必要です。既存の Ghidra のパスも登録できます。
### 始める前に
変更は事前に表示され、既存の設定はバックアップされます。要件と追加オプションは[インストールガイド](docs/installation.md)を参照してください。
### エージェントで使う
設定後にエージェントを再起動し、調べたいアプリや機能を説明します。REA は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code、VS Code に対応します。既存の REA 登録は初期選択され、それ以外の検出済みクライアントは選択が必要です。その他のエージェントは下記の MCP 設定を使えます。
Hopper はデモモードで使えます。初回起動の画面ではデモを選択するか、既存のライセンスを入力してください。
### AI コーディングアシスタント向けスキル(任意)
AI コーディングアシスタントにスキルを追加すると、より豊富なコンテキストが得られます:
```bash
npx skills add morluto/rea --skill reverse-engineer-anything
```
このスキルは REA の調査ワークフローを提供します。上記の setup を実行して REA をエージェントに接続し、解析ツールを設定してください。Setup は既定でバージョンに合ったスキルをインストールします。このコマンドではリポジトリ版のスキルをインストールします。
展開済みの JavaScript/Electron アプリのツリーや ASAR であれば、MCP の設定もネイティブエンジンも不要です:
```bash
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
パスを調査対象に置き換えてください(Windows では `"D:/apps/example"` のようなパス)。この静的ワークフローではそのようなパスも扱えます。ネイティブ解析には専用のエンジン設定が必要です。
### rea コマンドをインストール
コマンドラインツールをインストールします:
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
```
Node.js と npm を先にインストールしてください。ターミナルで実行すると、インストーラーは `rea` を追加してセットアップを開始します。
npm でインストールし、セットアップを実行することもできます:
```bash
npm install --global rea-agents
rea setup
```
### 要件
- macOS 12 以降
- Ubuntu 24.04+、Fedora 41+、または 64 ビット Arch Linux
- Node.js 22.19+ または 24.11+ と、Node に付属する npm
- Node.js 22.x (>=22.19)、24.x (>=24.11)、または 26+ と npm
`rea setup` は変更計画を表示し、確認後に適用します。Homebrew、Node.js、npm をインストールまたは更新しません。[Hopper](https://www.hopperapp.com/) がない場合は公式パッケージを提案します。Hopper は別製品で、ライセンスは別途必要です。
ネイティブバイナリ解析には Hopper または Ghidra が必要です。Hopper は別製品です。デモにはベンダー所定の制限がありますが、有料ライセンスは必須ではありません。
64 ビット Linux に Ghidra 12.1.4 PUBLIC と完全な 64 ビット JDK 21 がすでにある場合、Setup は承認後に `GHIDRA_INSTALL_DIR` と任意の `JAVA_HOME` も登録できます。REA は Ghidra や Java をダウンロード、インストール、変更しません。Ghidra プロバイダーは隔離された読み取り専用 headless セッションで、インベントリ、逆コンパイル、アセンブリ、呼び出し関係、型付き参照、xref、CFG、関数 dossier を提供します。GUI 状態と変更操作は引き続き利用できません。
Ghidra は Linux x64 と macOS x64/arm64 に対応します。Ghidra 12.1.4 と完全な 64 ビット JDK 21 を別途インストールし、REA で使うように設定してください。macOS ではホストのアーキテクチャに合うネイティブデコンパイラーも必要です。
#### Linux のインストールとトラブルシューティング
Setup はインストールを確認し、パスを保存できます。Ghidra、Java、Node.js、npm、Homebrew のインストールや更新は行いません。
Ubuntu 24.04+、Fedora 41+、64 ビット Arch Linux では、REA が公式の DEB、RPM、または Arch パッケージを取得し、公開されたサイズとチェックサムを検証してから `apt-get`、`dnf`、または `pacman` で依存関係を解決します。root 以外では `pkexec` がシステム承認を表示します。REA は `sudo` を呼び出しません。
リポジトリの main と npm 4.1.0 には、ローカル NTFS 上のネイティブ x86-64 PE アプリケーション(非マネージド、非 DLL 向け)を対象とする実験的な Windows x64 Ghidra P0 対応が含まれています。Job Object、プライベート DACL、パス許可の各制御が同梱されています。古い npm パッケージに期待する前に[リリース境界](docs/installation.md#released-package-and-main)を確認してください。前提条件と検証済みの範囲は [Windows Ghidra P0](docs/windows-ghidra-p0.md) を参照してください。
既定のランチャーは `/opt/hopper/bin/Hopper` です。別の場所では `HOPPER_LAUNCHER_PATH` を設定してください。Doctor が解析エンジン不足を報告した場合は `ldd /opt/hopper/bin/Hopper | grep 'not found'` を実行し、不足ライブラリを入れて `rea setup` を再実行します。REA は対応する Hopper デモ版を専用 Xvfb ディスプレイで起動し、解析セッションごとに Hopper が提示するデモモードを選択します。デスクトップの `DISPLAY` や有料ライセンスは不要ですが、デモ版にはベンダー所定の制限があります。`~/.local/bin` も `PATH` に追加してください。
### トラブルシューティング
```bash
# 1. REA をセットアップ
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx --yes rea-agents@latest setup
```
`npx -y rea-agents@latest doctor` はホスト、依存関係、解析ツール、エージェント設定を変更せずに確認します。構造化された診断には `--json` を追加してください。
macOS やインストーラーから確認を求められた場合は、その操作を完了してから同じコマンドをもう一度実行してください。
Linux の既定ランチャーは `/opt/hopper/bin/Hopper` です。別の場所には `HOPPER_LAUNCHER_PATH` を設定します。ファイルがあるのに解析エンジンが見つからない場合は、`ldd /opt/hopper/bin/Hopper | grep 'not found'` で不足ライブラリを確認してください。詳しくは [Hopper ガイド](docs/installation.md#hopper)を参照してください。
### 2. コーディングエージェントを再起動
### 更新とアンインストール
Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin を検出します。検出した最初の 6 クライアントは自動設定しますが、Devin はローカル MCP 設定境界が文書化されていないため、検出結果を報告するだけで変更しません。設定されたクライアントを再起動して REA を読み込んでください。
- `rea update` は現在の REA インストールを更新します。
- `rea uninstall` は REA が管理するエージェント登録とワークフローファイルを削除します。Hopper は残ります。
- `rea uninstall --purge-data` は REA のキャッシュと状態も削除します。それらを削除したい場合にだけ使ってください。
### 3. エージェントに依頼
## 現在の対応状況
アプリは名前で指定できます。コーディングエージェントがアプリを探し、REA に必要なプログラムファイルを渡します。
リポジトリの現在の機能とプラットフォーム要件は[英語の対応状況ガイド](README.md#current-status)で説明しています。main は [npm リリース](docs/installation.md#released-package-and-main)より先行する場合があります。
```text
REA でメモアプリをリバースエンジニアリングしてください。検索機能の仕組みと
根拠を示し、SQLite を使って私のプロジェクト向けに同様の機能を実装してください。
```
問題がある場合は次を実行します:
```bash
npx -y rea-agents@latest doctor
rea uninstall
rea uninstall --purge-data
```
- Ghidra は Linux x64、macOS x64/arm64、実験的な Windows x64 P0 境界で 25 件の読み取り専用操作を提供します。Linux/macOS ではアトミックなセッション内関数注釈にも対応します。Windows P0 は読み取り専用であり、Ghidra に GUI 操作はありません。
- 静的な Android 検査には持ち込みの JADX/Java が別途必要です。実プロバイダー検証は Linux を対象とします。詳しくは[Android 解析](docs/android-analysis.md)を参照してください。
- ブラウザー、Electron、プロセスの各リクエストは対象・操作・ライフサイクルを直接指定し、ホストの実際のアクセスに依存します。REA 側の個別の許可付与は不要です。Setup による設定書き込みと Hopper のインストールは、引き続き正確な計画の承認が必要です。
- `rea capabilities` はバイナリセッション操作の説明であり、すべてのアプリ/ランタイムツールの目録ではありません。接続済み MCP ツール一覧と関連ガイドを参照してください。
## ひとつのプロンプトで調査を完結
@@ -149,22 +200,28 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
## 調査ツールカタログ
| ツール群 | 数 | 例 |
| ---------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| バイナリ調査 | 36 | プロシージャ、疑似コード、アセンブリ、文字列、名前、セグメント、callers、callees、xrefs、注釈 |
| 合成解析 | 14 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift / ObjC 検出 |
| macOS ネイティブ | 5 | Mach-O メタデータ、署名、plist、アーキテクチャ、Swift デマングル。Hopper 起動不要 |
| アーティファクトグラフ | 3 | ディレクトリと対応パッケージの完全なインライン調査、および明示選択されたトランザクション抽出 |
| Managed PE/CLI | 8 | PE/CLI 識別、メタデータメンバー、CIL ハッシュ、P/Invoke/ネイティブ境界宣言と検証、アプリケーショングラフ投影、逆コンパイル再構成インポート、構造的 token 再マッピング、ランタイム相関計画、バージョン比較 |
| ブラウザ観察 | 9 | origin 限定 CDP 取得、bundle/source map 解析、WebMCP 検出、セッション、capture diff、視覚証拠 |
| Electron 解析 | 5 | canonical ファイルルート内の受動観察、有界な静的アプリマッピング、Evidence に基づく静的/実行時の照合 |
| JavaScript ランタイム | 2 | 承認済みのアタッチ専用 Node/Electron Inspector ターゲット検出と、評価や計装を行わない有界なスクリプト/実行コンテキスト観察 |
| アプリケーション | 10 | 有界なクロスレイヤー追跡、一意限定のバージョン比較、静的なエクスポート戻り値形状比較、承認済み Linux 隔離 extracted-module replay、マネージドランタイム特性評価、再構築カバレッジ閉包、決定論的な義務台帳 |
| バイナリセッション | 22 | `open_binary`、`binary_session`、証拠バンドル、有限リプレイ状態機械、プロセス/成果物/関数比較、残余未知レジストリ |
| ツール分類 | 数 | 用途 |
| -------------------- | --: | -------------------------------------------------------------------------------------------------------------------------------------------- |
| ネイティブ検査 | 41 | 関数、疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照、注釈、バイト読み取り、ファイルオフセット |
| 調査ワークフロー | 14 | アプリ概要、関数ドシエ、ネイティブ API とディスパッチ、一括逆コンパイル、機能トレース、コールパス、コールグラフ、Swift と Objective-C の検出 |
| macOS ネイティブ | 7 | Hopper を起動せずに扱える Mach-O メタデータ、署名、plist、アーキテクチャ、Swift 名の復元 |
| 成果物グラフ | 5 | ディレクトリとパッケージの目録、コンパイル済み Interface Builder ファイル、Apple アセットカタログ、抽出 |
| マネージド PE/CLI | 7 | .NET の識別、メタデータ、CIL 命令、ネイティブ依存関係、再構築インポート、ビルド比較 |
| ファームウェア | 2 | Linux ファームウェア領域の検査と明示的な展開 |
| Android APK | 5 | パッケージと manifest の宣言、クラス検索、メンバー一覧、メソッド逆コンパイル、静的参照 |
| ブラウザー観察 | 9 | ページ構成、ネットワークメタデータ、スクリプト、ソースマップ、WebMCP 検出、スクリーンショット、キャプチャ比較 |
| Electron 解析 | 5 | レンダラー観察、静的アプリマッピング、静的/実行時結果の対応付け |
| JavaScript 実行時 | 2 | Node/Electron Inspector 対象の検出、スクリプト位置、実行コンテキストイベント |
| アプリワークフロー | 8 | 取得した Web スクリプトのエクスポート、層をまたぐ機能トレース、ビルド比較、履歴ソース対応、静的戻り値形状の比較、再実装検証 |
| ワークスペースと観察 | 21 | セッション、証跡バンドル、ナビゲーション文脈、プロセス・成果物・関数比較、未解決事項の記録 |
## ロードマップ
次の作業はネイティブ対象の検証拡大、JavaScript と .NET のバージョン比較、実行時の観察と再実装の検証です。Setup はエージェント連携と Hopper のインストールを選択できます。他の解析ツールのインストールは今後の作業です。[インストールのロードマップ](docs/roadmap.md)と[解析ツールの評価](docs/provider-evaluation.md)を参照してください。
## 他のコーディングエージェントで使う
Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin を検出し、最初の 6 クライアントを自動設定します。Devin は検出結果を報告するだけで変更しません。ローカル MCP サーバーに対応するコーディングエージェントなら、次の設定で REA を利用できます。
Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code、VS Code に対応します。既存の REA 登録は初期選択され、それ以外の検出済みクライアントは選択が必要です。ローカル MCP サーバーに対応するエージェントは、次の設定でも接続できます。
<!-- x-release-please-start-version -->
@@ -173,7 +230,7 @@ Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@3.2.1", "mcp"]
"args": ["-y", "rea-agents@4.1.0", "mcp"]
}
}
}
@@ -193,7 +250,7 @@ flowchart LR
Ghidra --> App
```
CLI と MCP サーバーは同じ解析エンジンを使います。ターミナルのコマンドは終了時にアプリを閉じ、エージェントのセッションは調査中アプリを開いたままにします。
CLI と MCP サーバーは同じ解析処理を使います。ターミナルコマンドは完了後に自分のブリッジセッションを解放し、エージェントセッションは調査中の接続を維持できます。REA セッションを閉じても、ユーザーが利用中の Hopper は終了しません。
## CLI
@@ -229,7 +286,7 @@ REA は必要なときに Hopper を起動します。Hopper のランチャー
<details><summary><strong>Hopper を先に起動する必要がありますか?</strong></summary>
いいえ。REA が必要時に起動します。すでに起動している Hopper にも対応します。
いいえ。REA が必要時に起動します。Hopper が起動済みでも使えますが、既存の GUI ドキュメントには接続せず、新しい解析ドキュメントを開きます。
</details>
+127 -69
View File
@@ -4,7 +4,7 @@
# REA: 무엇이든 리버스 엔지니어링
### 코딩 에이전트가 무엇이든 리버스 엔지니어링할 수 있게 하는 하나의 CLI 및 MCP 서버
### 에이전트로 앱의 동작부터 네이티브 바이너리까지 조사하세요.
**마음에 드는 기능을 찾고, 작동 방식을 이해하고, 원하는 방식으로 구현하세요.**
@@ -13,35 +13,52 @@
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#조사-도구-카탈로그)
[![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
[![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM)
[빠른 시작](#빠른-시작) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [도구 카탈로그](#조사-도구-카탈로그) · [작동 방식](#작동-방식) · [FAQ](#faq)
<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>
[빠른 시작](#빠른-시작) · [현재 지원 범위](#현재-지원-범위) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [조사 도구 카탈로그](#조사-도구-카탈로그) · [로드맵](#로드맵) · [작동 방식](#작동-방식)
<table aria-label="REA community">
<tr>
<td align="center" width="360">
<a href="https://discord.gg/GkcryMnJDM">
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
<strong>리버스 엔지니어링 커뮤니티에 참여하세요</strong>
</a><br />
<sub>Discord · 질문과 답변 · 작업 공유</sub>
</td>
</tr>
</table>
<br />
<code>curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash</code>
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="Hopper에서 분석 브리지를 시작하고 네이티브 바이너리를 조사하는 REA" width="1200" />
</div>
---
어떤 앱에서 마음에 드는 기능을 자신의 제품에도 넣고 싶나요? 소스 코드가 없어도 앱을 코딩 에이전트에 전달할 수 있습니다. REA를 사용하면 에이전트가 기능을 조사하고 작동 방식을 이해한 뒤, 사용자의 기술 스택, 디자인, 요구 사항에 맞는 버전을 구현할 수 있습니다.
앱에서 마음에 드는 기능을 찾았다면 에이전트에게 REA로 조사해 달라고 요청하세요. 소스 코드가 없어도 앱을 분석하고, 기능의 동작과 근거를 설명한 뒤, 프로젝트에 맞는 기능을 구현할 수 있습니다.
REA는 이 과정을 하나의 CLI 및 MCP 서버로 제공합니다. 에이전트는 컴파일된 앱을 조사하고 기능의 작동 방식을 추적한 뒤 알아낸 내용을 일반 코딩 작업에 활용할 수 있습니다. 복잡한 리버스 엔지니어링 도구는 REA가 하나의 인터페이스 뒤에서 처리합니다.
REA는 네이티브 바이너리, JavaScript/Electron 앱, .NET 어셈블리, 웹사이트 분석 도구를 제공합니다. 에이전트와 터미널에서 같은 도구를 사용할 수 있습니다. 분석은 로컬에서 실행되며 결과에는 근거와 제한 사항이 포함됩니다.
Setup은 에이전트를 설정하고 기존 Hopper 또는 Ghidra 설치에 연결합니다. 승인 후 Hopper를 설치할 수도 있습니다.
## 에이전트에게 바로 요청하기
```bash
npx rea-agents setup
```
그런 다음 요청하세요.
[설정](#빠른-시작)을 마친 뒤 에이전트를 다시 시작하고 요청하세요.
```text
REA를 설정하고 메모 앱을 리버스 엔지니어링해 주세요. 검색 기능의 작동 방식과
그렇게 판단한 이유를 보여 주고, 제 프로젝트에 비슷한 기능을 구현해 주세요.
메모 앱의 검색 기능이 어떻게 동작하는지 조사하고 근거를 보여 주세요.
그다음 제 프로젝트에 비슷한 기능을 구현해 주세요.
```
메모 앱은 예시일 뿐입니다. 이해하고 싶은 앱을 지정하거나 먼저 개요부터 살펴보라고 요청할 수 있습니다.
메모 앱 대신 조사할 앱을 지정하거나 먼저 개요를 요청할 수 있습니다.
## 바이너리에서 동작까지
@@ -53,69 +70,104 @@ REA는 분석을 바이너리 증거에 근거하게 합니다. 원본 소스
## REA를 사용하는 이유
| | |
| ------------------ | ---------------------------------------------------------------------------------- |
| **에이전트용** | 컴파일된 앱에 관해 질문하고 추측 대신 증거를 수집하게 합니다. |
| **CLI와 MCP** | 터미널과 코딩 에이전트에서 동일한 리버스 엔지니어링 기능을 사용합니다. |
| **복잡성 처리** | 도구 설정, 앱 열기, 조사 유지, 작업 후 정리를 REA가 처리합니다. |
| **전체 조사 과정** | 첫 개요에서 의사 코드, 호출 관계, 타입, 구현 단서까지 이어서 조사합니다. |
| **로컬 분석** | 분석은 Mac에서 실행되며 REA는 바이너리를 호스팅 분석 서비스에 업로드하지 않습니다. |
| **컨텍스트 유지** | 질문마다 분석을 처음부터 시작하지 않고 여러 바이너리를 연속으로 조사합니다. |
| | |
| ------------------- | --------------------------------------------------------------------------------------------------- |
| **에이전트용** | 앱의 동작에 대해 질문하고 추측 대신 에이전트가 조사하게 합니다. |
| **CLI와 MCP** | 터미널과 에이전트에서 동일한 리버스 엔지니어링 기능을 사용합니다. |
| **안내식 설정** | 에이전트 설정, 기존 분석 도구 연결, 승인 후 Hopper 설치를 지원합니다. |
| **통찰에서 코드로** | 기능을 이해한 뒤 같은 코딩 세션에서 자신의 제품에 맞는 버전을 구현합니다. |
| **로컬 분석** | 분석은 지원되는 로컬 호스트에서 실행되며 REA는 바이너리를 호스팅 분석 서비스에 업로드하지 않습니다. |
| **컨텍스트 유지** | 질문마다 분석을 처음부터 시작하지 않고 여러 앱을 연속으로 조사합니다. |
## 빠른 시작
### 코딩 에이전트에서 시작하기(권장)
### 설정 실행하기(권장)
REA를 에이전트에 연결하세요.
```bash
npx --yes rea-agents@latest setup
npx rea-agents setup
```
에이전트에게 REA 설정을 요청하세요. Mac을 확인하고 필요한 설치 내용을 설명한 뒤 승인을 요청하며 시스템 프롬프트를 안내합니다. 전체 도구를 불러오기 위해 재시작하라고 하면 설정 후 에이전트를 재시작하세요.
Setup은 먼저 연결할 에이전트를 여러 개 선택하도록 안내합니다. 기존 REA 등록은 기본 선택되며, 감지만 된 클라이언트는 자동 선택되지 않습니다. 아직 설정되지 않은 클라이언트도 직접 선택할 수 있습니다. 정확한 경로와 변경 사항을 검토한 뒤 승인하세요. 선택한 에이전트에는 기본적으로 REA 워크플로가 설치됩니다. Hopper는 별도의 선택 사항이며 따로 승인해야 합니다. 기존 Ghidra 경로도 등록할 수 있습니다.
### 시작하기 전에
변경 전에 계획을 표시하고 기존 설정을 백업합니다. 요구 사항과 추가 옵션은 [설치 안내](docs/installation.md)를 참고하세요.
### 에이전트에서 사용하기
설정 후 에이전트를 다시 시작하고 조사할 앱이나 기능을 설명하세요. REA는 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, Command Code, VS Code를 지원합니다. 기존 REA 등록은 기본 선택되고, 그 밖의 감지된 클라이언트는 직접 선택해야 합니다. 다른 에이전트는 아래 MCP 설정을 사용할 수 있습니다.
Hopper는 데모 모드로 사용할 수 있습니다. 첫 실행 안내가 나오면 데모를 선택하거나 기존 라이선스를 입력하세요.
### AI 코딩 어시스턴트용 스킬(선택)
AI 코딩 어시스턴트에 스킬을 추가하면 더 풍부한 컨텍스트를 얻을 수 있습니다.
```bash
npx skills add morluto/rea --skill reverse-engineer-anything
```
이 스킬은 REA의 조사 워크플로를 제공합니다. 위 setup을 실행해 REA를 에이전트에 연결하고 분석 도구를 설정하세요. Setup은 기본적으로 버전에 맞는 스킬을 설치합니다. 이 명령은 저장소 버전의 스킬을 설치합니다.
압축을 해제한 JavaScript/Electron 앱 트리나 ASAR라면 MCP 설정이나 네이티브 엔진 없이 바로 분석할 수 있습니다.
```bash
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
경로를 조사 대상으로 바꾸세요(Windows에서는 `"D:/apps/example"` 같은 경로). 이 정적 워크플로에서는 이런 경로도 지원됩니다. 네이티브 분석에는 전용 엔진 설정이 필요합니다.
### rea 명령 설치하기
명령줄 도구를 설치하세요.
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
```
Node.js와 npm이 먼저 설치되어 있어야 합니다. 터미널에서 실행하면 설치 프로그램이 `rea`를 설치하고 설정을 시작합니다.
npm으로 설치한 뒤 설정을 실행할 수도 있습니다.
```bash
npm install --global rea-agents
rea setup
```
### 요구 사항
- macOS 12 이상
- Ubuntu 24.04+, Fedora 41+ 또는 64비트 Arch Linux
- Node.js 22.19+ 또는 24.11+ 및 Node와 함께 제공되는 npm
- Node.js 22.x (>=22.19), 24.x (>=24.11) 또는 26+와 npm
`rea setup`은 전체 변경 계획을 표시하고 확인 후 적용합니다. Homebrew, Node.js 또는 npm을 설치하거나 업데이트하지 않습니다. [Hopper](https://www.hopperapp.com/)가 없으면 공식 패키지 설치를 제안합니다. Hopper는 별도 라이선스가 필요한 독립 소프트웨어입니다.
네이티브 바이너리 분석에는 Hopper 또는 Ghidra가 필요합니다. Hopper는 별도 소프트웨어입니다. 데모에는 공급업체의 제한이 있지만 유료 라이선스가 필수는 아닙니다.
64비트 Linux에 Ghidra 12.1.4 PUBLIC과 완전한 64비트 JDK 21이 이미 설치되어 있다면 Setup은 승인 후 `GHIDRA_INSTALL_DIR`과 선택 사항인 `JAVA_HOME`도 등록할 수 있습니다. REA는 Ghidra나 Java를 다운로드, 설치 또는 수정하지 않습니다. Ghidra 공급자는 격리된 읽기 전용 headless 세션에서 인벤토리, 디컴파일, 어셈블리, 호출 관계, 형식이 지정된 참조, xref, CFG 및 함수 dossier를 제공합니다. GUI 상태와 변경 작업은 계속 사용할 수 없습니다.
Ghidra는 Linux x64와 macOS x64/arm64를 지원합니다. Ghidra 12.1.4와 완전한 64비트 JDK 21을 별도로 설치한 뒤 REA가 사용하도록 설정하세요. macOS에서는 호스트 아키텍처에 맞는 네이티브 디컴파일러도 필요합니다.
#### Linux 설치 및 문제 해결
Setup은 설치를 확인하고 경로를 저장할 수 있습니다. Ghidra, Java, Node.js, npm, Homebrew를 설치하거나 업데이트하지 않습니다.
Ubuntu 24.04+, Fedora 41+, 64비트 Arch Linux에서 REA는 공식 DEB, RPM 또는 Arch 패키지를 내려받아 게시된 크기와 체크섬을 검증한 뒤 `apt-get`, `dnf` 또는 `pacman`으로 의존성을 설치합니다. root가 아니면 `pkexec`가 시스템 승인 창을 표시합니다. REA는 `sudo`를 호출하지 않습니다.
저장소 main과 npm 4.1.0에는 로컬 NTFS상의 네이티브 x86-64 PE 애플리케이션(비관리, 비 DLL 대상)을 위한 실험적인 Windows x64 Ghidra P0 지원이 포함되어 있습니다. Job Object, 비공개 DACL, 경로 승인 제어가 함께 제공됩니다. 이전 npm 패키지에 이 기능이 있을 것으로 기대하기 전에 [릴리스 경계](docs/installation.md#released-package-and-main)를 확인하세요. 전제 조건과 검증 범위는 [Windows Ghidra P0](docs/windows-ghidra-p0.md)를 참고하세요.
기본 실행 파일은 `/opt/hopper/bin/Hopper`입니다. 다른 위치에 설치했다면 `HOPPER_LAUNCHER_PATH`를 설정하세요. Doctor가 분석 엔진 누락을 보고하면 `ldd /opt/hopper/bin/Hopper | grep 'not found'`를 실행하고 누락된 라이브러리를 설치한 다음 `rea setup`을 다시 실행하세요. REA는 지원되는 Hopper 데모 빌드를 전용 Xvfb 디스플레이에서 실행하고 분석 세션마다 Hopper가 제공하는 데모 모드를 선택합니다. 데스크톱 `DISPLAY`나 유료 라이선스는 필요하지 않지만 데모에는 공급업체가 정한 제한이 있습니다. `~/.local/bin`도 `PATH`에 추가해야 합니다.
### 문제 해결
```bash
# 1. REA 설정
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx --yes rea-agents@latest setup
```
`npx -y rea-agents@latest doctor`는 호스트, 의존성, 분석 도구, 에이전트 설정을 변경 없이 확인합니다. 구조화된 진단에는 `--json`을 추가하세요.
macOS나 설치 프로그램이 확인을 요청하면 해당 절차를 완료하고 같은 명령을 다시 실행하세요.
Linux의 기본 Hopper 실행 파일은 `/opt/hopper/bin/Hopper`입니다. 다른 위치에는 `HOPPER_LAUNCHER_PATH`를 설정하세요. 파일이 있는데도 분석 엔진이 없다고 하면 `ldd /opt/hopper/bin/Hopper | grep 'not found'`로 누락된 라이브러리를 확인하세요. 자세한 내용은 [Hopper 안내](docs/installation.md#hopper)를 참고하세요.
### 2. 코딩 에이전트 재시작
### 업데이트와 제거
Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin을 감지합니다. 감지된 앞의 여섯 클라이언트는 자동으로 구성하지만, Devin은 문서화된 로컬 MCP 구성 경계가 없어 감지만 보고하고 변경하지 않습니다. 구성된 클라이언트를 다시 시작해 REA를 불러오세요.
- `rea update`는 현재 REA 설치를 업데이트합니다.
- `rea uninstall`은 REA가 관리하는 에이전트 등록과 워크플로 파일을 제거합니다. Hopper는 유지합니다.
- `rea uninstall --purge-data`는 REA 캐시와 상태도 삭제합니다. 해당 데이터를 지우려는 경우에만 사용하세요.
### 3. 에이전트에게 요청
## 현재 지원 범위
앱 이름만 말해도 됩니다. 코딩 에이전트가 앱을 찾아 REA에 필요한 프로그램 파일을 전달합니다.
저장소의 현재 기능과 플랫폼 요구 사항은 [영문 지원 가이드](README.md#current-status)에 설명되어 있습니다. main은 [npm 릴리스](docs/installation.md#released-package-and-main)보다 앞설 수 있습니다.
```text
REA로 메모 앱을 리버스 엔지니어링해 주세요. 검색 기능의 작동 방식과 증거를 보여 주고,
SQLite를 사용해 제 프로젝트에 비슷한 기능을 구현해 주세요.
```
문제가 있으면 다음을 실행하세요.
```bash
npx -y rea-agents@latest doctor
rea uninstall
rea uninstall --purge-data
```
- Ghidra는 Linux x64, macOS x64/arm64, 실험적인 Windows x64 P0 경계에서 25개의 읽기 전용 작업을 제공합니다. Linux/macOS에서는 원자적 세션 함수 주석도 추가로 지원합니다. Windows P0는 읽기 전용이며 Ghidra에는 GUI 제어가 없습니다.
- 정적 Android 검사는 별도로 준비한 JADX/Java가 필요합니다. 실제 제공자 검증은 Linux를 대상으로 합니다. [Android 분석](docs/android-analysis.md)을 참고하세요.
- 브라우저, Electron, 프로세스 요청은 대상·동작·수명 주기를 직접 지정하며 호스트의 실제 접근에 의존합니다. 별도의 REA 권한 승인은 필요 없습니다. Setup의 설정 쓰기와 Hopper 설치는 여전히 정확한 계획에 대한 승인이 필요합니다.
- `rea capabilities`는 모든 앱/런타임 도구의 목록이 아니라 바이너리 세션 작업을 설명합니다. 연결된 MCP 도구 목록과 관련 가이드를 참고하세요.
## 하나의 프롬프트로 전체 조사
@@ -149,22 +201,28 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
## 조사 도구 카탈로그
| 도구 그룹 | 수 | 예시 |
| ------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 바이너리 검사 | 36 | 프로시저, 의사 코드, 어셈블리, 문자열, 이름, 세그먼트, callers, callees, xrefs, 주석 |
| 합성 분석 | 14 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift 및 ObjC 탐색 |
| macOS 네이티브 도구 | 5 | Mach-O 메타데이터, 코드 서명, plist, 아키텍처, Swift 디맹글링. Hopper 실행 불필요 |
| 아티팩트 그래프 | 3 | 디렉터리와 지원 패키지의 전체 인라인 검사 및 명시적으로 선택한 트랜잭션 추출 |
| Managed PE/CLI | 8 | PE/CLI ID, 메타데이터 멤버, CIL 해시, P/Invoke/네이티브 경계 선언 및 검증, 애플리케이션 그래프 투영, 디컴파일 재구성 가져오기, 구조적 token 재매핑, 런타임 상관 계획, 버전 비교 |
| 브라우저 관찰 | 9 | origin 제한 CDP 캡처, bundle/source map 분석, WebMCP 검색, 세션 타임라인, capture diff 및 시각 증거 |
| Electron 분석 | 5 | canonical 파일 루트 내 수동 관찰, 제한된 정적 앱 매핑, Evidence 기반 정적/런타임 조정 |
| JavaScript 런타임 | 2 | 승인된 연결 전용 Node/Electron Inspector 대상 탐색과 평가·계측 없는 제한된 스크립트 및 실행 컨텍스트 관찰 |
| 애플리케이션 | 10 | 제한된 교차 계층 추적, 고유 일치 버전 비교, 정적 export 반환 형태 비교, 승인된 Linux 격리 extracted-module replay, 관리형 런타임 특성화, 재구성 커버리지 폐쇄, 결정론적 의무 원장 |
| 바이너리 세션 | 22 | `open_binary`, `binary_session`, 증거 번들, 유한 replay 상태 머신, 프로세스·아티팩트·함수 비교, 잔여 미지수 레지스트리 |
| 도구 분류 | 수 | 용도 |
| ----------------- | --: | ------------------------------------------------------------------------------------------------------------------------- |
| 네이티브 검사 | 41 | 함수, 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조, 주석, 바이트 읽기, 파일 오프셋 |
| 조사 워크플로 | 14 | 앱 개요, 함수 기록서, 네이티브 API와 디스패치, 일괄 디컴파일, 기능 추적, 호출 경로, 호출 그래프, Swift와 Objective-C 탐색 |
| macOS 네이티브 | 7 | Hopper 실행 없이 처리하는 Mach-O 메타데이터, 서명, plist, 아키텍처, Swift 이름 복원 |
| 아티팩트 그래프 | 5 | 디렉터리와 패키지 목록, 컴파일된 Interface Builder 파일, Apple 애셋 카탈로그, 추출 |
| 관리 PE/CLI | 7 | .NET 식별, 메타데이터, CIL 명령어, 네이티브 종속성, 재구성 가져오기, 빌드 비교 |
| 펌웨어 | 2 | Linux 펌웨어 영역 검사 및 명시적 추출 |
| Android APK | 5 | 패키지와 manifest 선언, 클래스 검색, 멤버 목록, 메서드 디컴파일, 정적 참조 |
| 브라우저 관찰 | 9 | 페이지 구조, 네트워크 메타데이터, 스크립트, 소스 맵, WebMCP 탐색, 스크린샷, 캡처 비교 |
| Electron 분석 | 5 | 렌더러 관찰, 정적 앱 매핑, 정적·런타임 결과 연결 |
| JavaScript 런타임 | 2 | Node/Electron Inspector 대상 탐색, 스크립트 위치, 실행 컨텍스트 이벤트 |
| 앱 워크플로 | 8 | 캡처한 웹 스크립트 내보내기, 계층 간 기능 추적, 빌드 비교, 히스토리 소스 매핑, 정적 반환 구조 비교, 재구현 검증 |
| 작업 공간과 관찰 | 21 | 세션, 근거 번들, 탐색 컨텍스트, 프로세스·아티팩트·함수 비교, 미해결 항목 기록 |
## 로드맵
다음 작업은 네이티브 대상 검증 확대, JavaScript와 .NET의 버전 비교, 런타임 관찰과 재구현 검증입니다. Setup은 에이전트 연결과 Hopper 설치 선택을 이미 지원합니다. 다른 분석 도구 설치는 향후 작업입니다. [설치 로드맵](docs/roadmap.md)과 [분석 도구 평가](docs/provider-evaluation.md)를 참고하세요.
## 다른 코딩 에이전트에서 사용하기
Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin을 감지하고 앞의 여섯 클라이언트를 자동으로 구성합니다. Devin은 감지만 보고하고 변경하지 않습니다. 로컬 MCP 서버를 지원하는 코딩 에이전트라면 다음 설정으로 REA를 사용할 수 있습니다.
Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, Command Code, VS Code를 지원합니다. 기존 REA 등록은 기본 선택되고, 그 밖의 감지된 클라이언트는 직접 선택해야 합니다. 로컬 MCP 서버를 지원하는 에이전트는 다음 설정으로 연결할 수 있습니다.
<!-- x-release-please-start-version -->
@@ -173,7 +231,7 @@ Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@3.2.1", "mcp"]
"args": ["-y", "rea-agents@4.1.0", "mcp"]
}
}
}
@@ -193,7 +251,7 @@ flowchart LR
Ghidra --> App
```
CLI와 MCP 서버는 같은 분석 엔진을 사용합니다. 터미널 명령은 끝날 때 앱을 닫고, 에이전트 세션은 조사하는 동안 앱을 열어 둡니다.
CLI와 MCP 서버는 같은 분석 워크플로를 사용합니다. 터미널 명령은 완료 후 자신의 브리지 세션을 해제하고, 에이전트 세션은 조사 중 연결을 유지할 수 있습니다. REA 세션을 닫아도 사용자가 이용 중인 Hopper 앱은 종료하지 않습니다.
## CLI
@@ -229,7 +287,7 @@ REA는 필요할 때 Hopper를 시작합니다. Hopper 런처는 내부적으로
<details><summary><strong>Hopper를 미리 실행해야 하나요?</strong></summary>
아니요. REA가 필요할 때 시작하며 이미 실행 중인 Hopper도 지원합니다.
아니요. REA가 필요할 때 시작합니다. Hopper가 실행 중이어도 사용할 수 있지만 기존 GUI 문서에 연결하는 대신 새 분석 문서를 엽니다.
</details>
+127 -70
View File
@@ -4,7 +4,7 @@
# REA:逆向工程一切
### 一个 CLI 与 MCP 服务器,让编程智能体逆向工程任何程序
### 使用智能体逆向工程任何程序,从应用行为到原生二进制文件。
**看到喜欢的功能。理解它的原理。按照你的方式实现。**
@@ -13,35 +13,51 @@
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#调查工具目录)
[![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
[![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM)
[快速开始](#快速开始) · [从二进制到行为](#从二进制到行为) · [工具目录](#调查工具目录) · [工作原理](#工作原理) · [常见问题](#常见问题)
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
[快速开始](#快速开始) · [当前状态](#当前状态) · [从二进制到行为](#从二进制到行为) · [调查工具目录](#调查工具目录) · [路线图](#路线图) · [工作原理](#工作原理)
<table aria-label="REA community">
<tr>
<td align="center" width="360">
<a href="https://discord.gg/GkcryMnJDM">
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
<strong>加入逆向工程社区</strong>
</a><br />
<sub>Discord · 问答 · 成果分享</sub>
</td>
</tr>
</table>
<br />
<code>curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash</code>
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="REA 在 Hopper 中启动分析桥并检查原生二进制文件" width="1200" />
</div>
---
看到某个应用中想加入自己产品的功能?即使没有源代码,也可以把应用交给编程智能体。借助 REA,智能体能够调查该功能、理解其工作原理,并按照你的技术栈、设计和需求构建适合你产品的版本。
看到某个应用中想加入自己产品的功能?让智能体用 REA 调查它。即使没有源代码,智能体也可以检查应用、解释功能的工作方式、展示证据,并为你的项目实现类似功能。
REA 通过一个 CLI 与 MCP 服务器实现这套流程。智能体可以检查编译后的应用、追踪功能的工作方式,并把了解到的内容用于日常编码。REA 在一个接口背后处理复杂的逆向工程工具。
REA 提供原生二进制、JavaScript 与 Electron 应用、.NET 程序集和网站的分析工具。你可以从智能体或终端使用它们。分析在本机运行,结果会说明证据和限制。
Setup 可以配置智能体、连接已安装的 Hopper 或 Ghidra,也可以在你批准后安装 Hopper。
## 直接询问智能体
```bash
npx rea-agents setup
```
然后说:
完成[设置](#快速开始)后,重启智能体并提出问题:
```text
逆向工程“备忘录”应用。解释搜索功能的工作方式,
说明你是如何得出结论的,然后为我的项目构建类似功能。
解释“备忘录”应用的搜索功能,展示证据,然后为我的项目实现类似功能。
```
“备忘录”只是示例。你可以说出任何想了解的应用,也可以让智能体先从概览开始。
把“备忘录”替换成你想了解的应用,也可以先请求概览。
## 从二进制到行为
@@ -53,69 +69,104 @@ REA 让调查始终以二进制证据为依据。它不会声称能恢复原始
## 为什么选择 REA
| | |
| ---------------- | -------------------------------------------------------------- |
| **为智能体设计** | 直接询问编译后应用的行为,让智能体搜集证据,而不是猜测。 |
| **CLI 与 MCP** | 在终端或编程智能体中使用同一套逆向工程能力。 |
| **处理复杂流程** | REA 负责工具设置、打开应用、维持调查过程,并在完成后清理资源。 |
| **完整工作流** | 从初步概览推进到伪代码、调用关系、类型和实现线索。 |
| **本地运行** | 分析在你的 Mac 上运行;REA 不会把二进制上传到托管式分析服务。 |
| **保留上下文** | 连续调查多个二进制文件,无需为每个问题重新开始整个分析过程。 |
| | |
| ---------------- | -------------------------------------------------------------------- |
| **为智能体设计** | 直接询问应用的行为,让智能体去检查,而不是猜测。 |
| **CLI 与 MCP** | 在终端或智能体中使用同一套逆向工程能力。 |
| **引导式设置** | 配置智能体、连接已有分析工具,或在你批准后安装 Hopper。 |
| **从洞察到代码** | 理解功能后,在同一次编程会话中构建属于你自己的版本。 |
| **本地运行** | 分析在受支持的本机系统上运行;REA 不会把二进制上传到托管式分析服务。 |
| **保留上下文** | 连续调查多个应用,无需为每个问题重新开始整个分析过程。 |
## 快速开始
### 使用编程智能体(推荐)
### 运行设置(推荐)
配置 REA 与智能体的连接:
```bash
npx --yes rea-agents@latest setup
npx rea-agents setup
```
让智能体设置 REA。它会检查你的 Mac,说明需要安装的内容,先征得同意,并引导你处理系统提示。如果智能体要求重启以加载完整工具,请在设置后重启。
Setup 首先让你多选要连接的智能体。已有 REA 注册默认选中;仅被检测到的客户端不会自动选中,也可以手动选择尚未配置的客户端。查看具体路径和变更后再批准。所选智能体默认安装 REA 工作流;Hopper 是单独的可选操作,需要单独批准。Setup 也可以记录现有 Ghidra 的路径。
### 开始之前
Setup 会先展示变更并备份已有配置。要求与更多选项见[安装与设置](docs/installation.md)。
### 使用智能体
设置完成后,重启智能体并描述你想了解的应用或功能。REA 支持 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code 和 VS Code。已有 REA 注册默认选中;其他检测到的客户端需手动选择。其他智能体可以使用下方的 MCP 配置。
Hopper 支持演示模式。如果首次启动时出现提示,选择演示模式或输入已有许可证。
### AI 编程助手技能(可选)
为你的 AI 编程助手添加 skill,以获得更丰富的上下文:
```bash
npx skills add morluto/rea --skill reverse-engineer-anything
```
该 skill 提供 REA 的调查工作流。先运行上文的 setup,把 REA 连接到你的智能体并配置分析工具。Setup 默认会安装版本匹配的 skill;这条命令安装的是仓库版本。
对于已解包的 JavaScript/Electron 应用目录或 ASAR,无需 MCP 配置,也无需原生引擎:
```bash
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
把路径换成你的目标(例如 Windows 上的 `"D:/apps/example"`)。这条静态流程支持此类路径。原生分析仍需要先配置对应的分析引擎。
### 安装 rea 命令
安装命令行工具:
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
```
需要预先安装 Node.js 和 npm。在终端中运行时,安装程序会安装 `rea` 并启动设置。
也可以用 npm 安装,再运行设置:
```bash
npm install --global rea-agents
rea setup
```
### 系统要求
- macOS 12 或更高版本
- Ubuntu 24.04+、Fedora 41+ 或 64 位 Arch Linux
- Node.js 22.19+ 或 24.11+,以及随 Node 安装的 npm
- Node.js 22.x (>=22.19)、24.x (>=24.11) 或 26+,以及 npm
`rea setup` 会先显示完整变更计划并请求确认。它不会安装或更新 Homebrew、Node.js 或 npm。缺少 [Hopper](https://www.hopperapp.com/) 时,Setup 会提议安装官方软件包;Hopper 是需要单独许可证的独立软件。
原生二进制分析需要 Hopper 或 Ghidra。Hopper 是独立软件;演示模式有厂商规定的限制,不要求购买许可证。
如果你已在 64 位 Linux 上安装 Ghidra 12.1.4 PUBLIC 和完整的 64 位 JDK 21,Setup 也可以在批准后登记 `GHIDRA_INSTALL_DIR` 和可选的 `JAVA_HOME`。REA 不会下载、安装或修改 Ghidra 或 Java。Ghidra provider 在隔离的只读 headless 会话中提供清单、反编译、汇编、调用关系、带类型的引用、xref、CFG 与函数 dossier;GUI 状态和修改操作仍不可用。
Ghidra 支持 Linux x64 和 macOS x64/arm64。单独安装 Ghidra 12.1.4 和完整的 64 位 JDK 21,然后配置 REA 使用它们。macOS 还需要对应架构的原生反编译器。
#### Linux 安装与故障排除
Setup 可以验证安装并保存路径,不会安装或升级 Ghidra、Java、Node.js、npm 或 Homebrew。
在 Ubuntu 24.04+、Fedora 41+ 和 64 位 Arch Linux 上,REA 会下载 Hopper 官方的 DEB、RPM 或 Arch 软件包,验证发布方提供的大小和校验和,然后通过 `apt-get`、`dnf` 或 `pacman` 安装依赖。非 root 运行时,`pkexec` 会显示系统授权提示;REA 不调用 `sudo`。
仓库 main 分支与 npm 4.1.0 已包含实验性的 Windows x64 Ghidra P0 支持,适用于本地 NTFS 上的原生 x86-64 PE 应用(非托管、非 DLL),并附带 Job Object、私有 DACL 与路径准入控制。请先确认[发布边界](docs/installation.md#released-package-and-main),再假定旧版 npm 包具备该能力。详见 [Windows Ghidra P0](docs/windows-ghidra-p0.md) 的前置条件与已验证范围。
默认启动器是 `/opt/hopper/bin/Hopper`。自定义安装可设置 `HOPPER_LAUNCHER_PATH`。如果 Doctor 报告缺少分析引擎,请运行 `ldd /opt/hopper/bin/Hopper | grep 'not found'`,安装缺少的系统库后重新运行 `rea setup`。REA 会在私有 Xvfb 显示器上运行受支持的 Hopper 演示版,并为每个分析会话选择 Hopper 提供的演示模式;无需桌面 `DISPLAY` 或付费许可证,但演示版受厂商限制。Linux 上还应确保 `~/.local/bin` 位于 `PATH` 中。
### 故障排除
```bash
# 1. 安装和配置 REA
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx --yes rea-agents@latest setup
```
运行 `npx -y rea-agents@latest doctor`,检查主机、依赖、分析工具和智能体配置。该命令不会修改文件。添加 `--json` 可获取结构化诊断。
如果 macOS 或安装程序要求确认,请完成提示,然后再次运行同一命令。
Linux 上的默认 Hopper 启动器为 `/opt/hopper/bin/Hopper`。其他路径可通过 `HOPPER_LAUNCHER_PATH` 指定。如果文件存在但仍报告缺少分析引擎,运行 `ldd /opt/hopper/bin/Hopper | grep 'not found'` 检查缺少的系统库。安装详情见 [Hopper 指南](docs/installation.md#hopper)。
### 2. 重启编程智能体
### 更新与卸载
Setup 会检测 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf 和 Devin。它会自动配置检测到的前六种客户端;由于目前没有明确的本地 MCP 配置边界,检测到 Devin 时只会报告而不会修改。重启已配置的客户端,让它加载 REA。
- `rea update` 更新当前 REA 安装。
- `rea uninstall` 移除 REA 管理的智能体配置项和工作流文件,保留 Hopper。
- `rea uninstall --purge-data` 还会删除 REA 的缓存和状态。仅在需要移除这些数据时使用。
### 3. 直接询问智能体
## 当前状态
你可以直接说出应用名称。编程智能体会找到应用,并把 REA 所需的程序文件交给它。
仓库当前能力与平台要求见[英文支持指南](README.md#current-status)。main 分支可能领先于 [npm 发布版](docs/installation.md#released-package-and-main)。
```text
使用 REA 逆向工程“备忘录”应用。解释搜索功能的工作方式,展示证据,
然后使用 SQLite 为我的项目构建一个类似功能。
```
如果遇到问题,请运行:
```bash
npx -y rea-agents@latest doctor
rea uninstall
rea uninstall --purge-data
```
- Ghidra 在 Linux x64、macOS x64/arm64 以及实验性的 Windows x64 P0 边界上提供 25 个只读操作。Linux/macOS 额外支持原子化的会话级函数注释。Windows P0 为只读;Ghidra 不提供 GUI 控制。
- 静态 Android 检查需要自备 JADX/Java,前置条件独立;真机验证覆盖 Linux。详见 [Android 分析](docs/android-analysis.md)。
- 浏览器、Electron 与进程请求需直接声明目标、动作与生命周期,并依赖宿主的实际访问权限,无需 REA 另行授权。Setup 的配置写入与 Hopper 安装仍需逐项批准确切计划。
- `rea capabilities` 描述的是二进制会话操作,而非全部应用/运行时工具。请以已连接 MCP 工具列表与相关指南为准。
## 一个提示词,完成一次完整调查
@@ -149,22 +200,28 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
## 调查工具目录
| 工具类别 | 数量 | 示例 |
| ----------------- | ---: | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| 二进制检查 | 36 | 过程、伪代码、汇编、字符串、名称、段、调用者、被调用者、交叉引用、注释 |
| 组合分析 | 14 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`、Swift 与 ObjC 发现 |
| macOS 原生工具 | 5 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原,无需启动 Hopper |
| 制品图谱 | 3 | 目录及支持的软件包的完整内联检查,以及显式选择的事务式提取 |
| Managed PE/CLI | 8 | PE/CLI 身份、元数据成员、CIL 哈希、P/Invoke/原生边界声明与验证、应用图谱投影、反编译重建导入、结构化 token 重映射、运行时关联计划与跨版本比较 |
| 浏览器观察 | 9 | 精确来源 CDP 抓取、bundle/source map 分析、WebMCP 发现、会话时间线、capture diff 与视觉证据 |
| Electron 分析 | 5 | 限定 canonical 文件根的被动观察、有界静态应用映射,以及基于 Evidence 的静态/运行时对账 |
| JavaScript 运行时 | 2 | 经批准的只附加 Node/Electron Inspector 目标发现,以及不执行求值或插桩的有界脚本和执行上下文观察 |
| 应用工作流 | 10 | 有界跨层追踪、唯一匹配跨版本图比较、静态导出返回形状比较、经批准的 Linux 隔离 extracted-module replay、托管运行时表征、重建覆盖闭合和确定性义务账本 |
| 二进制会话 | 22 | `open_binary`、`binary_session`、证据包、有限重放状态机、进程、制品与函数比较、残余未知项注册表 |
| 工具类别 | 数量 | 用途 |
| ----------------- | ---: | ------------------------------------------------------------------------------------------------------ |
| 原生检查 | 41 | 函数、伪代码、汇编、字符串、符号、调用、引用、注释、字节读取与文件偏移 |
| 调查工作流 | 14 | 应用概览、函数档案、原生 API 与分发、批量反编译、功能追踪、调用路径与调用图、Swift 与 Objective-C 发现 |
| macOS 原生工具 | 7 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原,无需启动 Hopper |
| 产物图 | 5 | 目录与软件包清单、编译后的 Interface Builder 文件、Apple 资源目录与提取 |
| 托管 PE/CLI | 7 | .NET 程序集身份、元数据、CIL 指令、原生依赖声明、重建导入与构建比较 |
| 固件 | 2 | Linux 固件区域检查与显式提取 |
| Android APK | 5 | 包与 manifest 声明、类搜索、成员清单、方法反编译与静态引用 |
| 浏览器观察 | 9 | 页面结构、网络元数据、脚本、来源映射、WebMCP 发现、截图与捕获比较 |
| Electron 分析 | 5 | 渲染进程观察、静态应用映射、静态/运行时结果关联 |
| JavaScript 运行时 | 2 | Node/Electron Inspector 目标发现、脚本位置与执行上下文事件 |
| 应用工作流 | 8 | 捕获的网站脚本导出、跨层功能追踪、构建比较、历史源码映射、静态返回结构比较与重建验证 |
| 工作区与观察 | 21 | 会话、证据包、导航上下文、进程/产物/函数比较与待解决问题跟踪 |
## 路线图
接下来的工作包括更多原生目标验证、JavaScript 与 .NET 跨版本比较,以及运行时观察和重建验证。当前设置已支持选择智能体集成和 Hopper 安装;安装其他分析工具仍是后续工作。详见[安装路线图](docs/roadmap.md)和[分析工具评估](docs/provider-evaluation.md)。
## 与其他编程智能体一起使用
Setup 会检测 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf 和 Devin,并自动配置检测到的前六种客户端;检测到 Devin 时只会报告而不会修改。任何支持本地 MCP 服务器的编程智能体都可以使用以下配置连接 REA。
Setup 支持 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code 和 VS Code。已有 REA 注册默认选中;其他检测到的客户端需手动选择。任何支持本地 MCP 服务器的智能体都可以使用以下配置连接 REA。
<!-- x-release-please-start-version -->
@@ -173,7 +230,7 @@ Setup 会检测 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Win
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@3.2.1", "mcp"]
"args": ["-y", "rea-agents@4.1.0", "mcp"]
}
}
}
@@ -193,7 +250,7 @@ flowchart LR
Ghidra --> App
```
CLI 与 MCP 服务器使用相同的分析引擎。终端命令完成后会关闭应用;智能体会话则会在调查期间保持应用打开。
CLI 与 MCP 服务器使用相同的分析流程。终端命令完成后释放自己创建的桥接会话;智能体会话可以在调查期间保持连接。关闭 REA 会话不会退出用户正在使用的 Hopper 应用。
## CLI
@@ -223,7 +280,7 @@ REA 会推导明确的格式和架构参数,以避免常见的 FAT 与 ARM 选
## 安全模型
每个桥会话都使用随机能力令牌和仅限当前用户的 Unix 套接字。协议消息有大小限制,面向调用者的错误不会暴露启动器 stderr 或内部异常原因。Ghidra 会话还使用隔离的临时项目,不会打开或修改用户自己的 Ghidra 项目。
每个桥会话都使用随机能力令牌和仅限当前用户的 Unix 套接字。诊断保留本机路径、摘要和错误位置等排查信息,同时移除凭据和认证令牌。Ghidra 会话还使用隔离的临时项目,不会打开或修改用户自己的 Ghidra 项目。
这不是沙箱,也无法防御以同一操作系统用户身份运行的恶意进程。打开不可信二进制文件会让所选本地提供商以当前用户权限进行解析和分析。请按照 [SECURITY.md](SECURITY.md) 中的私密流程报告漏洞。
@@ -231,7 +288,7 @@ REA 会推导明确的格式和架构参数,以避免常见的 FAT 与 ARM 选
<details><summary><strong>Hopper 是否需要提前运行?</strong></summary>
不需要。REA 会在操作需要时启动 Hopper,也支持已经运行的 Hopper。
不需要。REA 会在操作需要时启动 Hopper。Hopper 已经运行时也可以使用,但 REA 会打开新的分析文档,不会连接并复用现有 GUI 文档。
</details>
File diff suppressed because it is too large Load Diff
+113
View File
@@ -0,0 +1,113 @@
import com.google.gson.JsonArray;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import ghidra.program.model.address.Address;
import ghidra.program.model.data.DataType;
import ghidra.program.model.listing.Program;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import java.lang.reflect.InvocationTargetException;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;
/** Session-owned extension loading; analyzer algorithms stay behind each adapter ABI. */
final class ReaGhidraExtensions implements AutoCloseable {
private record Loaded(URLClassLoader loader, Object instance, String sha256) {}
private final List<Loaded> loaded = new ArrayList<>();
private final JsonArray reports = new JsonArray();
void analyze(JsonArray descriptors, Program program, TaskMonitor monitor) throws Exception {
for (var item : descriptors) {
monitor.checkCancelled();
JsonObject descriptor = item.getAsJsonObject();
String id = descriptor.get("id").getAsString();
String entry = descriptor.get("entry_class").getAsString();
String digest = descriptor.get("sha256").getAsString();
JsonObject report = new JsonObject();
report.addProperty("id", id);
report.addProperty("sha256", digest);
URLClassLoader loader = null;
try {
if (!id.equals("nativeaot") || !entry.equals("rea.extensions.nativeaot.NativeAotExtension") ||
descriptor.get("integration_api").getAsInt() != 1)
throw new IllegalArgumentException("Unregistered Ghidra extension ABI: " + id);
Path path = Path.of(descriptor.get("path").getAsString());
if (!path.isAbsolute() || !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.size(path) > 8 * 1024 * 1024)
throw new IllegalArgumentException("Invalid private extension artifact: " + path);
String measured = java.util.HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(path)));
if (!measured.equals(digest)) throw new IllegalArgumentException("Private extension digest mismatch: " + id);
loader = new URLClassLoader(new java.net.URL[] { path.toUri().toURL() }, getClass().getClassLoader());
Class<?> type = Class.forName(entry, true, loader);
Object instance = type.getConstructor().newInstance();
Object response = type.getMethod("analyze", Program.class, TaskMonitor.class).invoke(instance, program, monitor);
if (!(response instanceof JsonObject result)) throw new IllegalArgumentException("Invalid Ghidra extension result: " + id);
report.addProperty("status", result.get("status").getAsString());
report.add("reason", result.get("reason").deepCopy());
report.add("result", result);
loaded.add(new Loaded(loader, instance, digest));
loader = null;
} catch (InvocationTargetException ex) {
if (ex.getCause() instanceof CancelledException cancelled) throw cancelled;
failure(report, ex.getCause());
} catch (Exception | LinkageError ex) {
failure(report, ex);
} finally {
if (loader != null) loader.close();
}
reports.add(report);
}
}
JsonArray reports() { return reports.deepCopy(); }
JsonArray inspectImage() throws Exception {
JsonArray summaries = new JsonArray();
for (Loaded extension : loaded) {
Object response = extension.instance.getClass().getMethod("inspectImage").invoke(extension.instance);
if (!(response instanceof JsonObject summary)) throw new IllegalArgumentException("Invalid extension image summary");
summary.addProperty("analysis_artifact_sha256", extension.sha256);
summaries.add(summary);
}
return summaries;
}
JsonObject inspectDataType(Program program, DataType type, Address address, TaskMonitor monitor) throws Exception {
for (Loaded extension : loaded) {
try {
Object response = extension.instance.getClass().getMethod("inspectDataType", Program.class, DataType.class, Address.class, TaskMonitor.class)
.invoke(extension.instance, program, type, address, monitor);
if (response instanceof JsonObject result) return result;
if (response != null) throw new IllegalArgumentException("Invalid extension type metadata result");
} catch (InvocationTargetException ex) {
if (ex.getCause() instanceof CancelledException cancelled) throw cancelled;
throw new IllegalStateException("Ghidra extension type inspection failed: " + ex.getCause(), ex.getCause());
}
}
return null;
}
private static void failure(JsonObject report, Throwable ex) {
String reason = "Extension loading/analysis failed: " + ex;
if (ex.getCause() != null) reason += "; caused by " + ex.getCause();
report.addProperty("status", "failed");
report.addProperty("reason", reason);
JsonObject result = new JsonObject();
result.addProperty("loader_failure", reason);
report.add("result", result);
}
@Override public void close() throws Exception {
Exception failure = null;
for (Loaded extension : loaded) {
try { extension.loader.close(); }
catch (Exception ex) { if (failure == null) failure = ex; else failure.addSuppressed(ex); }
}
loaded.clear();
if (failure != null) throw failure;
}
}
+32
View File
@@ -0,0 +1,32 @@
import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import java.math.BigInteger;
/** Seed the declared DOS COM analysis context before default auto-analysis. */
public final class ReaGhidraPrepareCom extends GhidraScript {
@Override
public void run() throws Exception {
if (!currentProgram.getLanguageID().getIdAsString().equals("x86:LE:16:Real Mode") ||
!currentProgram.getExecutableFormat().equals("Raw Binary")) {
throw new IllegalArgumentException("DOS COM preparation requires raw x86 real-mode import");
}
Address entry = currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress("1000:0100");
Address start = currentProgram.getMinAddress();
Address end = currentProgram.getMaxAddress();
if (!entry.equals(start) || end.subtract(start) + 1 > 0xff00) {
throw new IllegalArgumentException("DOS COM import must map 1..65280 bytes at 1000:0100");
}
for (String name : new String[] { "CS", "DS", "ES", "SS" }) {
monitor.checkCancelled();
currentProgram.getProgramContext().setValue(
currentProgram.getRegister(name), start, end, BigInteger.valueOf(0x1000)
);
}
addEntryPoint(entry);
if (!disassemble(entry) || createFunction(entry, "entry") == null) {
throw new IllegalStateException("DOS COM entry cannot be decoded as a function");
}
currentProgram.getOptions(Program.PROGRAM_INFO).setBoolean("REA DOS COM prepared", true);
}
}
@@ -0,0 +1,70 @@
package rea.extensions.nativeaot;
import ghidra.app.util.importer.MessageLog;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import ghidra.util.task.TaskMonitor;
import java.util.LinkedHashSet;
import nativeaot.rtr.ReadyToRunDirectory;
import nativeaot.rtr.ReadyToRunSection;
import nativeaot.rtr.SignatureReadyToRunLocator;
import nativeaot.rtr.SymbolReadyToRunLocator;
/** Validate the producer's NativeAOT row layout before database mutation. */
final class NativeAotDirectory {
record Located(Address address, ReadyToRunDirectory directory, String discovery) {}
static Located locate(Program program, TaskMonitor monitor, MessageLog log) throws Exception {
Address[] symbols = SymbolReadyToRunLocator.instance.locateModules(program, monitor, log);
String discovery = symbols.length == 0 ? "signature-heuristic" : "symbol";
Address[] found = symbols.length == 0
? SignatureReadyToRunLocator.instance.locateModules(program, monitor, log) : symbols;
var candidates = new LinkedHashSet<Address>();
for (Address address : found) if (address.getOffset() != 0) candidates.add(address);
if (candidates.isEmpty()) return null;
if (candidates.size() != 1)
throw new UnsupportedOperationException("Ambiguous NativeAOT directory candidates: " + candidates);
Address address = candidates.iterator().next();
var memory = program.getMemory();
byte[] header = new byte[16];
if (memory.getBytes(address, header) != header.length)
throw new IllegalArgumentException("Incomplete NativeAOT header at " + address);
int count = Short.toUnsignedInt(memory.getShort(address.add(12)));
if (count < 1 || count > 80 || Byte.toUnsignedInt(header[14]) != 24 || header[15] != 1)
throw new UnsupportedOperationException("Unsupported NativeAOT row layout at " + address +
": sections=" + count + ", entry_size=" + Byte.toUnsignedInt(header[14]) + ", entry_type=" + Byte.toUnsignedInt(header[15]));
byte[] rows = new byte[count * 24];
if (memory.getBytes(address.add(16), rows) != rows.length)
throw new IllegalArgumentException("Incomplete NativeAOT directory rows at " + address);
ReadyToRunDirectory directory = ReadyToRunDirectory.readAtAddress(program, address);
if (directory.getMajorVersion() != 9 || directory.getMinorVersion() != 1)
throw new UnsupportedOperationException("Unsupported NativeAOT RTR format " +
directory.getMajorVersion() + "." + directory.getMinorVersion() + " at " + address + "; verified format is 9.1 (.NET 8.0.22 Linux x64 fixture).");
var types = new LinkedHashSet<Integer>();
for (ReadyToRunSection section : directory.getSections()) {
monitor.checkCancelled();
if (!types.add(section.getType())) throw new IllegalArgumentException("Duplicate NativeAOT section " + section.getType() + " at " + address);
// ModuleInfoRow flags bit 0 declares an end pointer. ThreadStaticIndex
// (204) is a single-address entry with flags=0 and an absent end.
if (section.getFlags() != 0 && section.getFlags() != 1)
throw new UnsupportedOperationException("Unsupported NativeAOT section flags " + section.getFlags() + " for section " + section.getType());
boolean hasEnd = section.getFlags() == 1;
if (!hasEnd && section.getEnd() != 0)
throw new IllegalArgumentException("NativeAOT single-address section " + section.getType() + " reports an unexpected end");
if (hasEnd && Long.compareUnsigned(section.getStart(), section.getEnd()) > 0)
throw new IllegalArgumentException("Reversed NativeAOT section " + section.getType() + " at " + address);
if (section.getStart() != 0 && (!hasEnd || section.getStart() != section.getEnd()) &&
(!memory.contains(address.getNewAddress(section.getStart())) ||
(hasEnd && !memory.contains(address.getNewAddress(section.getEnd() - 1)))))
throw new IllegalArgumentException("Unmapped NativeAOT section " + section.getType() + " at " + address);
}
if (directory.getSectionByType(ReadyToRunSection.DEHYDRATED_DATA) == null)
throw new UnsupportedOperationException("NativeAOT directory at " + address + " has no DEHYDRATED_DATA; non-dehydrated layouts are not verified.");
for (int type : new int[] { ReadyToRunSection.DEHYDRATED_DATA, ReadyToRunSection.FROZEN_OBJECT_REGION }) {
ReadyToRunSection section = directory.getSectionByType(type);
if (section != null && section.getFlags() != 1)
throw new IllegalArgumentException("NativeAOT section " + type + " requires an end pointer");
}
return new Located(address, directory, discovery);
}
}
@@ -0,0 +1,183 @@
package rea.extensions.nativeaot;
import com.google.gson.JsonArray;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import ghidra.app.util.importer.MessageLog;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSet;
import ghidra.program.model.data.DataType;
import ghidra.program.model.listing.Program;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import nativeaot.objectmodel.FrozenObjectAnnotator;
import nativeaot.objectmodel.MethodTableCrawler;
import nativeaot.objectmodel.MethodTableManager;
import nativeaot.rehydration.MetadataRehydratorNet80;
import nativeaot.rtr.ReadyToRunSection;
import java.util.ArrayList;
import java.util.List;
import java.security.MessageDigest;
/** Optional headless facade over the unchanged upstream analysis primitives. */
public final class NativeAotExtension {
private MethodTableManager manager;
private NativeAotDirectory.Located located;
private JsonObject report;
private final List<String> diagnostics = new ArrayList<>();
private JsonObject derivedRange;
public JsonObject analyze(Program program, TaskMonitor monitor) throws Exception {
report = new JsonObject();
report.addProperty("id", "nativeaot");
report.addProperty("integration_api", 1);
report.addProperty("source_revision", "effeb734fc570c32650f88b159608979dc7b423e");
report.addProperty("source_revision_authority", "build-reported-unattested");
report.addProperty("status", "not_applicable");
report.addProperty("reason", "No NativeAOT directory with the supported row layout was found.");
report.addProperty("method_tables", 0);
if (!program.getLanguageID().getIdAsString().startsWith("x86:LE:64"))
return failure("unsupported", "NativeAOT adapter supports x86-64 only.");
MessageLog log = new MessageLog();
int transaction = program.startTransaction("REA NativeAOT metadata recovery");
boolean commit = false;
String stage = "directory-discovery";
try {
located = NativeAotDirectory.locate(program, monitor, log);
if (located == null) return finish(log);
report.addProperty("header_address", address(located.address()));
report.addProperty("discovery", located.discovery());
report.addProperty("format_major", located.directory().getMajorVersion());
report.addProperty("format_minor", located.directory().getMinorVersion());
stage = "metadata-rehydration";
var section = located.directory().getSectionByType(ReadyToRunSection.DEHYDRATED_DATA);
Address start = located.address().getNewAddress(section.getStart());
Address end = located.address().getNewAddress(section.getEnd());
if (start.compareTo(end) >= 0) throw new IllegalArgumentException("Empty dehydrated section at " + start);
var scan = new MetadataRehydratorNet80().rehydrate(program, new AddressSet(start, end).getFirstRange(), monitor, log);
// Upstream reports an inclusive end at base + written length.
long writtenLength = scan.getScanningRange().getLength() - 1;
derivedRange = new JsonObject();
derivedRange.addProperty("address", address(scan.getScanningRange().getMinAddress()));
derivedRange.addProperty("size_bytes", writtenLength);
derivedRange.addProperty("sha256", hashMemory(program, scan.getScanningRange().getMinAddress(), writtenLength, monitor));
derivedRange.add("file_offset", JsonNull.INSTANCE);
stage = "method-table-recovery";
manager = MethodTableManager.createForDirectory(program, located.directory());
var conventions = NativeAotMethodConventions.capture(program, monitor);
new MethodTableCrawler(manager, program, scan).analyze(monitor, log);
stage = "calling-convention-preservation";
NativeAotMethodConventions.restore(program, manager, conventions, monitor);
log.appendMsg("REA", "Preserved pre-recovery calling conventions; new functions use the loaded compiler specification default. Original method signatures remain unknown.");
if (manager.getObjectMT() == null || manager.getMethodTableCount() == 0)
throw new UnsupportedOperationException("No unambiguous System.Object method table was recovered at header " + located.address());
stage = "frozen-object-annotation";
if (manager.getStringMT() == null || scan.getPointerLocations().length == 0) {
diagnostics.add("Frozen-object annotation unavailable: System.String table or pointer locations were not recovered.");
} else {
new FrozenObjectAnnotator(program, manager).analyze(located.directory(), scan.getPointerLocations(), monitor, log);
}
monitor.checkCancelled();
report.addProperty("method_tables", manager.getMethodTableCount());
JsonObject coverage = auditFrozenObjects(program, scan.getPointerLocations(), monitor);
report.add("coverage", coverage);
if (coverage.get("frozen_objects_annotated").getAsInt() != coverage.get("frozen_object_candidates").getAsInt())
diagnostics.add("Some frozen-object candidates lack committed instance annotations; inspect coverage counts and Ghidra diagnostics.");
report.addProperty("status", diagnostics.isEmpty() ? "complete" : "partial");
report.add("reason", JsonNull.INSTANCE);
report.add("derived_memory", derivedRange.deepCopy());
JsonArray tables = new JsonArray();
for (var mt : manager.getMethodTables()) {
monitor.checkCancelled();
JsonObject identity = new JsonObject();
identity.addProperty("address", address(mt.getAddress()));
identity.addProperty("type", mt.getMTType().getPathName());
tables.add(identity);
}
report.add("types", tables);
commit = true;
return finish(log);
} catch (CancelledException ex) {
manager = null;
throw ex;
} catch (Exception ex) {
manager = null;
diagnostics.add(stage + ": " + ex.getClass().getSimpleName() + ": " + ex.getMessage());
report.addProperty("status", ex instanceof UnsupportedOperationException ? "unsupported" : "failed");
report.addProperty("reason", stage + ": " + ex.getMessage());
return finish(log);
} finally {
program.endTransaction(transaction, commit);
}
}
/** Inline discovery for the existing loaded-image inspection. */
public JsonObject inspectImage() {
JsonObject summary = new JsonObject();
summary.addProperty("format", "dotnet-nativeaot");
for (String key : new String[] { "status", "reason", "header_address", "discovery", "format_major", "format_minor", "method_tables", "derived_memory", "coverage", "diagnostics" })
summary.add(key, report.has(key) ? report.get(key).deepCopy() : JsonNull.INSTANCE);
summary.add("types", report.has("types") ? report.get("types").deepCopy() : new JsonArray());
JsonArray limitations = new JsonArray();
limitations.add("Recovery covers the reported metadata candidates; it does not reconstruct original C# source, custom field layouts, every runtime object, or execution behavior.");
limitations.add("Method prototypes and parameter names remain Ghidra inferences; verify them against instructions and call sites. Pre-recovery conventions are retained rather than universally forcing __thiscall.");
limitations.add("NativeAOT type identities and relationships are inferred; generated names are not original names. Derived memory has no original file offsets.");
summary.add("limitations", limitations);
return summary;
}
/** Read recovered relationships without restoring or mutating database state. */
public JsonObject inspectDataType(Program program, DataType type, Address selected, TaskMonitor monitor) throws Exception {
if (manager == null) return null;
return NativeAotTypeMetadata.describe(program, manager, located, type, report, derivedRange, monitor);
}
private JsonObject auditFrozenObjects(Program program, Address[] pointerLocations, TaskMonitor monitor) throws Exception {
JsonObject coverage = new JsonObject();
int candidates = 0, annotated = 0;
var section = located.directory().getSectionByType(ReadyToRunSection.FROZEN_OBJECT_REGION);
if (section != null) for (Address location : section.getPointersInSection(pointerLocations)) {
monitor.checkCancelled();
var table = manager.getMethodTable(program.getMemory().getLong(location));
if (table == null) continue;
candidates++;
var expected = table.getInstanceType();
var actual = program.getListing().getDataAt(location);
if (expected != null && actual != null && actual.getDataType().getPathName().equals(expected.getPathName())) annotated++;
}
coverage.addProperty("frozen_object_candidates", candidates);
coverage.addProperty("frozen_objects_annotated", annotated);
coverage.addProperty("basis", "rehydrated-pointer-candidates-and-committed-instance-types");
return coverage;
}
private JsonObject finish(MessageLog log) {
if (!log.toString().isBlank()) diagnostics.add(log.toString());
JsonArray messages = new JsonArray();
for (String message : diagnostics) messages.add(message);
report.add("diagnostics", messages);
return report;
}
private JsonObject failure(String status, String reason) {
report.addProperty("status", status);
report.addProperty("reason", reason);
return finish(new MessageLog());
}
static String address(Address value) { return "0x" + Long.toUnsignedString(value.getOffset(), 16); }
private static String hashMemory(Program program, Address start, long length, TaskMonitor monitor) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] buffer = new byte[65536];
for (long offset = 0; offset < length;) {
monitor.checkCancelled();
int count = (int)Math.min(buffer.length, length - offset);
if (program.getMemory().getBytes(start.add(offset), buffer, 0, count) != count)
throw new IllegalStateException("Incomplete derived-memory read at " + start.add(offset));
digest.update(buffer, 0, count);
offset += count;
}
return java.util.HexFormat.of().formatHex(digest.digest());
}
}
@@ -0,0 +1,38 @@
package rea.extensions.nativeaot;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import ghidra.util.task.TaskMonitor;
import nativeaot.objectmodel.MethodTableManager;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
/** Keep upstream's unconditional __thiscall assignment out of x64 ABI evidence. */
final class NativeAotMethodConventions {
static Map<Address, String> capture(Program program, TaskMonitor monitor) throws Exception {
Map<Address, String> result = new HashMap<>();
var functions = program.getFunctionManager().getFunctions(true);
while (functions.hasNext()) {
monitor.checkCancelled();
var function = functions.next();
result.put(function.getEntryPoint(), function.getCallingConventionName());
}
return result;
}
static void restore(Program program, MethodTableManager manager, Map<Address, String> previous, TaskMonitor monitor) throws Exception {
var visited = new HashSet<Address>();
String defaultConvention = program.getCompilerSpec().getDefaultCallingConvention().getName();
for (var table : manager.getMethodTables()) for (long target : table.getVTable()) {
monitor.checkCancelled();
Address address = table.getAddress().getNewAddress(target);
if (!visited.add(address)) continue;
var function = program.getFunctionManager().getFunctionAt(address);
if (function == null) continue;
String convention = previous.containsKey(address) ? previous.get(address) : defaultConvention;
if (!java.util.Objects.equals(function.getCallingConventionName(), convention))
function.setCallingConvention(convention);
}
}
}
@@ -0,0 +1,70 @@
package rea.extensions.nativeaot;
import com.google.gson.JsonArray;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import ghidra.program.model.data.DataType;
import ghidra.program.model.listing.Program;
import ghidra.util.task.TaskMonitor;
import nativeaot.objectmodel.MethodTable;
import nativeaot.objectmodel.MethodTableManager;
/** Translate a selected recovered type into provider-neutral metadata evidence. */
final class NativeAotTypeMetadata {
static JsonObject describe(Program program, MethodTableManager manager, NativeAotDirectory.Located located,
DataType type, JsonObject report, JsonObject derivedRange, TaskMonitor monitor) throws Exception {
for (MethodTable table : manager.getMethodTables()) {
monitor.checkCancelled();
boolean sameType = type.getPathName().equals(table.getMTType().getPathName()) ||
(table.getInstanceType() != null && type.getPathName().equals(table.getInstanceType().getPathName()));
if (!sameType) continue;
JsonObject result = new JsonObject();
result.addProperty("format", "dotnet-nativeaot");
result.addProperty("status", report.get("status").getAsString());
result.addProperty("header_address", NativeAotExtension.address(located.address()));
result.addProperty("format_major", located.directory().getMajorVersion());
result.addProperty("format_minor", located.directory().getMinorVersion());
result.addProperty("method_table_address", NativeAotExtension.address(table.getAddress()));
result.addProperty("name_origin", table == manager.getObjectMT() || table == manager.getStringMT() ? "inferred" : "generated");
result.add("original_name", JsonNull.INSTANCE);
result.addProperty("base_size_bytes", table.getBaseSize());
result.add("related_type", identity(manager, table.getRelatedTypeAddress()));
JsonArray interfaces = new JsonArray();
for (long value : table.getInterfaceAddresses()) if (value != 0) interfaces.add(identity(manager, value));
result.add("interfaces", interfaces);
JsonArray slots = new JsonArray();
for (int slot = 0; slot < table.getVTableSlotCount(); slot++) {
monitor.checkCancelled();
long value = table.getVTableSlot(slot);
JsonObject target = new JsonObject();
target.addProperty("slot", slot);
target.addProperty("slot_address", NativeAotExtension.address(table.getAddress().add(24 + slot * 8L)));
target.add("target_address", value == 0 ? JsonNull.INSTANCE : new com.google.gson.JsonPrimitive("0x" + Long.toUnsignedString(value, 16)));
var function = value == 0 ? null : program.getFunctionManager().getFunctionAt(table.getAddress().getNewAddress(value));
target.add("procedure_name", function == null ? JsonNull.INSTANCE : new com.google.gson.JsonPrimitive(function.getName(true)));
target.addProperty("basis", "method-table-pointer");
slots.add(target);
}
result.add("virtual_slots", slots);
result.add("derived_memory", derivedRange.deepCopy());
result.add("diagnostics", report.get("diagnostics").deepCopy());
JsonArray limitations = new JsonArray();
limitations.add("Type identities and relationships are recovered by upstream heuristics. Original source names and custom field layouts are not recovered.");
limitations.add("Method prototypes are inferred, not original signatures. REA preserves pre-recovery calling conventions and the loaded compiler default for new functions.");
limitations.add("Rehydrated metadata bytes are derived analysis-memory content; their original file offsets and runtime behavior remain unknown.");
limitations.add("Verified workflow: .NET 8.0.22 NativeAOT RTR 9.1, Linux x64 ELF / Windows x64 PE on Linux; other runtime layouts and hosts are unverified.");
result.add("limitations", limitations);
return result;
}
return null;
}
private static com.google.gson.JsonElement identity(MethodTableManager manager, long value) throws Exception {
if (value == 0) return JsonNull.INSTANCE;
JsonObject result = new JsonObject();
result.addProperty("address", "0x" + Long.toUnsignedString(value, 16));
MethodTable table = manager.getMethodTable(value);
result.add("type", table == null ? JsonNull.INSTANCE : new com.google.gson.JsonPrimitive(table.getMTType().getPathName()));
return result;
}
}
+70 -27
View File
@@ -128,6 +128,10 @@ class CapabilityUnavailableError(Exception):
"""The active Hopper build lacks one admitted public API operation."""
class InvalidRequestError(Exception):
"""Caller input failed bridge request or operation validation."""
EXHAUSTIVE_ANALYSIS_METHODS = frozenset(
(
"analyze_function",
@@ -150,6 +154,11 @@ EXHAUSTIVE_ANALYSIS_METHODS = frozenset(
def _hex(value):
if not isinstance(value, int) or isinstance(value, bool):
try:
value = int(str(value), 16)
except ValueError as error:
raise ValueError("Hopper returned a non-hexadecimal address") from error
return "0x%x" % value
@@ -177,8 +186,8 @@ def _document(name=None):
current = api.current_document()
if current is not None and current.getDocumentName() == name:
return current
raise ValueError("Hopper document name is ambiguous")
raise ValueError("Unknown Hopper document")
raise InvalidRequestError("Hopper document name is ambiguous")
raise InvalidRequestError("Unknown Hopper document")
if _selected_document is not None:
for candidate in documents:
if candidate.getDocumentName() == _selected_document:
@@ -188,7 +197,7 @@ def _document(name=None):
return session_document
current = api.current_document()
if current is None:
raise ValueError("No Hopper document is loaded")
raise CapabilityUnavailableError("No Hopper document is loaded")
return current
@@ -197,20 +206,20 @@ def _address(document, value=None):
if value is None:
return document.getCurrentAddress()
if not isinstance(value, str):
raise ValueError("Address must be a string")
raise InvalidRequestError("Address must be a string")
try:
return int(value, 16)
except ValueError:
result = document.getAddressForName(value)
if result in BAD_ADDRESSES:
raise ValueError("Unknown Hopper address or name")
raise InvalidRequestError("Unknown Hopper address or name")
return result
def _segment(document, address):
result = document.getSegmentAtAddress(address)
if result is None:
raise ValueError("Address is outside every segment")
raise InvalidRequestError("Address is outside every segment")
return result
@@ -218,7 +227,7 @@ def _procedure(document, value=None):
address = document.getCurrentAddress() if value is None else _address(document, value)
result = _segment(document, address).getProcedureAtAddress(address)
if result is None:
raise ValueError("No procedure exists at the requested address")
raise InvalidRequestError("No procedure exists at the requested address")
return result
@@ -232,6 +241,10 @@ def _procedure_identity(procedure):
"address": _hex(procedure.getEntryPoint()),
"name": _procedure_name(procedure),
"classification": None,
"body": {
"available": False,
"reason": "Hopper's public Python API does not expose complete function body ranges",
},
}
@@ -278,7 +291,7 @@ def _procedure_references(document, params):
procedure = _procedure(document, params.get("procedure"))
direction = params.get("direction", "outgoing")
if direction not in ("incoming", "outgoing"):
raise ValueError("direction must be incoming or outgoing")
raise InvalidRequestError("direction must be incoming or outgoing")
addresses = _instruction_addresses(procedure)
edges = set()
for address in addresses:
@@ -300,6 +313,12 @@ def _procedure_references(document, params):
})
return {
"procedure": _procedure_identity(procedure), "direction": direction,
"reference_kinds_available": False,
# The public Hopper API returns observed references but does not expose
# enough flow metadata to enumerate calls with no resolved target.
# Keep the required list field explicit; the provider records this as
# unknown coverage rather than claiming the list is exhaustive.
"unresolved_calls": [],
"references": items,
}
@@ -366,13 +385,13 @@ def _search_inventory(document, kind):
def _search_results(document, kind, params):
pattern = params.get("pattern")
if not isinstance(pattern, str) or not pattern:
raise ValueError("pattern must be a non-empty string")
raise InvalidRequestError("pattern must be a non-empty string")
mode = params.get("mode", "literal")
if mode not in ("literal", "regex"):
raise ValueError("mode must be literal or regex")
raise InvalidRequestError("mode must be literal or regex")
case_sensitive = params.get("case_sensitive", False)
if not isinstance(case_sensitive, bool):
raise ValueError("case_sensitive must be a boolean")
raise InvalidRequestError("case_sensitive must be a boolean")
if mode == "literal":
needle = pattern if case_sensitive else pattern.casefold()
@@ -381,7 +400,7 @@ def _search_results(document, kind, params):
try:
expression = re.compile(pattern, 0 if case_sensitive else re.IGNORECASE)
except (re.error, OverflowError) as error:
raise ValueError("Invalid regex pattern") from error
raise InvalidRequestError("Invalid regex pattern") from error
matches = lambda value: expression.search(value) is not None
selected = []
@@ -544,7 +563,11 @@ def _analyze_function(document, params):
referenced_strings.sort(key=lambda item: (int(item["address"], 16), int(item["source_address"], 16)))
referenced_names.sort(key=lambda item: (int(item["address"], 16), int(item["source_address"], 16)))
return {
"procedure": {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure), "classification": None, "signature": procedure.signatureString(), "locals": _procedure_locals(procedure)},
"procedure": {
**_procedure_identity(procedure),
"signature": procedure.signatureString(),
"locals": _procedure_locals(procedure),
},
"pseudocode": pseudo,
"assembly": assembly_lines,
"comments": comments,
@@ -618,9 +641,9 @@ def _dispatch(method, params):
if method == "read_bytes":
length = params.get("length", 256)
if isinstance(length, bool) or not isinstance(length, int):
raise ValueError("Byte-read length must be an integer")
raise InvalidRequestError("Byte-read length must be an integer")
if length < 1:
raise ValueError("Byte-read length must be at least 1")
raise InvalidRequestError("Byte-read length must be at least 1")
reader = getattr(document, "readBytes", None)
if not callable(reader):
raise CapabilityUnavailableError(
@@ -654,7 +677,7 @@ def _dispatch(method, params):
or offset in BAD_ADDRESSES
or offset < 0
):
raise ValueError("Address has no authoritative file-offset mapping")
raise InvalidRequestError("Address has no authoritative file-offset mapping")
return {"address": _hex(address), "file_offset": offset}
if method == "resolve_containing_procedure":
address = _address(document, params.get("address"))
@@ -690,7 +713,7 @@ def _dispatch(method, params):
else:
result = document.getInstructionStart(max(0, target - 1))
if result in BAD_ADDRESSES:
raise ValueError("No adjacent address")
raise InvalidRequestError("No adjacent address")
return _hex(result)
if method == "list_segments":
result = []
@@ -729,14 +752,14 @@ def _dispatch(method, params):
if requested is not None:
key = _hex(_address(document, requested))
values = {key: values[key]} if key in values else {}
return [{"address": _hex(address), "value": value} for address, value in sorted(values.items())]
return [{"address": address, "value": value} for address, value in values.items()]
if method == "list_names":
result = dict(_search_inventory(document, "name"))
requested = params.get("address")
if requested is not None:
key = _hex(_address(document, requested))
result = {key: result[key]} if key in result else {}
return [{"address": _hex(address), "value": value} for address, value in sorted(result.items(), key=lambda item: int(item[0]))]
return [{"address": address, "value": value} for address, value in result.items()]
if method in ("search_procedures", "search_strings"):
kind = "procedure" if method == "search_procedures" else "string"
return _search_results(document, kind, params)
@@ -755,7 +778,16 @@ def _dispatch(method, params):
if method == "procedure_info":
blocks = list(procedure.basicBlockIterator())
length = sum(max(0, block.getEndingAddress() - block.getStartingAddress()) for block in blocks)
return {"name": _procedure_name(procedure), "entrypoint": _hex(procedure.getEntryPoint()), "basicblock_count": procedure.getBasicBlockCount(), "length": length, "signature": procedure.signatureString(), "locals": _procedure_locals(procedure), "classification": None}
return {
"name": _procedure_name(procedure),
"entrypoint": _hex(procedure.getEntryPoint()),
"basicblock_count": procedure.getBasicBlockCount(),
"length": length,
"signature": procedure.signatureString(),
"locals": _procedure_locals(procedure),
"classification": None,
"body": _procedure_identity(procedure)["body"],
}
if method == "set_address_name":
address = _address(document, params.get("address"))
try:
@@ -789,7 +821,7 @@ def _dispatch(method, params):
address = _address(document, params.get("address"))
document.removeBookmarkAtAddress(address)
return not document.hasBookmarkAtAddress(address)
raise ValueError("Unknown bridge method")
raise InvalidRequestError("Unknown bridge method")
def _serve_connection(connection):
@@ -803,13 +835,24 @@ def _serve_connection(connection):
authenticated = False
should_stop = False
try:
request = json.loads(line.decode("utf-8"))
if set(request) != {"id", "token", "method", "params"}:
raise ValueError("Invalid bridge request shape")
try:
request = json.loads(line.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as error:
raise InvalidRequestError("Invalid bridge request JSON") from error
if not isinstance(request, dict) or set(request) != {"id", "token", "method", "params"}:
raise InvalidRequestError("Invalid bridge request shape")
request_id = request["id"]
if isinstance(request_id, bool) or not isinstance(request_id, int) or request_id < 0:
raise ValueError("Invalid bridge request id")
if not isinstance(request["token"], str) or not hmac.compare_digest(request["token"], REA_TOKEN):
raise InvalidRequestError("Invalid bridge request id")
if (
not isinstance(request["method"], str) or not request["method"]
or not isinstance(request["params"], dict)
):
raise InvalidRequestError("Invalid bridge method or parameters")
if (
not isinstance(request["token"], str) or not request["token"].isascii()
or not hmac.compare_digest(request["token"], REA_TOKEN)
):
raise PermissionError("Invalid bridge capability")
authenticated = True
_write_message(file, {"id": request_id, "event": {
@@ -864,7 +907,7 @@ def _diagnostic_type(error):
return "capability_unavailable"
if isinstance(error, PermissionError):
return "authorization"
if isinstance(error, (ValueError, TypeError, KeyError)):
if isinstance(error, InvalidRequestError):
return "invalid_request"
return "bridge_exception"
+157
View File
@@ -0,0 +1,157 @@
import AppKit
import ApplicationServices
import CryptoKit
import ScreenCaptureKit
import Darwin
struct Request: Decodable {
var pid: Int32
var window_id: UInt32
var executable: String
var sha256: String
var launch_time: Double?
var screenshot: Bool
var accessibility: Bool
var max_nodes: Int
var action: Action?
}
struct Action: Decodable { var kind: String; var path: [Int]?; var direction: String?; var text: String? }
struct BoundaryFailure: Error { var code: String; var message: String }
func fail(_ code: String, _ message: String) throws -> Never { throw BoundaryFailure(code: code, message: message) }
func attribute(_ element: AXUIElement, _ key: String) -> CFTypeRef? {
var value: CFTypeRef?
return AXUIElementCopyAttributeValue(element, key as CFString, &value) == .success ? value : nil
}
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 text(_ element: AXUIElement, _ key: String) -> Any { (attribute(element, key) as? String) ?? NSNull() as Any }
func windowBounds(_ element: AXUIElement) -> CGRect? {
guard let p = attribute(element, kAXPositionAttribute), let s = attribute(element, kAXSizeAttribute),
CFGetTypeID(p) == AXValueGetTypeID(), CFGetTypeID(s) == AXValueGetTypeID() else { return nil }
var point = CGPoint.zero; var size = CGSize.zero
guard AXValueGetValue(p as! AXValue, .cgPoint, &point), AXValueGetValue(s as! AXValue, .cgSize, &size) else { return nil }
return CGRect(origin: point, size: size)
}
func observe(_ request: Request) async throws -> [String: Any] {
guard let app = NSRunningApplication(processIdentifier: request.pid), let url = app.executableURL else {
try fail("target-mismatch", "Selected PID does not expose an application executable URL")
}
guard let canonicalPointer = realpath(url.path, nil) else { try fail("target-mismatch", "Running executable path cannot be resolved") }
defer { free(canonicalPointer) }
let canonicalExecutable = String(cString: canonicalPointer)
guard canonicalExecutable == request.executable else {
try fail("target-mismatch", "Selected PID executable \(canonicalExecutable) differs from active target \(request.executable)")
}
guard let launch = app.launchDate?.timeIntervalSince1970 else {
try fail("process-identity-unavailable", "Selected application does not expose a launch time; stable PID reuse checks are unavailable")
}
if let expected = request.launch_time, expected != launch { try fail("process-replaced", "Selected process was replaced") }
let executableSize = (try FileManager.default.attributesOfItem(atPath: url.path)[.size] as? NSNumber)?.int64Value ?? -1
guard executableSize >= 0 && executableSize <= 256 * 1024 * 1024 else { try fail("target-limit", "Running executable exceeds the 256 MiB identity-check budget") }
let executableBytes = try Data(contentsOf: url, options: .mappedIfSafe)
guard SHA256.hash(data: executableBytes).map({ String(format: "%02x", $0) }).joined() == request.sha256 else {
try fail("target-mismatch", "Running executable bytes differ from the active target digest")
}
guard let windows = CGWindowListCopyWindowInfo([.optionIncludingWindow], request.window_id) as? [[String: Any]],
windows.count == 1, let window = windows.first,
(window[kCGWindowOwnerPID as String] as? NSNumber)?.int32Value == request.pid,
(window[kCGWindowLayer as String] as? NSNumber)?.intValue == 0,
let boundsDictionary = window[kCGWindowBounds as String] as? [String: Any],
let bounds = CGRect(dictionaryRepresentation: boundsDictionary as CFDictionary) else {
try fail("window-mismatch", "Selected window is absent or belongs to another process")
}
if request.screenshot && !CGPreflightScreenCaptureAccess() {
try fail("screen-recording-denied", "Grant Screen Recording to the REA host in System Settings > Privacy & Security, then retry; no broad capture fallback is used")
}
var selected: AXUIElement?
if request.accessibility || request.action != nil {
guard AXIsProcessTrusted() else { try fail("accessibility-denied", "Grant Accessibility to the REA host in System Settings > Privacy & Security, then retry") }
let application = AXUIElementCreateApplication(request.pid)
AXUIElementSetMessagingTimeout(application, 2)
guard let axWindows = attribute(application, kAXWindowsAttribute) as? [AXUIElement] else {
try fail("accessibility-unavailable", "Selected application does not expose accessibility windows")
}
let matches = axWindows.filter { windowBounds($0) == bounds }
guard matches.count == 1 else { try fail("ambiguous-window", "Accessibility window geometry does not identify exactly one selected window; selected CG bounds \(bounds), AX candidates \(axWindows.map { String(describing: windowBounds($0)) })") }
selected = matches[0]
}
if let action = request.action, let root = selected {
var element = root
for index in action.path ?? [] {
var selectedChild: CFArray?
guard index >= 0 && index < childCount(element),
AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, index, 1, &selectedChild) == .success,
let item = (selectedChild as? [AXUIElement])?.first else { try fail("element-missing", "Selected accessibility path no longer exists") }
element = item
}
var owner: pid_t = 0
guard AXUIElementGetPid(element, &owner) == .success && owner == request.pid else { try fail("element-owner-mismatch", "Selected element belongs to another process") }
let outcome: AXError
switch action.kind {
case "click": outcome = AXUIElementPerformAction(element, kAXPressAction as CFString)
case "scroll": outcome = AXUIElementPerformAction(element, (action.direction == "increment" ? kAXIncrementAction : kAXDecrementAction) as CFString)
case "key-entry": outcome = AXUIElementSetAttributeValue(element, kAXValueAttribute as CFString, (action.text ?? "") as CFString)
default: try fail("unsupported-action", "Only selected-element press, increment/decrement and text-value entry are admitted")
}
guard outcome == .success else { try fail("action-failed", "Accessibility action failed with AXError \(outcome.rawValue)") }
}
var nodes: [[String: Any]] = []; var truncated = false
if request.accessibility, let root = selected {
var pending: [(AXUIElement, [Int])] = [(root, [])]
while let (element, path) = pending.popLast() {
if nodes.count >= request.max_nodes { truncated = true; break }
let totalChildren = childCount(element)
var actions: CFArray?
AXUIElementCopyActionNames(element, &actions)
nodes.append(["path": path, "role": text(element, kAXRoleAttribute), "title": text(element, kAXTitleAttribute), "value": text(element, kAXValueAttribute), "actions": actions as? [String] ?? [], "children_count": totalChildren])
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)
if totalChildren > count { truncated = true }
for index in (0..<count).reversed() { pending.append((items[index], path + [index])) }
}
}
var screenshot: Any = NSNull()
if request.screenshot {
guard #available(macOS 14, *) else { try fail("unsupported-host", "Selected-window screenshots require macOS 14 or later") }
let content = try await SCShareableContent.excludingDesktopWindows(true, onScreenWindowsOnly: false)
guard let scWindow = content.windows.first(where: { $0.windowID == request.window_id && $0.owningApplication?.processID == request.pid }) else {
try fail("capture-window-missing", "Selected window disappeared before capture")
}
guard bounds.width > 0 && bounds.height > 0 && bounds.width <= 100000 && bounds.height <= 100000 else { try fail("window-bounds", "Selected window has invalid or oversized geometry") }
let filter = SCContentFilter(desktopIndependentWindow: scWindow)
let configuration = SCStreamConfiguration()
let scale = min(1, 2048 / max(bounds.width, bounds.height))
configuration.width = max(1, Int(bounds.width * scale)); configuration.height = max(1, Int(bounds.height * scale))
configuration.showsCursor = false
let image = try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: configuration)
guard let png = NSBitmapImageRep(cgImage: image).representation(using: .png, properties: [:]) else {
try fail("capture-failed", "Selected-window PNG encoding failed")
}
screenshot = ["mime_type": "image/png", "base64": png.base64EncodedString(), "sha256": SHA256.hash(data: png).map({ String(format: "%02x", $0) }).joined(), "width": image.width, "height": image.height]
}
return ["window": ["pid": request.pid, "window_id": request.window_id, "executable": request.executable, "launch_time": launch, "title": window[kCGWindowName as String] as? String ?? ""], "nodes": nodes, "truncated": truncated, "screenshot": screenshot, "gaps": request.accessibility ? [] : ["Accessibility capture was disabled"]]
}
Task { @MainActor in
do {
let request = try JSONDecoder().decode(Request.self, from: Data(CommandLine.arguments[1].utf8))
let result = try await observe(request)
let output = try JSONSerialization.data(withJSONObject: ["ok": true, "result": result], options: [.sortedKeys])
FileHandle.standardOutput.write(output)
} catch {
let failure = error as? BoundaryFailure
let output: [String: Any] = ["ok": false, "code": failure?.code ?? "capture-failed", "message": failure?.message ?? error.localizedDescription]
if let data = try? JSONSerialization.data(withJSONObject: output, options: [.sortedKeys]) { FileHandle.standardOutput.write(data) }
}
exit(0)
}
RunLoop.main.run()
@@ -5,9 +5,12 @@
- Implementation status: The provider registry, deterministic selection,
target binding, analysis-profile commitment, and snapshot/Evidence migration
are implemented. Ghidra discovery, target/profile resolution, doctor checks,
the private headless-session foundation, ten read-only inventory capabilities,
and nine function-analysis capabilities are implemented with real
cross-format conformance.
the private headless-session foundation, thirteen read-only inventory/load-image
capabilities, and twelve function-analysis capabilities are implemented with
real cross-format conformance. Linux/macOS also supports atomic session
function annotations; experimental Windows x64 P0 uses bundled native controls
and admits only the 25 read-only operations. See the
[release boundary](../installation.md#released-package-and-main).
## Context
@@ -25,7 +28,7 @@ engine, engine version, or complete analysis configuration. Two analyses of the
same bytes could therefore disagree because of language, compiler, loader,
analyzer, or provider-version differences while appearing target-compatible.
These constraints are embodied in the current
The decision addressed the earlier representations of
[`CompositeProvider`](../../src/application/CompositeProvider.ts),
[runtime composition](../../src/application/runtime.ts),
[`BinarySession`](../../src/application/BinarySession.ts),
@@ -569,5 +572,7 @@ The first six implementation stages are shipped:
Future stages may deepen format and indirect-flow coverage, but must continue
to compare normalized semantics rather than provider-specific pseudocode or
assembly text. Ghidra continues to omit GUI and mutation operations, so
unsupported requests cannot route to an unverified implementation.
assembly text. Linux and macOS now admit atomic function-name and entry-comment
edits in the ephemeral database, with readback, rollback, and cache invalidation.
Executable bytes remain unchanged. Windows P0 retains its read-only boundary;
GUI and other unimplemented mutations remain unavailable.
@@ -1,10 +1,14 @@
# ADR-0002: Controlled JavaScript replay authority and sandbox policy
- Status: Accepted
- Status: Superseded; the controlled JavaScript replay tool was removed
- Date: 2026-07-16
- Implementation status: Implemented by `run_controlled_replay` with a
Linux-x86_64 Bubblewrap, seccomp, and delegated systemd user-cgroup backend.
Hosts that cannot establish every required boundary fail closed.
- Implementation status: The implementation was removed. The design below is
retained as historical context and does not describe a supported REA tool.
> Historical design note: The tool, sandbox backend, and plan/execute workflow
> described here are no longer part of REA. For runtime behavior, run probes
> directly against the application and use the available browser, Electron, or
> process capture workflows.
## Context
@@ -1,13 +1,17 @@
# ADR-0003: Managed-code evidence and provider boundary
- Status: Accepted
> Historical design record: The non-executing runtime-correlation planning
> tool described in earlier revisions was removed. This ADR's current
> implementation status below covers only the static managed tools that ship.
- Status: Superseded for runtime planning; static evidence boundary remains in force
- Date: 2026-07-16
- Implementation status: Read-only PE/CLI triage and exact identity are shipped
through `inspect_managed_artifact` / `rea inspect-managed-artifact`. Bounded
metadata, signatures, method bodies, raw CIL hashes, limited
decoded-instruction-tuple v1 hashes, exception regions, call edges, and
field-access anchors are shipped through `inspect_managed_members`; `rea
inspect-managed-members` provides CLI parity. Static member comparison and build-local
inspect-managed-members` provides CLI parity. Static member comparison and build-local
token remapping are shipped through `compare_managed_members` /
`rea compare-managed-members`. Declaration-only managed/native boundary
inventory for ModuleRef, ImplMap/PInvoke, ReadyToRun indicators, and non-IL
@@ -19,11 +23,9 @@ inspect-managed-members` provides CLI parity. Static member comparison and build
addresses. Decompiler reconstruction import is
shipped through `import_managed_reconstruction` /
`rea import-managed-reconstruction`; it records user-supplied C#/IL/pseudocode
as analyst inference only after exact static member locks match. Default-disabled
runtime-correlation admission planning is shipped through
`plan_managed_runtime_correlation` / `rea plan-managed-runtime-correlation`;
it does not attach, load, debug, reflect, instrument, invoke, or execute
target code. Managed static graph projection is shipped through
as analyst inference only after exact static member locks match. Managed
inspection does not execute assemblies or observe CLR internals. Managed static graph
projection is shipped through
`project_managed_application_graph` /
`rea project-managed-application-graph`; it projects authenticated managed
Evidence into the application graph without changing managed tokens into
@@ -50,7 +52,7 @@ makes names especially weak while tokens remain coordinates in only one build.
REA already separates one selected deep native provider from disjoint auxiliary
providers under [ADR-0001](0001-provider-selection-and-analysis-profiles.md).
It also separates static evidence, passive observation, process capture, and
controlled replay authorities. Managed analysis must fit those boundaries
interactive observation authorities. Managed analysis must fit those boundaries
without requiring a CLR to inspect an artifact, loading target code into the
REA process, or treating a managed token as a native address.
@@ -257,10 +259,13 @@ exception/local commitments, and an explicit algorithm version require a new
normalized-CIL schema.
Metadata tokens are reported because they are precise coordinates inside one
module. Cross-version identity never consists of a token alone. A structural
match cites both build commitments, both local tokens, the v1 tuple inputs, and
all competing candidates. V1 cannot cite a
separate matching-algorithm version because none is exposed.
module. Cross-version identity never consists of a token alone. Matching tries
exact CIL/signature identity, then an exact declared-type/name/signature tuple,
then structural shape; a name by itself never selects a pair. Partial or
malformed bodies leave body facets unknown after a signature tuple pairs the
methods. Matches cite both build commitments, both local tokens, and all
competing candidates. V1 cannot cite a separate matching-algorithm version
because none is exposed.
### 5. Separate five evidence layers
@@ -356,24 +361,13 @@ emit full IL, decompiled source, runtime logs, user data, credentials, or
service/account material. No proprietary binary, hash-locked application
manifest, or derived dump is committed to REA.
### 8. Keep runtime correlation a different, default-disabled authority
### 8. Keep runtime behavior separate from static managed analysis
Static managed support grants no permission to attach, load, debug, reflect,
instrument, invoke, or execute. The shipped runtime-correlation planning
capability distinguishes those effects, remains disabled by default, requires
explicit administrator and per-call approval, and binds to the exact artifact
SHA-256, MVID, method signature, and CIL/body shape observed by the static
path. It records an admission plan only; a future executor must pass a separate
threat model before any effect can occur.
Runtime admission must additionally validate host OS, CLR family, architecture,
and supported build/tool versions; bound time, threads, outputs, UI and network
effects; avoid real services and accounts; and own all created processes and
artifacts through complete cleanup. Reflection-only loading is still a CLR load
and is not a substitute for the static byte parser.
Runtime execution is intentionally outside the current managed-code track and
requires its own threat model and admission decision.
No managed runtime planning or execution tool ships. Managed inspection does
not execute assemblies or establish which methods ran. Where supported, direct
process capture can record target-level inputs and outputs, but it does not
prove CLR method execution or resolution. Internal managed behavior remains
unknown until independently observed with suitable runtime evidence.
## Public contract consequences
@@ -440,9 +434,7 @@ or development-only oracles.
5. Add source-built, pinned real-tool, package, CLI, and MCP conformance.
Source-owned conformance is shipped; pinned external real-tool checks remain
planned.
6. Add separately authorized runtime-correlation admission planning. Shipped;
runtime execution remains planned.
7. Project authenticated managed static Evidence into the application graph.
6. Project authenticated managed static Evidence into the application graph.
Shipped.
Every gate must keep unsupported ReadyToRun, NativeAOT, IL2CPP, mixed-mode, or
+5 -5
View File
@@ -4,8 +4,8 @@ Accepted decisions describe the architecture that implementation PRs must
follow. Acceptance does not by itself mean the behavior is shipped; each record
states its implementation status separately.
| ADR | Status | Implementation |
| ------------------------------------------------------------------------------------------------------------------ | -------- | ----------------------------------------------- |
| [0001: Provider selection and analysis profiles](0001-provider-selection-and-analysis-profiles.md) | Accepted | Foundation shipped; capability work continues |
| [0002: Controlled JavaScript replay authority and sandbox policy](0002-controlled-replay-authority-and-sandbox.md) | Accepted | Linux x86_64 extracted-module replay shipped |
| [0003: Managed-code evidence and provider boundary](0003-managed-code-evidence-and-provider-boundary.md) | Accepted | PE/CLI triage shipped; deeper contracts planned |
| ADR | Status | Implementation |
| ------------------------------------------------------------------------------------------------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| [0001: Provider selection and analysis profiles](0001-provider-selection-and-analysis-profiles.md) | Accepted | Provider selection and 25 read-only Ghidra operations on Linux/macOS and experimental Windows x64 P0; Linux/macOS session annotations |
| [0002: Controlled JavaScript replay authority and sandbox policy](0002-controlled-replay-authority-and-sandbox.md) | Superseded | Historical design; the controlled JavaScript replay tool was removed |
| [0003: Managed-code evidence and provider boundary](0003-managed-code-evidence-and-provider-boundary.md) | Partially superseded | Seven static managed tools remain; runtime planning removed |
+130
View File
@@ -0,0 +1,130 @@
# Static Android APK analysis
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
application workflow and return artifact-bound Evidence inline.
This family is present on repository main and in npm 4.1.0. Check the
[released package boundary](installation.md#released-package-and-main) and the
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,
an Android SDK, JADX, a device service or an emulator. Set the path in the shell
or MCP server environment:
```sh
export REA_JADX_MCP_JAR=/absolute/path/jadx-headless-mcp-0.7.1-all.jar
# Optional: select an existing JDK rather than java on PATH.
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.
Windows is unsupported for this provider's owned stdio process boundary.
## Answer an analyst question
```sh
rea inspect-android-package /path/Example.apk
rea search-android-classes /path/Example.apk MainActivity
rea inspect-android-class /path/Example.apk example.MainActivity
rea inspect-android-method /path/Example.apk example.MainActivity onCreate
rea inspect-android-method /path/Example.apk example.MainActivity select --overload-index 1
rea trace-android-references /path/Example.apk example.MainActivity
rea trace-android-references /path/Example.apk example.MainActivity --method-name onCreate
```
Equivalent MCP methods are `inspect_android_package`, `search_android_classes`,
`inspect_android_class`, `inspect_android_method` and `trace_android_references`.
Every request includes `path`. Class/method operations use `class_name`,
`method_name` and optional `overload_index` as applicable. For example:
```json
{
"name": "inspect_android_method",
"arguments": {
"path": "/path/Example.apk",
"class_name": "example.MainActivity",
"method_name": "select",
"overload_index": 1
}
}
```
Use `inspect_android_class` to choose an overload. REA rejects an ambiguous
method request rather than silently selecting the first match. The engine's
method reference operation cannot select overloads, so REA reports that boundary
as unsupported when multiple same-name methods exist.
The engine's smali fallback also joins same-name overloads; REA rejects that
fallback for an overloaded method instead of attributing all bodies to one
selected overload. Native/abstract methods with no decompiled body report
`body_status: "not_available"` without inferring which implementation flag caused it.
## Interpret the result
- Package inspection retains decoded manifest XML, summary declarations,
permissions and class/resource counts. Summary fields are extracted by the
upstream engine and should be checked against the retained XML when needed.
Signature verification is explicitly `not_performed`.
- Class search 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.
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
returned text. Decompiled source is a derived representation. A complete text
result does not establish complete semantics or a byte-identical reconstruction.
- Reference results are incoming static relationships between provider nodes.
Source lines and DEX instruction offsets remain unknown. They are not observed
runtime calls.
- Original requests, APK SHA-256, actual engine JAR SHA-256 and raw producer
responses remain in Evidence. Source revision is populated only for bytes
matching the audited release JAR.
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.
## 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 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
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
as complete. These are memory-safety boundaries, not undocumented result caps.
## Real verification
Download the explicit public fixtures once; this installs no tools:
```sh
npm run fixtures:android
export REA_JADX_MCP_JAR="$PWD/_reference/apk-integration/jadx-headless-mcp-0.7.1-all.jar"
export REA_ANDROID_TEST_APK="$PWD/_reference/apk-integration/ApiDemos-debug.apk"
npm run verify:android
```
The download script verifies fixed SHA-256 values, reuses matching files and
refuses to overwrite a different existing file. APK/JAR files live under ignored
`_reference/` and are excluded from package contents. The lane exercises actual
REA CLI and MCP results with the real engine, separately from synthetic protocol
regressions. It needs no Gradle build, emulator, Android SDK or Ghidra.
+41 -10
View File
@@ -1,7 +1,7 @@
graph TD
subgraph "Entry Points"
CLI["src/cli.ts<br/>One-shot CLI adapter<br/>setup, diagnostics, analysis, decompilation"]
MAIN["src/main.ts<br/>MCP stdio server adapter<br/>config, session, process lifetime"]
CLI["src/cli.ts + src/cli/<br/>One-shot CLI adapter<br/>setup, diagnostics, analysis, decompilation"]
MAIN["src/main.ts + src/main/<br/>MCP stdio server adapter<br/>config, session, process lifetime"]
DISPATCH["scripts/rea.mjs<br/>Executable dispatcher<br/>routes mcp / one-shot"]
end
@@ -9,30 +9,38 @@ graph TD
SERVER["src/server/<br/>MCP request translation<br/>createServer, tool registration, toolResult"]
HOPPER_BRIDGE["bridge/hopper_bridge.py<br/>Runs inside Hopper<br/>adapts operations to Hopper Python API"]
GHIDRA_BRIDGE["bridge/ghidra/ReaGhidraBridge.java<br/>Packaged HeadlessScript<br/>inventory, decompiler, references, CFG"]
INSTALL["Host installation adapters<br/>macOS: verified vendor DMG to ~/Applications<br/>Linux: verified vendor package + apt/dnf/pacman"]
INSTALL["src/cli/ + src/application/Setup*<br/>Host installation adapters<br/>macOS: verified vendor DMG to ~/Applications<br/>Linux: verified vendor package + apt/dnf/pacman"]
end
subgraph "Application Layer"
APP["src/application/<br/>Shared CLI/MCP workflows<br/>enhanced analysis and investigations"]
SESSION["SessionProviderRouter + BinarySession<br/>one immutable deep binding per target<br/>disjoint auxiliary routing"]
REGISTRY["AnalysisProviderRegistry<br/>sorted deep candidates<br/>deterministic selection, no fallback"]
JS_RECON["JavaScript artifact reconstruction<br/>directory/ASAR reads<br/>AST-only bundle projection"]
JS_RECON["src/application/ + src/artifacts/<br/>JavaScript artifact reconstruction<br/>directory/ASAR reads<br/>AST-only bundle projection"]
end
subgraph "Provider Layers"
HOPPER["src/hopper/<br/>Hopper launch & Unix-socket protocol<br/>BridgeLauncher, HopperClient, protocol"]
GHIDRA["src/ghidra/<br/>Ghidra discovery, profile & function analysis<br/>snapshot, Unix/Windows transport, bounded lifecycle"]
IDA["src/ida/<br/>Upstream MCP contract adaptation<br/>live GUI binding or owned headless database"]
IDA_MCP["External ida-pro-mcp<br/>GUI proxy or database supervisor<br/>upstream installation and engine lifecycle"]
AUX["src/artifacts/ + src/native/<br/>disjoint artifact and native capabilities"]
BROWSER["src/browser/<br/>Loopback CDP transport<br/>exact-origin passive observation"]
REPLAY["src/replay/<br/>Disposable JavaScript worker<br/>Bubblewrap, seccomp, cgroup limits"]
BROWSER["src/browser/<br/>CDP / Playwright capture<br/>captured script adapters and publication"]
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"]
REFERENCE["src/reference/<br/>Filesystem reference-source reader<br/>bounded traversal, validation, errors"]
end
subgraph "Provider Process Foundation"
PROCESS["src/process/<br/>owned process groups, private roots<br/>deadlines, bounded file reads and diagnostics, cleanup"]
WINDOWS["src/windows/<br/>Windows host boundary adapters<br/>package loader, opaque handles, owned jobs and private runtimes"]
WINDOWS_NATIVE["native/windows/src/<br/>Node-API implementation<br/>NTFS admission, protected DACLs and atomic Job Object assignment"]
end
subgraph "Contracts Layer"
CONTRACTS["src/contracts/<br/>Caller-visible schemas for analysis, managed, browser, Electron, application, and session tools"]
ERRORS["src/contracts/errorSchemas.ts<br/>Shared analysis error schemas<br/>CLI diagnostics, generated error catalog"]
end
subgraph "Domain Layer"
@@ -40,48 +48,71 @@ graph TD
end
subgraph "Configuration"
CONFIG["src/config.ts<br/>Zod-validated env parsing<br/>provider settings, origins, and approved roots"]
CONFIG["src/config.ts + src/config/<br/>Zod-validated env parsing<br/>provider settings and origins"]
end
subgraph "Tests"
TESTS["tests/<br/>Vitest suite<br/>fixtures/ holds fake launcher, bridge, and CDP seams"]
end
subgraph "Evaluation"
EVALUATION["src/evaluation/<br/>Agent release evaluation<br/>session transcript scoring"]
end
DISPATCH --> CLI
DISPATCH --> MAIN
MAIN --> CONFIG
MAIN --> SERVER
MAIN --> APP
CLI --> APP
APP --> FIRMWARE
FIRMWARE --> PROCESS
FIRMWARE --> CONTRACTS
APP --> ANDROID
ANDROID --> PROCESS
ANDROID --> CONTRACTS
ANDROID --> DOMAIN
CLI --> ERRORS
APP --> SESSION
SESSION --> REGISTRY
REGISTRY --> HOPPER
REGISTRY --> GHIDRA
REGISTRY --> IDA
IDA --> IDA_MCP
IDA --> CONTRACTS
IDA --> DOMAIN
SESSION --> AUX
APP --> JS_RECON
APP --> INSTALL
SERVER --> CONTRACTS
SERVER --> APP
APP --> BROWSER
APP --> REPLAY
APP --> DOTNET
DOTNET --> DOMAIN
HOPPER --> HOPPER_BRIDGE
GHIDRA --> GHIDRA_BRIDGE
HOPPER --> PROCESS
GHIDRA --> PROCESS
GHIDRA --> WINDOWS
PROCESS --> WINDOWS
WINDOWS --> WINDOWS_NATIVE
APP --> PROCESS
APP --> DOMAIN
JS_RECON --> AUX
JS_RECON --> DOMAIN
APP --> REFERENCE
REFERENCE --> DOMAIN
CONTRACTS --> DOMAIN
HOPPER --> DOMAIN
GHIDRA --> DOMAIN
AUX --> DOMAIN
BROWSER --> DOMAIN
REPLAY --> DOMAIN
PROCESS --> DOMAIN
TESTS -.-> MAIN
TESTS -.-> HOPPER
TESTS -.-> GHIDRA
TESTS -.-> IDA
TESTS -.-> PROCESS
TESTS -.-> BROWSER
TESTS -.-> REPLAY
TESTS -.-> EVALUATION
TESTS -.-> ERRORS
+4
View File
@@ -0,0 +1,4 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Discord</title>
<path fill="#5865F2" d="M20.317 4.3698a19.7913 19.7913 0 0 0-4.8851-1.5152.0741.0741 0 0 0-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 0 0-.0785-.037 19.7363 19.7363 0 0 0-4.8852 1.515.0699.0699 0 0 0-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 0 0 .0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 0 0 .0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 0 0-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 0 1-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 0 1 .0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 0 1 .0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 0 1-.0066.1276 12.2986 12.2986 0 0 1-1.873.8914.0766.0766 0 0 0-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 0 0 .0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 0 0 .0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 0 0-.0312-.0286ZM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189Zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+36 -46
View File
@@ -1,23 +1,27 @@
# Website observation with CDP
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.
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.
## Shipped surfaces
- `list_browser_targets` / `rea list-browser-targets` discovers page targets whose current URL matches an approved exact origin.
- `inspect_web_page` / `rea inspect-web-page` captures DOM structure, accessibility nodes, scripts, resources, safe response/DOM metadata, attach-window network and console metadata, WebSocket frame sizes, workers, quota, and optionally storage key names, redacted storage-content fingerprints, or script sources.
- `analyze_web_bundle` / `rea analyze-web-bundle` parses approved script artifacts without execution and derives chunk edges, route and endpoint candidates, vendor fingerprints, static WebMCP declarations, and optionally approved source-map/original-source evidence.
- `list_browser_targets` / `rea list-browser-targets` discovers all eligible page targets at the endpoint by default, with an optional exact-origin filter.
- `inspect_web_page` / `rea inspect-web-page` captures DOM structure, accessibility nodes, scripts, resources, safe response/DOM metadata, attach-window network and console metadata, WebSocket frame sizes, workers, quota, and optionally selected storage key names, redacted storage-content fingerprints, or script sources.
- `analyze_web_bundle` / `rea analyze-web-bundle` parses selected script artifacts without execution and derives chunk edges, route and endpoint candidates, vendor fingerprints, static WebMCP declarations, and optionally source-map/original-source evidence.
- `observe_web_session` / `rea observe-web-session` arms an observation window of the requested duration for an external user action and records ordered reload, SPA navigation, redirect, failure, lifecycle, and target-termination metadata.
- `discover_webmcp_tools` / `rea discover-webmcp-tools` uses the experimental CDP WebMCP domain to return every in-scope registration and its complete structural input-schema summary inline. REA never exposes `WebMCP.invokeTool`; malformed protocol payloads fail explicitly instead of returning incomplete tool metadata.
- `discover_webmcp_tools` / `rea discover-webmcp-tools` uses the experimental CDP WebMCP domain to return every in-scope registration and its complete structural input-schema summary inline. Registrations with the same name and URL in distinct observed frames retain separate identities. `tool_key` includes the observed frame ID; it is not a stable identity across navigation or browser sessions. Same-frame registration updates replace that owner only, and removal leaves other owners intact. REA never exposes `WebMCP.invokeTool`; malformed protocol payloads fail explicitly instead of returning incomplete tool metadata.
- `compare_web_captures` / `rea compare-web-captures` compares passive captures or exact-step-aligned browser scenarios. Scenario results commit reproducible literal normalization and expose alignment failures plus artifact-level action, screenshot, DOM, accessibility, URL, history, storage, and event differences. Missing or truncated evidence is never treated as equivalence.
- `capture_web_screenshot` / `rea capture-web-screenshot` returns an explicitly approved, content-addressed visible-viewport PNG.
- `capture_web_screenshot` / `rea capture-web-screenshot` returns a content-addressed visible-viewport PNG.
- `compare_web_screenshots` / `rea compare-web-screenshots` performs bounded local PNG pixel comparison without OCR or external services.
- Every surface has equivalent CLI and MCP contracts and returns Evidence provenance.
- MCP tools return complete results inline and include the Evidence ID. Session evidence can be exported with `export_evidence_bundle`.
Target discovery returns the complete approved target array in one result.
Target discovery returns the complete in-scope target array in one result.
Electron `file://` pages use a separate provider, permission capability, and root model; see [electron-observation.md](electron-observation.md).
Electron `file://` pages use a separate provider and target boundary; see [electron-observation.md](electron-observation.md).
Existing static application Evidence and passive web/Electron captures can be
combined later through
[JavaScript static/runtime reconciliation](javascript-runtime-reconciliation.md).
@@ -35,35 +39,20 @@ google-chrome \
REA does not launch, own, or terminate this browser. Use a dedicated profile and stop it yourself when the investigation is complete. Do not expose the debugging port on a non-loopback interface.
## Configure authority
## Request boundary
Browser observation is fail-closed by default. Configure the administrator ceiling before starting REA:
```bash
export REA_BROWSER_OBSERVE_ENABLED=true
export REA_BROWSER_CDP_ENDPOINTS_JSON='["http://127.0.0.1:9222"]'
export REA_BROWSER_ALLOWED_ORIGINS_JSON='["http://127.0.0.1:3000"]'
```
`REA_BROWSER_CDP_ENDPOINTS_JSON` accepts at most 16 literal `http://127.0.0.1:PORT` or `http://[::1]:PORT` endpoints. `localhost`, private-LAN addresses, HTTPS endpoints, credentials, paths, queries, fragments, and implicit ports are rejected.
`REA_BROWSER_ALLOWED_ORIGINS_JSON` accepts at most 32 exact HTTP(S) origins. Paths, credentials, queries, fragments, and wildcards are rejected. An input must stay within both this administrator ceiling and any active project/session/once grant.
Configuration is reloadable through the existing `SIGHUP` permission-policy path. Each request is checked against the configured endpoint, origin, and permission grants.
Inspect the exact effective scope without contacting the browser:
```bash
rea policy explain browser_observe \
--origins http://127.0.0.1:9222 \
--origins http://127.0.0.1:3000 \
--network loopback \
--json
```
Discovery supplies a literal loopback CDP endpoint and returns all eligible
page targets unless an exact HTTP(S) origin filter narrows the result. Follow-up
inspection requests select one target; their default scope is its current origin. `localhost`, private-LAN
addresses, HTTPS CDP endpoints, credentials, paths, queries, fragments, and
implicit ports are not valid CDP endpoints. Origin filters contain only scheme,
host, and port; paths, credentials, queries, fragments, and wildcards are not
valid origins.
## CLI workflow
Configured origins are used by default. `--allowed-origins` can request a narrower set that still fits the configured ceiling.
Pass the endpoint and selected target for each call. An origin filter is
optional and narrows the target set.
```bash
rea list-browser-targets http://127.0.0.1:9222 --json
@@ -96,7 +85,7 @@ rea inspect-web-page http://127.0.0.1:9222 TARGET_ID \
--json
```
Accessibility text and script content may contain application secrets and become part of the returned Evidence. Credential-shaped console substrings are redacted. JSON and WebSocket captures retain complete paths, types, and counts, never values or examples. Storage values remain redacted even when key-name capture is requested. `--include-storage-fingerprints` requests SHA-256 identity/value fingerprints, which support comparison only when cookies, DOM storage, IndexedDB, and Cache Storage were all captured completely.
Accessibility text, script content, query values, and explicitly requested primitive console text may contain application secrets and become part of the returned Evidence. URL username/password credentials are removed; ordinary query values, fragments, and parameter order remain intact. JSON and WebSocket captures retain complete paths, types, and counts, never values or examples. Storage values remain redacted even when key-name capture is requested. `--include-storage-fingerprints` requests SHA-256 identity/value fingerprints, which support comparison only when cookies, DOM storage, IndexedDB, and Cache Storage were all captured completely.
Screenshot capture returns the complete visible viewport as an inline image artifact:
@@ -110,7 +99,6 @@ rea capture-web-screenshot http://127.0.0.1:9222 TARGET_ID \
```json
{
"cdp_endpoint": "http://127.0.0.1:9222",
"allowed_origins": ["http://127.0.0.1:3000"],
"target_id": "TARGET_ID_FROM_LIST_BROWSER_TARGETS"
}
```
@@ -121,25 +109,25 @@ Collection counts are not capped. Per-value validation, the selected observation
window, and CDP protocol limits still apply; any incomplete or truncated values
are reported in the result.
Call `list_browser_targets` first because target IDs are browser-instance-specific. REA rechecks the selected target's current type and origin immediately before attaching.
Call `list_browser_targets` first because target IDs are browser-instance-specific. REA rechecks the selected target's current type and origin immediately before attaching. Without an origin filter, discovery lists all eligible pages; an individual capture defaults to the selected page's current origin.
REA establishes an authorized main-frame boundary before enabling Runtime, Debugger, or Network observation, rechecks it before document capture, and validates it again afterward. If the main frame navigates during final capture, REA discards the mixed result and returns `target_changed`.
REA establishes a main-frame origin boundary before enabling Runtime, Debugger, or Network observation, rechecks it before document capture, and validates it again afterward. If the main frame navigates during final capture, REA discards the mixed result and returns `target_changed`.
## Data minimization
REA removes sensitive values before normalized event data is retained:
REA retains local observation data and removes only URL userinfo credentials, explicitly declared scenario secrets, and values from structured credential/storage fields:
- URLs retain a bounded path and at most 256 bounded query parameter names, replace every retained query value with `[REDACTED]`, and remove credentials and fragments.
- URL observations preserve the complete local URL, including query values, fragments, duplicate parameters, and their order. Only username/password userinfo is removed; malformed original URL strings remain available with `origin: null`.
- DOM snapshots retain node types, node names, value lengths, and attribute names, but not text or attribute values.
- Accessibility structure and roles are retained by default, while names and descriptions require `include_accessibility_text: true`.
- Network observations retain method, status, MIME type, size, type, initiator stack location, and redacted URLs. Headers are discarded after an allowlisted projection of length/encoding, structured CSP/Link/policy fields, and untrusted agent hints. Cookies and authorization headers are never retained.
- Network observations retain method, status, MIME type, size, type, initiator stack location, and complete URLs with userinfo credentials removed. Headers are discarded after an allowlisted projection of length/encoding, structured CSP/Link/policy fields, and untrusted agent hints. Cookies and authorization headers are never retained.
- Request/response bodies are not requested or parsed by default. When selected, only allowed-origin JSON media types are read, converted immediately to complete value-free property paths/types, and discarded. `Network.getResponseBody` is never sent unless JSON body shape capture is selected.
- Console observations with a stack source retain call type, argument types, timestamp, and redacted source location. When console text capture is selected, already-delivered primitive values are retained after credential redaction; objects, getters, and remote properties are never expanded.
- Console observations with a stack source retain call type, argument types, timestamp, and source location. When console text capture is selected, already-delivered primitive values are retained verbatim; objects, getters, and remote properties are never expanded.
- WebSocket observations retain direction, opcode, and payload byte length. When shape capture is selected, text frames are classified as text or complete value-free JSON shape; binary bytes, hashes, prefixes, and raw frames are never retained.
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` selection. Cache bodies above 64 KiB and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` selection. Cookie fingerprints cover cookies applicable to the current main-frame URL, including its path, rather than every cookie path on that origin. Other storage remains scoped to the origin. Cache bodies above 64 KiB and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
- Script metadata is included only when CDP supplies a URL on an allowed origin. Stable keys exclude transient CDP script IDs, and exact transient raw URLs are used only during script/resource reconciliation. URL-less scripts are excluded because their origin cannot be established. When CDP supplies an execution-context association, the accepted script retains its authorized frame ID for later attribution. Source content is omitted unless explicitly requested and is returned inline as a self-verifying artifact with its SHA-256 digest.
Cross-origin frames, resources, scripts, events, and workers are excluded unless their exact origins are also approved. Excluded target details are counted without being exposed.
Frames, resources, scripts, events, and workers outside the selected origin scope are excluded. An explicit `allowed_origins` list can include additional origins; excluded target details are counted without being exposed.
Retained workers include validated opener-target and parent-frame IDs when CDP
provides them; these relationships do not expand the exact-origin boundary or
prove static module ownership.
@@ -156,12 +144,14 @@ coverage. It reports excluded items and any values omitted or truncated by
validation; it does not drop records to meet an internal collection quota.
Disallowed-origin entries are filtered before retention.
CDP discovery, WebSocket connection, and each command have a 5-second timeout. Version discovery is capped at 64 KiB, target discovery at 2 MiB, each WebSocket message at 16 MiB, and correlated pending commands at 128. Target and navigation-event inventories have no separate count cap. These transport bounds protect protocol framing and request correlation; they do not silently trim returned findings. Malformed unsolicited protocol events poison the connection so later commands fail closed.
CDP discovery, WebSocket connection, and commands continue until the peer responds or the caller cancels the operation; REA has no internal byte, message-count, or pending-command quota for these exchanges. Without caller cancellation, a stalled peer can leave an operation waiting. On cancellation, REA closes only its own WebSocket, waiting up to one second before terminating it. Malformed unsolicited protocol events poison the connection so later commands fail closed.
Network and console coverage starts only after REA attaches and enables the relevant CDP domains. `prior_activity_available` is always `false`; absence from these arrays is not evidence that an event never occurred. Source maps are not fetched by inspection. Bundle analysis can fetch them only after separate approval, with exact-origin redirect checks and no credentials, cookies, or referrer. Every requested map, mapping, original source, AST node, and finding from parsed content is returned inline; failed maps remain explicitly unavailable and make bundle completeness partial.
Network and console coverage starts only after REA attaches and enables the relevant CDP domains. `prior_activity_available` is always `false`; absence from these arrays is not evidence that an event never occurred. Source maps are not fetched by inspection. Bundle analysis fetches them only when `fetch_source_maps` is selected, with exact-origin redirect checks and no credentials, cookies, or referrer. Every requested map, mapping, original source, AST node, and finding from parsed content is returned inline; failed maps remain explicitly unavailable and make bundle completeness partial. Original source artifacts with `.ts`, `.tsx`, `.mts`, or `.cts` suffixes carry `text/typescript`; other source names retain `text/javascript`.
## Non-goals and threat model
The browser provider deliberately does not expose generic CDP commands, `Runtime.evaluate`, WebMCP invocation, navigation, input, downloads, page closure, or browser closure. Screenshot capture is the sole pixel surface and requires separate literal approval. REA disables the domains it enabled, detaches the target session, and closes only its own WebSocket.
The browser provider deliberately does not expose generic CDP commands, `Runtime.evaluate`, WebMCP invocation, navigation, input, downloads, page closure, or browser closure. Screenshot capture is the sole pixel surface and returns an image only when requested. REA disables the domains it enabled, detaches the target session, and closes only its own WebSocket.
This feature is not a browser sandbox or network containment mechanism. The attached page and browser continue running with their existing privileges and may make external requests independently of REA. CDP gives deep access to the selected browser profile, so use a dedicated profile, approve only origins you intend to inspect, and treat other same-user processes as outside this boundary.
This feature is not a browser sandbox or network containment mechanism. The attached page and browser continue running with their existing privileges and may make external requests independently of REA. CDP gives deep access to the selected browser profile, so use a dedicated profile, include only origins you intend to inspect, and treat other same-user processes as outside this boundary.
Response metadata normalizes a `Referrer-Policy` fallback list to its last recognized nonempty policy, including mixed-case tokens. Unknown policy names are skipped; malformed token syntax and lists without a recognized policy remain `null`. This reports the response's declared policy, not the effective policy of a document or an inferred browser default.
+239 -37
View File
@@ -3,42 +3,158 @@
`capture_browser_scenario` accepts the provider-neutral
`browserScenarioSchema` through both MCP and the
`capture-browser-scenario INPUT_JSON` CLI command. The request is declarative:
it admits a fixed action vocabulary, exact HTTP(S) origins, deterministic
browser settings, explicit storage and request replay, and provider-owned
it admits a fixed action vocabulary, explicit HTTP(S) targets, deterministic
browser settings, explicit storage seeds, and provider-owned
liveness deadlines. `INPUT_JSON` may be inline JSON or a JSON file path.
The minimal request contains only browser launch/connect authority, `start_url`,
`allowed_origins`, and at least one `actions` entry. Environment settings,
empty storage, disabled request replay, the required redaction policy, and a
final sanitized URL capture are supplied by default. Add secret declarations,
storage seeds, replay routes, query redaction names, and additional artifact or
event capture only when the investigation needs them. Action, secret, storage,
and replay-route counts are not capped. Duration, action, and navigation
The minimal request contains only browser launch/connect selection, `start_url`,
and at least one `actions` entry. Launch `headless` defaults to `true` and can
be set to `false` where the host supports a visible browser. Navigations follow
the destinations named by the scenario and the page's ordinary browser requests;
there is no separate origin allowlist or request-interception layer. Environment
settings (including service workers blocked by default), empty storage, and a
final sanitized URL capture are supplied by default. Set
`environment.service_workers` to `allow` when the application requires them.
Add secret declarations, storage seeds, and additional artifact or event
capture only when needed. Action, secret, and storage counts are not capped.
Duration, action, and navigation
timeouts remain fixed provider-owned liveness limits; the request does not
accept a caller-controlled `limits` object.
## Minimal launch example
Save this as `browser-scenario.json`, replacing the executable path with an
already-installed Chrome-family browser and the URL with your running fixture:
```json
{
"browser": {
"mode": "launch",
"executable_path": "/absolute/path/to/chromium"
},
"start_url": { "url": "http://127.0.0.1:3000" },
"actions": [
{ "step_id": "settle", "action": "wait_for_timeout", "duration_ms": 100 }
]
}
```
```bash
rea capture-browser-scenario ./browser-scenario.json --json > browser-capture.json
```
The default capture includes the final URL. Select DOM, screenshots, or events
only when they help answer the question. REA launches and cleans up the browser
profile for this request; it does not start your fixture server or install a
browser. A schema-valid scenario does not prove that the executable or target
is available on the host.
## Interaction example: capture the visible result
The default final URL answers "where did the page end up?", not "what did the
page show?". Many interactions, such as a search button that renders results in
place, change the page without changing the URL. A capture with only the
default `url` artifact then reports that the click succeeded and the URL is
unchanged, which cannot tell you whether results appeared.
Request the artifacts that answer your question and wait for the state you want
to inspect. This scenario clicks a Search button, waits for the current search
results, and captures the final DOM, accessibility tree, and URL. It assumes the
local page clears `#results`'s `data-state` while loading and sets it to `ready`
after rendering the current response. Adapt the selectors and readiness marker
to your page; an already-visible container or previous results are insufficient.
```json
{
"browser": {
"mode": "launch",
"executable_path": "/absolute/path/to/chromium"
},
"start_url": { "url": "http://127.0.0.1:3000" },
"actions": [
{
"step_id": "search",
"action": "click",
"locator": { "kind": "css", "selector": "#search" },
"timeout_ms": 10000
},
{
"step_id": "results-ready",
"action": "wait_for",
"locator": { "kind": "css", "selector": "#results[data-state='ready']" },
"state": "visible",
"timeout_ms": 10000
}
],
"capture": { "at_end": ["dom", "accessibility", "url"] }
}
```
Save it as `browser-search.json` and run it the same way as the minimal
example:
```bash
rea capture-browser-scenario ./browser-search.json --json > browser-search-capture.json
```
Through MCP, pass the same object as the `capture_browser_scenario` arguments.
The final step's DOM and accessibility artifacts retain the page state at that
capture time inline. REA still launches and cleans up the browser profile.
The click's `timeout_ms` covers the click action. A non-navigating `fetch` or
delayed render can finish later, so use `wait_for` to observe a page-specific
ready condition before final capture. A page that updates synchronously can
omit that wait. REA does not infer application readiness from capture selection.
Choose captures by the question being asked:
- `dom` and `accessibility`: text or structure the page rendered, such as
search results, validation messages, or updated lists. The accessibility tree
is usually the more compact way to read visible text.
- `screenshot`: visual layout or styling, or content not exposed as text, such
as a canvas.
- `history` or `storage`: navigation history, or cookies and web storage the
interaction wrote. Request them only when that state is the question; they
are never captured implicitly.
- `after_each_step` instead of `at_end`: intermediate state between actions in
a multi-step scenario.
- `events` (`console`, `page-errors`, `network`, and so on): how the page
reached its result, such as the request a search issued or an error it
logged. Select only the event families you need.
Capture states are reported per artifact. An artifact you did not request is
`not_requested`, which means REA did not look, not that the page had nothing
to show. A step's `completeness` of `complete` covers only the sections that
were requested. A URL-only capture can be `complete` and still be unable to
answer a question about rendered content, so add the relevant artifact and run
the scenario again rather than inferring from its absence. Capture completeness
also does not assert that the page's background work has finished.
The browser boundary is part of the contract. Launch mode requires a
caller-selected absolute executable, a provider-owned temporary profile,
headless operation, and close-plus-delete cleanup. Connect mode accepts only an
explicit-port loopback CDP endpoint, declares the browser externally owned, and
permits disconnect-only cleanup. The Playwright driver preserves this
caller-selected executable and always uses a provider-owned temporary profile
that is closed and deleted during cleanup. Connect mode accepts only an
explicit-port loopback CDP endpoint. The Playwright driver preserves this
distinction: it closes and deletes provider-owned profiles, but only disconnects
from an external CDP browser. Real-browser verification checks both outcomes and
confirms that an attached external browser remains alive.
URLs contain a query-free HTTP(S) base plus ordered query entries. Every query,
form, storage, cookie, and replay value is either an explicitly public literal
or a reference to a declared environment-backed secret. Raw credentials in
URLs and literal credential headers are rejected. Secret query names must also
appear in the redaction policy, and the required credential headers cannot be
removed from that policy. Durable results must replace resolved secret values
HTTP(S) URLs may include ordinary query values and fragments directly. Use
ordered structured query entries when a value needs to reference a declared
environment-backed secret. Form, storage, and cookie values are either literal
strings or declared secret references. Raw URL userinfo credentials are rejected.
Captured credential-header
values are not retained, and declared secret values are redacted automatically.
A secret may be declared solely to
redact matching observed content; every secret reference in an action, URL,
storage value still needs a declaration. Ordinary query values
and fragments remain intact. Durable results replace resolved secret values
with their secret references.
Every start, navigation, storage, replay, and redirect origin must be listed in
`allowed_origins`. Unsupported action tags, unknown fields, duplicate step or
route IDs, undeclared or unused secrets, and provider-owned replay headers fail
validation. HTTP and WebSocket routing enforces the same declared scope. Exact
replay may abort unmatched requests or pass through only approved origins.
The exact `start_url`, navigation destinations, and storage-seed origins define
the inputs to a scenario; ordinary page requests are handled by the browser.
Storage cookies are scoped to the page URL at each capture. Unsupported action
tags, unknown fields, duplicate step IDs, and undeclared secret references fail
validation.
The result is Evidence with an initial state followed by one record per
declared action. Each step reports action status, elapsed time, sanitized URLs,
@@ -53,21 +169,107 @@ inline without application-defined size ceilings. Other missing sections
remain explicit and make the capture ineligible for equality claims.
Attach-mode captures also declare the unavoidable pre-attach event gap.
Browser automation is disabled by default. The administrator ceiling is
configured with:
Selected event families are observed on the root page and discovered descendant
popups, including nested popups. Popup lifecycle records are emitted only when
`popups` is selected. Launch-mode network observation starts at the owned
context before navigation, so popup document requests, redirects, and responses
are included once. Connected contexts can contain unrelated tabs: only network
events with a frame belonging to the root or a discovered popup are retained.
Initial popup requests without a frame cannot be attributed in shared contexts
and are omitted; attach coverage remains incomplete.
- `REA_BROWSER_SCENARIO_ENABLED`
- `REA_BROWSER_SCENARIO_EXECUTABLE_ROOTS_JSON`
- `REA_BROWSER_SCENARIO_CDP_ENDPOINTS_JSON`
- `REA_BROWSER_SCENARIO_ALLOWED_ORIGINS_JSON`
- `REA_BROWSER_SCENARIO_ALLOWED_ENV_JSON`
Playwright reports new popup Pages after their first navigation has committed.
It buffers early console messages and page errors, but the pinned API cannot
recover every earlier frame, worker, WebSocket, or download event. If one of
those families is selected and a popup is discovered, REA reports the family gap
in `limitations`, marks `events` missing, and disables equality eligibility.
Later events remain available; no initial navigation is synthesized from the
popup's current URL.
Unlike passive observation, browser automation is not granted automatically.
Use an explicit project/session grant, or opt into the administrator grant with
`REA_BROWSER_SCENARIO_AUTO_GRANT=true` for a trusted unattended environment.
Authorization commits the exact executable or CDP endpoint, origins, environment
variable names, network class, and scenario digest before any browser side
effect.
Each request names the exact launch executable or loopback CDP endpoint, target,
actions, and capture behavior. Origin filters and environment-variable names
are included when the scenario needs them.
Launch owns a temporary profile; connect mode disconnects from the selected
external browser without closing it. The request itself defines the operation;
there is no additional REA grant step. The browser and host still enforce their
own access and process rules.
## Network evidence
`capture.events: ["network"]` retains request, response, completion, failure,
and unfinished-request metadata. Select content independently:
```json
{
"browser": {
"mode": "launch",
"executable_path": "/absolute/path/to/chromium"
},
"start_url": { "url": "http://127.0.0.1:3000" },
"actions": [
{
"step_id": "run",
"action": "click",
"locator": { "kind": "css", "selector": "#start" },
"timeout_ms": 10000
},
{
"step_id": "ready",
"action": "wait_for",
"locator": { "kind": "css", "selector": "#done" },
"state": "visible",
"timeout_ms": 10000
}
],
"capture": {
"network": {
"request_body": true,
"response_body": true,
"header_values": true
}
}
}
```
All three options default to `false`. Any selected network content also enables
network metadata observation; listing `network` in `events` is then optional.
Each observed request object receives a capture-scoped `transaction_id`.
Requests sharing a URL remain distinct. Redirects link to the preceding
transaction only when it was observed; a null predecessor does not establish
that no earlier hop existed. Older captures without IDs remain readable and
their transaction association is unknown.
`scenario.network_content` records the resolved content selection. Comparing
captures with different selection, or legacy captures without that coverage
information, reports network evidence as unknown with a recovery instruction.
Capturing more data alone does not establish a website change.
`network-content` events reference the exact request or response event through
`source_event_sequence`. They include independently typed headers and body
states. Captured bodies retain base64 bytes, byte count, media type, and SHA-256
of the retained bytes. Request bytes are those exposed by Playwright; a
`not_exposed` request body does not prove absence or complete multipart file
coverage. Response bytes are decoded by the browser, not the compressed wire
representation. No later fetch is substituted for the original response.
Headers preserve producer order and duplicates. Authorization,
Proxy-Authorization, Cookie, and Set-Cookie values are null with
`redacted: true`; ordinary values remain intact. Declared UTF-8 secret values
and their standard URI/form or JSON-escaped representations are replaced without
decoding unrelated binary bytes. Other encodings are not interpreted. Digests describe the
retained representation after redaction.
Only completed responses are read. At the end of the final step, observation
stops; requests still in flight emit `request-unfinished`, and their observed
responses have explicitly unavailable bodies. This cutoff does not wait for
long-lived streaming connections. Already-started content reads settle before
browser cleanup, with a provider-owned five-second timeout per read and
cancellation support. Failed or unexposed reads retain per-content states and make selected
event coverage incomplete. Body events can arrive during later steps or final
drain; event bounds describe receipt during the step, while the source event
reference establishes the transaction phase. Comparison attributes content to
its source event's step and excludes the sequence reference from normalization.
Playwright scenario capture does not supply initiator stacks; passive CDP
observation retains its own initiator evidence.
## Scenario comparison
+95
View File
@@ -0,0 +1,95 @@
# Incremental capability migration
This working guide tracks the migration in #740. Update it as concrete domains
migrate. Existing public contracts and domain-specific ports remain authoritative;
there is no new universal provider interface or fixed module manifest.
## Ownership to preserve
| Responsibility | Current owner | Migration direction |
| --------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------- |
| Analyst semantics and Evidence representation | domain | Keep pure and provider-neutral |
| Named public input/output contracts | contracts | Keep exact CLI/MCP meaning and complete discovery |
| 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 |
| 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 |
Retain exact Evidence IDs, provenance, detached reads, atomic Evidence/Unknown
mutations and optimistic revisions. Preserve current close and failed-cleanup
record clearing while extracting ownership; #721 owns any retention-policy change.
Each CLI invocation and MCP connection keeps its own state. Provider registration
must not launch engines or install prerequisites.
## Migrate before generalizing
1. Record the actual callers, state, environment, protocol and verification owner
of the capability being migrated.
2. Move concrete construction behind its existing typed port. Preserve injected
ports, selected environment, queues, failure state and cleanup behavior.
3. Test both caller paths and the applicable package/real-provider boundary.
4. Use independently migrated capabilities to identify repeated metadata and
composition needs. Derive a small source declaration only when those needs
are established; keep unlike target/lifecycle semantics separate.
5. Move a coherent capability's files, fixtures and verification lanes in a
mechanical PR. Update all path-sensitive consumers in that same PR.
Pure helpers do not need an interface each. A capture-format adapter or typed
factory may be sufficient. Avoid generic JSON execution, external MCP catalog
passthrough, eager provider acquisition, new permission fields or a multi-package
rewrite. Optional loading should preserve successful peers and report the failed
capability without hiding tools from the catalog.
## Checks before and after a move
`npm run verify:test-discovery` compares actual Vitest file discovery with every
repository-owned `*.test.ts` under src and tests, including newly added files. The
check requires Git, includes untracked non-ignored tests, and excludes removed
paths during a move. It
rejects missing files and overlapping project ownership. It only lists files;
it does not replace behavior tests or real-provider verification.
Check imports, project isolation and coverage, development-test selection,
lint exceptions, catalog/generator imports, npm and CI verification entrypoints,
fixture ownership, and installed-package bridge/native path resolution.
Domain and contracts must not import application workflows, caller adapters or
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.
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
engines. Recording ports do not prove real provider or platform support.
## Extension examples to validate during migration
An implementation of an existing port should require a provider adapter, typed
production factory selection, exact producer parsing/provenance and matching
verification. Existing workflow meaning and public contracts should stay stable.
A new analyst capability deliberately adds semantics, named contracts, a typed
port/workflow when needed, both caller adapters, availability, documentation and
verification. Its integration is broader than swapping a provider. The migration
should eliminate incidental duplicate wiring, not erase those design decisions.
Update these examples with real migrated entrypoints before closing #740.
## First production factory migration
`src/composition/android.ts` and `src/composition/firmware.ts` construct providers
behind the existing AndroidAnalysisPort and FirmwareAnalysisPort. CLI registrars
pass their selected environment; MCP uses the process environment unless a caller
injects a port. Each factory call returns a fresh instance and acquires no process,
workspace or toolchain. Queues, cleanup-failure state and per-request cleanup stay
inside the provider. The existing launcher seams remain available to boundary
fixtures, which now exercise the production factories.
Application workflows and MCP registration must not import these concrete
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.
-101
View File
@@ -1,101 +0,0 @@
# Controlled JavaScript replay
`run_controlled_replay` executes only operator-selected extracted modules. It
does not launch or drive the original application. Static inference, passive
browser/Electron observation, isolated replay, and real application behavior
remain different authorities.
## Enable the ceiling
Replay is disabled by default. Configure exact source roots before REA starts:
```sh
export REA_JAVASCRIPT_REPLAY_ENABLED=true
export REA_JAVASCRIPT_REPLAY_ROOTS_JSON='["/absolute/extracted/modules"]'
```
The default trusted executables are the current Node.js runtime,
`/usr/bin/bwrap`, `/usr/bin/systemd-run`, `/usr/bin/systemctl`, and
`/usr/bin/bash`. Override them only with the corresponding
`REA_JAVASCRIPT_REPLAY_*_PATH` variable. Configuration creates the
administrator ceiling; it does not replace per-call approval. `rea doctor`
reports disabled, available, and exact sandbox-probe failures without running
module code. REA setup never installs these host components.
## Plan, review, execute
Call the same CLI command or MCP tool twice. The first request uses
`mode: "plan"`. Its response commits module paths and digests, closed dependency
aliases, cases, deterministic providers, runtime/backend identities, resource
limits, no-network policy, private filesystems, and a `plan_digest`.
Runtime commitments include the exact worker, seccomp filter, Node executable,
ELF loader, and shared-library source/destination paths and SHA-256 digests.
The execute request must repeat the same manifest and include:
```json
{
"mode": "execute",
"approved": true,
"plan_digest": "<exact digest returned by plan>"
}
```
REA rebuilds the plan. Any changed module, stub, case, runtime, backend, limit,
or export commitment returns `plan_stale` before worker admission.
Modules use `esm` or `commonjs-factory`. The latter accepts extracted Rspack
factory syntax such as `123(module, exports, require) { ... }` and implements
the bounded `require.d`, `require.r`, `require.n`, and `require.nmd` helpers.
Every import or require must map to a declared alias. There is no ambient
package resolution.
Explicit cases can be combined with deterministic `parser-boundaries`,
`sanitizer-boundaries`, or `clipboard-boundaries` generation. Supplying a
`right` manifest runs both sides with fresh realms per case and reports
`equal`, `changed`, or `unknown` comparisons.
## Boundary and Evidence
The worker receives module bytes over bounded stdin and runs under a fresh
Bubblewrap user/PID/network/IPC/UTS namespace, an architecture-checked seccomp
filter, an empty mount root with a descriptor-backed read-only Node closure,
private tmpfs, and a transient systemd user cgroup. Host and external network,
host writes, process spawning, workers, native addons, inspector access, and
ambient environment are unavailable. Wall time, memory, swap, tasks, CPU,
case input, worker-protocol input, output, stderr, depth, and node counts are
independently bounded. The parent also requires an exact worker response shape
and authenticates every returned case ID, order, and input digest. Values are
projected through own data descriptors only; Proxy values and accessors are
rejected without invoking their traps or getters.
Returns, exceptions, serialization failures, denials, timeouts, OOMs, crashes,
protocol failures, and cleanup state are observations with provider
`rea-javascript-replay` and authority `controlled-replay`. An observation does
not prove that the original renderer, preload, main process, browser, or remote
service behaved identically.
Each left/right run is retained as its own `observed` source Evidence. A
differential envelope is `derived` and links those source Evidence IDs, so the
comparison never erases the underlying observations.
Optional `reproducer_export` is committed by the plan and writes to its
caller-supplied path after the sandbox has stopped.
The owner-only manifest is written only after complete sandbox cleanup. Source
bytes are excluded unless `include_sources: true` was explicitly approved.
An export failure is retained in the result and does not erase a completed
replay observation.
## Local real-artifact verification
`npm run verify:replay` uses source-owned parser and hostile fixtures. An
operator can additionally supply a local ESM or Rspack/CommonJS-factory
manifest without copying module source into the repository:
```sh
REA_REPLAY_INPUT_PATH=/absolute/replay-manifest.json npm run verify:replay
```
The verifier canonicalizes the manifest's module paths and prints only plan,
module, and Evidence digests, case/comparison summaries, and cleanup state.
This is the intended seam for local Notion-like parser/sanitizer benchmarks.
+24 -30
View File
@@ -1,24 +1,23 @@
# Electron file-page observation
REA can attach to a user-owned Electron/Chromium CDP endpoint and inspect existing `file://` renderer pages without evaluating JavaScript or invoking Electron APIs. The caller supplies the endpoint directly; REA accepts only literal-port loopback HTTP endpoints. Selecting an endpoint exposes every Electron target it serves, including local file paths and page metadata.
REA can attach to an Electron/Chromium CDP endpoint and inspect existing `file://` renderer pages without evaluating JavaScript or invoking Electron APIs. The request supplies the endpoint directly; REA accepts only literal-port loopback HTTP endpoints. Selecting an endpoint exposes every Electron target it serves, including local file paths and page metadata.
This passive runtime surface is distinct from the target-free static
[`analyze_javascript_application`](javascript-artifact-reconstruction.md)
workflow. Static analysis reads an approved ASAR or extracted directory under
observations and static inferences are never silently treated as the same fact.
workflow. Static analysis reads a local ASAR or extracted directory without
executing application code. Passive observation inspects an existing Electron
page. The results keep static inferences and runtime observations distinct.
Use the separate
[`reconcile_javascript_runtime`](javascript-runtime-reconciliation.md) workflow
when both Evidence sets already exist.
## Configure authority
## Target boundary
The capability is disabled by default:
```bash
export REA_ELECTRON_OBSERVE_ENABLED=true
```
The capability is disabled by default. Its permission ceiling is loopback-only. REA accepts local hostless `file://` URLs that resolve to regular files; remote hosts, encoded path separators, and nonexistent paths are rejected. There is no separate filesystem-root configuration.
The request supplies a literal-port loopback endpoint and, for inspection, a
target ID. REA accepts local hostless `file://` URLs that resolve to regular
files; remote hosts, encoded path separators, and nonexistent paths are
rejected. The endpoint exposes every eligible target and its local path
metadata.
## Workflow
@@ -32,7 +31,8 @@ rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID \
--observation-ms 100 --json
```
Script content is excluded by default. Requesting it includes every authorized script source:
Script content is excluded by default. Setting `include_script_sources` in the
request returns the selected target's script sources:
```bash
rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID \
@@ -42,9 +42,9 @@ rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID \
The normalized result contains canonical local paths, frame and DOM
structure, resource metadata, stable script/resource identities, explicit
completeness, and content-addressed approved source artifacts. Script metadata
completeness, and content-addressed script artifacts. Script metadata
retains its execution-context frame ID when CDP supplies one. The capture also
inventories authorized worker, service-worker, and shared-worker targets with
inventories worker, service-worker, and shared-worker targets with
validated opener-target and parent-frame IDs. Worker discovery uses passive
target metadata; REA does not attach to or execute code in those targets.
Collection counts and aggregate script-source bytes are not capped. Like every
@@ -58,31 +58,25 @@ invoke Electron IPC, close a target, or terminate the application.
## Active Electron scenarios
Active Electron authority is separate and disabled by default. When enabled,
REA launches an operator-approved Electron executable through the official
Playwright Electron API, owns its lifetime, accepts bounded click/wait actions,
The scenario request supplies the Electron executable and application entry
point, plus any arguments and actions. The application root defaults to the
entry point's parent directory and can be supplied when the app uses a different
root. REA launches that executable through
the official Playwright Electron API and owns its lifetime. It accepts click/wait actions,
window-targeted renderer reload/crash actions, and synthetic `open-url` or
`second-instance` deep-link delivery. It records capture-scoped window and
WebContents identities, process metrics, and IPC channel and value-shape metadata.
Payload values are not retained.
Configure exact roots and run the real fixture verifier with an operator-owned
Electron runtime:
Run the real fixture verifier with an operator-provided Electron runtime:
```bash
export REA_ELECTRON_AUTOMATE_ENABLED=true
export REA_ELECTRON_AUTOMATE_AUTO_GRANT=false
export REA_ELECTRON_AUTOMATE_EXECUTABLE_ROOTS_JSON='["/absolute/path/to/runtime"]'
export REA_ELECTRON_AUTOMATE_APPLICATION_ROOTS_JSON='["/absolute/path/to/app"]'
REA_ELECTRON_EXECUTABLE=/absolute/path/to/electron npm run verify:electron
```
This capability actively launches and interacts with the target. It is not a
passive CDP observation and must be granted separately as `electron_automate`.
The default is fail-closed (`AUTO_GRANT=false`); an operator can instead issue
a project/session/one-shot grant through the normal permission workflow. The
owned process keeps normal host filesystem and network privileges, so this is
an authority boundary and lifecycle boundary, not a sandbox.
This operation actively launches and interacts with the selected target. It is
not a passive CDP observation. The owned process keeps normal host filesystem
and network privileges, so lifecycle ownership does not make it a sandbox.
The CLI and MCP surfaces accept the same schema. For a JSON request file:
@@ -105,7 +99,7 @@ caller-selected capture limits.
Active Electron Evidence can also be supplied to
[`reconcile_javascript_runtime`](javascript-runtime-reconciliation.md). That
projection is intentionally target-only and partial: it binds the approved
projection is intentionally target-only and partial: it binds the selected
application path and capture outcome to the static graph, while frames, scripts,
workers, and execution claims remain unavailable until a separate passive runtime
capture provides them.
+32 -282
View File
@@ -9,7 +9,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -23,12 +22,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -48,7 +43,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -62,12 +56,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -87,7 +77,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -101,12 +90,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -126,7 +111,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -140,12 +124,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -165,7 +145,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -179,12 +158,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -204,7 +179,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -218,12 +192,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -243,7 +213,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -257,12 +226,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -304,7 +269,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -318,12 +282,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -343,7 +303,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -357,12 +316,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -382,7 +337,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -396,12 +350,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -413,137 +363,6 @@
"required": ["code", "category", "message", "retryable", "remediation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"code": { "type": "string", "const": "permission_required" },
"category": {
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"unavailable",
"execution_failure"
]
},
"message": { "type": "string", "minLength": 1 },
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"additionalProperties": false
},
"details": {
"type": "object",
"properties": {
"capability": { "type": "string" },
"requested": {
"type": "object",
"properties": {
"capability": { "type": "string" },
"roots": { "type": "array", "items": { "type": "string" } },
"executables": {
"type": "array",
"items": { "type": "string" }
},
"environment_names": {
"type": "array",
"items": { "type": "string" }
},
"origins": { "type": "array", "items": { "type": "string" } },
"network": {
"type": "string",
"enum": ["none", "loopback", "external"]
},
"mount": { "type": "boolean" }
},
"required": [
"capability",
"roots",
"executables",
"environment_names",
"network",
"mount"
],
"additionalProperties": false
},
"missing": {
"type": "object",
"properties": {
"roots": { "type": "array", "items": { "type": "string" } },
"executables": {
"type": "array",
"items": { "type": "string" }
},
"environment_names": {
"type": "array",
"items": { "type": "string" }
},
"origins": { "type": "array", "items": { "type": "string" } },
"network": {
"type": "string",
"enum": ["none", "loopback", "external"]
},
"mount": { "type": "boolean", "const": true }
},
"additionalProperties": false
},
"ceiling": {
"anyOf": [
{
"type": "object",
"properties": {
"capability": { "type": "string" },
"roots": { "type": "array", "items": { "type": "string" } },
"executables": {
"type": "array",
"items": { "type": "string" }
},
"environment_names": {
"type": "array",
"items": { "type": "string" }
},
"origins": {
"type": "array",
"items": { "type": "string" }
},
"network": {
"type": "string",
"enum": ["none", "loopback", "external"]
},
"mount": { "type": "boolean" }
},
"required": [
"capability",
"roots",
"executables",
"environment_names",
"network",
"mount"
],
"additionalProperties": false
},
{ "type": "null" }
]
}
},
"required": ["capability", "requested", "missing", "ceiling"],
"additionalProperties": false
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
@@ -552,7 +371,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -566,12 +384,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -591,7 +405,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -605,12 +418,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -630,7 +439,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -644,12 +452,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -669,7 +473,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -683,12 +486,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -708,7 +507,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -722,12 +520,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -747,7 +541,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -761,51 +554,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"additionalProperties": false
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"code": { "type": "string", "const": "plan_stale" },
"category": {
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"unavailable",
"execution_failure"
]
},
"message": { "type": "string", "minLength": 1 },
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
+173
View File
@@ -0,0 +1,173 @@
# 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.
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.
## Caller-supplied tools
Provide existing tools using absolute executable paths, scoped to your shell:
```sh
export REA_BINWALK_COMMAND=/absolute/path/to/binwalk
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
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.
Unblob needs the external extractor for the selected format on its inherited
`PATH`: for example, `debugfs` for ext2/ext3/ext4. Its default Landlock sandbox
and skip rules stay enabled. Sandbox startup failures remain actionable errors;
REA does not retry with `--no-sandbox`. Missing extractors reported in a valid
analysis report retain partial results and dependency names inline.
Upstream sources are preserved unmodified as optional pinned submodules:
```sh
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).
## CLI and MCP
```sh
rea inspect-firmware-regions /path/to/firmware.bin
rea extract-firmware /path/to/firmware.bin /path/to/new-output
rea extract-firmware /path/to/firmware.bin /path/to/selected-output \
--offset 64 --length 4096 --max-depth 3
```
The corresponding MCP tools accept the same intent:
```json
{
"name": "inspect_firmware_regions",
"arguments": { "path": "/path/to/firmware.bin" }
}
```
```json
{
"name": "extract_firmware",
"arguments": {
"path": "/path/to/firmware.bin",
"output_directory": "/path/to/new-output",
"range": { "offset": 64, "length": 4096 },
"max_depth": 3
}
}
```
Both return Evidence with the original path and SHA-256, raw producer reports,
actual executable path/digest, version and limitations. Binwalk signature UUIDs
and Unblob blob IDs are producer-local identities; compare content hashes and
source ranges across runs. A matching version does not attest an installed
build's source revision, so that field remains unknown.
### Interpret the results
- Region offsets refer to bytes in the original firmware. `reported_size`
preserves Binwalk's output; `size_basis=provider_reported_validation_unknown`
reflects that Binwalk can replace unknown lengths with the next signature or
EOF before serialization. Neither a signature nor its confidence byte proves
a valid partition, encryption or runtime behavior.
- Extraction returns regular file paths, independently verified SHA-256 values,
producer hashes when available, chunks, diagnostics and task derivations.
Unblob's task report is unordered; REA resolves relationships by paths and
blob IDs. Logical references use `$input`, `$output`, or `$output/<relative-path>`
so a real file named `$input` stays distinct from the original input.
Chunk offsets belong to the task's input file. Only chunks in the
selected input have a known `root_file_range`, adjusted by the selected offset.
- Decompressed children have unknown original byte ranges and runtime addresses.
Task lineage does not establish a byte-for-byte mapping through decompression.
- `partial` marks unknown/encrypted chunks, reported extraction problems,
unpublished links/special files or reached depth limits. `complete` means the
engine finished its configured extraction without those reported conditions;
default skip rules and format coverage still apply. It does not establish
complete firmware reconstruction.
- The destination must be absent, absolute, with an existing parent. Publication
uses exclusive, symlink-resistant writes and durable digest readback. Existing
destinations are never overwritten. This is not an atomic directory rename:
a new tree can be visible while files are written. Failed uncommitted output
is rolled back. Symlinks and special files are recorded without publication;
empty directories, ownership and executable permission bits are not reproduced.
Select a verified native child for the existing tools:
```json
{
"name": "open_binary",
"arguments": {
"path": "/path/from/extraction/usr/bin/program",
"provider_id": "ghidra"
}
}
```
Then use `analyze_function`, `inspect_native_api` or the other native primitives.
Native architecture and executable validation still apply. This milestone does
not add MIPS/PPC/RISC-V decompilation, raw flash load profiles, bootloader/device
tree semantics, vulnerability scanning, emulation or hardware runtime capture.
## Resource and lifecycle policy
- Nonempty regular firmware inputs: at most **128 MiB**, copied and hashed into
a private workspace before analysis. No in-place source modification.
- One queued heavy operation per provider instance; Binwalk one thread and
Unblob one worker. No automatic parallel extraction engines.
- **120-second** deadline; inherited **1 GiB per-process address-space** limit,
120 CPU seconds and zero core-dump allowance through `prlimit`.
- JSON reports: at most **8 MiB**; oversized reports fail without a truncated
success. Diagnostic output is bounded with exact captured byte counts.
- Default staging budget: **256 MiB** and **10,000 entries**, configurable using
`max_output_bytes` / `max_output_files` (CLI `--max-output-bytes` /
`--max-output-files`). Depth defaults to 3, allowed range 1–10.
Staging budgets are polled and rechecked after exit; short-lived overshoot is
possible. The inherited file-size limit bounds each file, not total storage;
address-space limits are per process, not an aggregate memory quota. Worker
count is not CPU affinity for every external extractor. These limits do not
claim cgroup/container containment. Cancellation cleans the owned process group
before removing its workspace; uncertain cleanup retains resources and blocks
further operations in that provider instance.
## Verification
Generate small public, source-derived fixtures into ignored `_reference/`:
```sh
npm run fixtures:firmware
npm run verify:firmware
```
Fixture generation requires existing Python 3 and a host C compiler (`cc`);
override with `REA_FIRMWARE_PYTHON` / `REA_FIRMWARE_CC`. The independent oracle
records gzip offset/length and extracted file hashes. Real verification checks
CLI/MCP, full-file and selected-range extraction, unknown chunks, depth limits,
content parity and unchanged source bytes. No binary fixture is committed.
Additional lanes are explicit, so the base lane stays small:
```sh
# Existing mke2fs and debugfs required; put debugfs on the scoped PATH.
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.
REA_FIRMWARE_VERIFY_GHIDRA=1 npm run verify:firmware
```
`REA_FIRMWARE_MKE2FS_COMMAND` selects the optional ext4 fixture producer.
`REA_FIRMWARE_FIXTURE_ROOT` selects generated data storage.
`REA_FIRMWARE_TEST_ENTRYPOINT` can select an isolated installed npm package's
`scripts/rea.mjs` to verify the packaged CLI/MCP workflow. Real coverage here is
generated gzip/USTAR/ext4 with a Linux x64 ELF child; SquashFS, UBIFS, proprietary
firmware, other hosts and architectures remain unverified.
+188
View File
@@ -0,0 +1,188 @@
# DOS MZ and COM analysis with Ghidra
REA can analyze DOS MZ executables and explicitly selected COM images through the bring-your-own Ghidra adapter.
The Linux x64 verification lane exercises actual 16-bit disassembly and
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.
## Open and inspect
```bash
rea inspect /absolute/path/to/legacy.exe --provider ghidra --json
rea search /absolute/path/to/legacy.exe entry --kind procedures --provider ghidra --json
rea instructions /absolute/path/to/legacy.exe entry --provider ghidra --json
rea function /absolute/path/to/legacy.exe entry --provider ghidra --json
rea inspect-native-load-image /absolute/path/to/legacy.exe --provider ghidra --json
rea read-bytes /absolute/path/to/legacy.exe 0x10000 --length 16 --provider ghidra --json
rea address-to-file-offset /absolute/path/to/legacy.exe 0x10000 --provider ghidra --json
```
Use an observed procedure name or address from discovery when the target does
not name its entry function `entry`. MCP uses `open_binary`, `list_procedures`,
`procedure_info`, `read_function_instructions`, and `analyze_function` with the
same admitted target and provider semantics.
The classifier validates MZ page counts, initialized module length, header and
relocation-table bounds, relocation destinations, and the initialized entry
point. It distinguishes DOS load-module and relocation bytes from the Windows
new-header field. PE remains PE; NE, LE, and LX declarations are explicitly
unsupported. A damaged declared Windows header does not silently become DOS.
Inventory classification returns `unknown` when its bounded prefix cannot
establish the format; this is distinct from the complete target admission check.
Ghidra imports a private target snapshot with `MzLoader`, language
`x86:LE:16:Real Mode`, and compiler specification `default`. These settings,
the fixed Ghidra load segment `0x1000`, and the linear address convention are
committed in the analysis profile. The handshake rejects language or compiler
drift. Closing the session deletes the temporary project and snapshot.
The profile also commits `function_body_evidence: complete-inclusive-ranges-v1`,
so snapshots from older length-only Ghidra results do not satisfy the current
profile.
## Headerless COM images
COM has no identifying header. Select its interpretation explicitly; neither the
`.com` suffix nor arbitrary unrecognized bytes are enough to admit it.
```bash
rea inspect /absolute/path/to/legacy.com --target-format dos-com --provider ghidra --json
rea function /absolute/path/to/legacy.com 0x10100 --target-format dos-com --provider ghidra --json
rea inspect-native-load-image /absolute/path/to/legacy.com --target-format dos-com --provider ghidra --json
```
```json
{
"name": "open_binary",
"arguments": {
"path": "/absolute/path/to/legacy.com",
"format": "dos-com",
"provider_id": "ghidra"
}
}
```
The caller's interpretation applies to the complete file, even if its filename
suggests another format. Files must contain 1..65280 bytes: the 64 KiB COM segment
minus the 256-byte PSP prefix. Ghidra uses `BinaryLoader` with real-mode language
and maps file offset zero at `1000:0100` (linear `0x10100`). A packaged pre-analysis
script seeds the entry function and CS/DS/ES/SS context `0x1000`. These settings
and the preparation policy are committed in the analysis profile; a failed
preparation prevents the resident bridge from serving results.
Load-image verification checks all original and modified file bytes, complete
source mapping/memory digests, absence of relocations, entry and measured register
context. The context is imposed for analysis, not observed from execution. PSP
memory, initial stack contents/SP, DOS interrupts and PC-98 devices are not
modeled. COM has no MZ header, relocation table or appended-overlay model; all
selected bytes belong to its analysis module. The original file stays unchanged.
## Addresses and function extent
Returned default-space addresses are linear byte coordinates. A Ghidra
segment:offset address has linear value `segment * 16 + offset`; multiple
segment aliases can identify the same byte. REA accepts the returned linear
address for subsequent lookups and leaves instruction decoding context with
Ghidra. These coordinates are analysis load addresses, not file offsets or
observations of a running DOS system.
`procedure_info` and function dossiers include `body` evidence:
```json
{
"available": true,
"provenance": "ghidra-function-body-address-set",
"ranges": [
{ "start": "0x10000", "end": "0x1000d" },
{ "start": "0x10060", "end": "0x10065" }
],
"total_bytes": 20,
"span_bytes": 102,
"non_contiguous": true,
"contains_entry": true
}
```
Each end is **inclusive**. `total_bytes` counts the complete observed
AddressSet; `span_bytes` measures its enclosing span and is null across address
spaces. Gaps and shared tails must not be treated as owned bytes. This is
Ghidra's database observation, not a proof of original source ownership or
exhaustive code discovery. Providers that do not report complete ranges return
`available: false` and an explicit reason.
## Loaded-image evidence
`inspect_native_load_image` independently checks the immutable session snapshot
against the loaded Ghidra Program. It measures complete original and modified
FileBytes digests, every source mapping and its memory digest, relocation records
(including multiplicity), and external entry points. It verifies MZ header and
module partitions, relocation fixups and linear coordinates rather than relying
on the imported executable SHA alone.
The result is `verified`, `mismatch`, or `unsupported`. Every check retains its
expected and observed values. A memory-digest mismatch identifies the source
range's file offset and analysis address; it does not locate the first differing
byte inside that range. Mapping ends are **inclusive**, unlike the exclusive ends
from `list_segments`. Appended overlays remain in source-file identity and are
excluded from initialized module coverage. Uninitialized allocations are reported;
their contents and DOS runtime semantics are not verified. Formats other than MZ and explicit COM return
`unsupported` with measured observations, not a successful MZ verification.
`read_bytes` returns initialized provider memory, including loader fixups; an
unmapped or uninitialized byte ends the read with an explicit returned length and
`complete: false`. `address_to_file_offset` uses the provider's source mapping;
unmapped, uninitialized and ambiguous mappings fail explicitly. A relocated word
can differ from the original file word even when its source offset is known.
## Coverage boundaries
Static decompilation does not execute the target or emulate BIOS, DOS
interrupts, device ports, or self-modifying code. PC-98 hardware behavior and
recovered calling conventions can remain uncertain; provider pseudocode and
diagnostics are retained for analyst review.
For a packed executable, importing the original can analyze its unpacking
stub. Analyze a separately prepared unpacked copy to inspect the original
program's recovered code. Retain both artifact digests and the transformation
evidence; REA never silently replaces the admitted target. Appended overlays
are separate from the initialized MZ module. Raw COM files and DOS extenders
without an admitted MZ real-mode entry are outside this import boundary.
DOS MZ is available through Ghidra, with no Hopper support claim. It is outside
the Windows native PE P0 boundary. macOS DOS verification remains unverified.
## Real verification
```bash
npm run verify:ghidra:dos
```
The lane generates a small public fixture from source at runtime. It verifies
16-bit instruction bytes, a near call, a segment-relocated far call, actual
decompilation, disjoint function ranges, stable CLI/MCP observations, unchanged
input bytes, and session/process cleanup. It does not require external
executable fixtures, an emulator, or an existing analysis database.
Provider p-code representations can contain process-specific address-space selector
tokens. The lane preserves those reported values and compares stable dossier
observations rather than asserting cross-process identity of raw p-code.
## Function annotations
`annotate_native_function` changes the name and/or comments at one exact local function entry, verifies readback, and returns a refreshed dossier. This is useful when recording recovered DOS function roles; it does not claim original-source names. Name writes preserve the existing namespace; readback returns the fully qualified name used by function inventories. Regular comments map to Ghidra PRE comments and inline comments to EOL comments. Empty text clears a comment; omitted fields preserve it. The whole edit rolls back if a setter, readback or analysis fails.
```json
{
"procedure": "0x10100",
"name": "entry",
"comment": "Recovered startup role",
"inline_comment": "Inspect segment setup"
}
```
```bash
rea annotate-native-function /local/program.com 0x10100 --target-format dos-com --provider ghidra --name entry --comment 'Recovered startup role' --json
```
The CLI returns the updated analysis before closing its isolated session. MCP edits remain visible to later reads until `close_binary`. Original bytes remain unchanged, database edits are discarded on close, and immutable analysis snapshots are invalidated after edits. This operation does not save a Ghidra project, change ABI/types/body ranges, patch instructions, or emulate a DOS runtime. Windows P0 does not admit it.
+99
View File
@@ -0,0 +1,99 @@
# Optional NativeAOT metadata recovery
REA integrates [Washi1337/ghidra-nativeaot](https://github.com/Washi1337/ghidra-nativeaot)
through an optional headless adapter. The original source is an opt-in submodule,
pinned to `effeb734fc570c32650f88b159608979dc7b423e`; its MIT license and existing
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.
Windows Ghidra P0 has no database mutation authority. A PE/CLI or ReadyToRun
assembly should use `inspect_managed_artifact`; it is not NativeAOT.
## Build and configure
Bring your own supported Ghidra/JDK installation. This developer build needs
existing `git`, `javac` and `jar`; it does not invoke Gradle, download toolchains,
or change host configuration:
```sh
git submodule update --init third_party/ghidra-nativeaot
npm run build:ghidra:nativeaot
export REA_GHIDRA_NATIVEAOT_JAR="$PWD/_reference/nativeaot-integration/extension/rea-ghidra-nativeaot.jar"
```
The build requires a clean pinned upstream checkout, compiles unchanged upstream
Java plus the REA facade, uses a fresh classes directory, and writes the JAR and
build manifest under ignored `_reference/`. `REA_NATIVEAOT_BUILD_ROOT` can select
another local output directory; `REA_NATIVEAOT_SOURCE` can select an equivalent
clean pinned source checkout. Neither output nor compiled fixture belongs in Git
or the npm package. An installed REA accepts an existing explicitly supplied JAR.
REA commits the actual JAR SHA-256 into the analysis profile, copies identical
bytes into the owned runtime, rechecks the digest before loading, and closes the
classloader/project on session close. Profiles cannot enable code absent from
explicit local configuration. JARs execute within Ghidra; supply a build you trust.
The producer's reported source revision is build metadata, not an attestation of
arbitrary caller-supplied bytes. The bounded loader accepts regular JARs up to
8 MiB to limit startup artifact memory; this does not truncate analysis results.
## Use existing tools
Open the executable with provider `ghidra`. `inspect_native_load_image` returns
optional `observations.metadata_recovery` inline, including the detected header,
layout, recovery status, type category paths/addresses, diagnostics, annotation
coverage and derived-memory SHA. The enclosing `unsupported` load-image status
for PE/ELF concerns independent DOS loader verification; metadata recovery has
its own status. It is not a failed import.
Pass one returned type path or its typed address to `inspect_native_data_type`.
Its optional `metadata_recovery` includes related/base type, implemented
interfaces and virtual slots with targets and known procedure names. Follow
these with `analyze_function`, string searches and ordinary cross-references.
For CLI use:
```sh
rea inspect-native-load-image ./NativeAotFixture --provider ghidra --json
rea inspect-native-data-type ./NativeAotFixture --type /NativeAOT/MethodTables/Class_ADDRESS_MT --provider ghidra --json
rea function ./NativeAotFixture 0xADDRESS --provider ghidra --json
```
No new MCP workflow or approval flag is required. Configuration opts into the
adapter; without it ordinary Ghidra profiles/results remain unchanged.
## Evidence boundaries
Discovery prefers an imported RTR symbol, otherwise the upstream initialized,
non-executable-memory signature heuristic. REA rejects ambiguous candidates,
unknown layout versions, malformed directory rows, unsupported flags and
unmapped ranges before recovery. It requires the verified dehydrated layout.
The adapter performs rehydration, method-table recovery and frozen-object
annotation in one ephemeral Program transaction. Fatal failure/cancellation
rolls it back. Recovery may change analysis-memory bytes, never the original
executable file. Derived ranges have a digest and **no original file offset**;
they are not captured runtime state. Candidate/committed instance annotation
counts distinguish partial frozen-object coverage. Coverage concerns the
recovered candidate set, not every possible object or every runtime type.
REA preserves pre-recovery calling conventions and uses the loaded compiler
specification default for newly created methods. The upstream universal
`__thiscall` assignment is not treated as x64 ABI authority. Parameters and
method prototypes remain inferences; verify them against instructions/call sites.
Type relationships and System.Object/System.String identification use upstream
heuristics. Generated names are explicitly marked; original class/member names
and custom field layouts remain unknown. Pseudocode is recovered native logic,
not original C# source. Unsupported recovery reports its stage and reason; it
does not imply that ordinary native disassembly/decompilation is impossible.
## Verification
See [the dedicated lane](testing.md#optional-nativeaot-ghidra-analysis).
Fixtures are source-built benign .NET programs with inheritance, an interface,
virtual dispatch, representative arithmetic and frozen strings. Independent
compiler/linker symbols and raw directory bytes provide the oracle. Stripped
fixtures exercise heuristic detection. Tests do not execute the resulting
NativeAOT binaries. Fixture generation and adapter compilation are optional and
never prerequisites of ordinary native analysis or setup.
+111
View File
@@ -0,0 +1,111 @@
# IDA Pro provider
REA adapts an existing [mrexodia/ida-pro-mcp](https://github.com/mrexodia/ida-pro-mcp) server into its native analysis tools. Install and configure IDA, Hex-Rays, Python, and upstream MCP using that repository's instructions. REA does not install, upgrade, activate, or license them, and does not bundle another IDA server.
Choose one lifecycle mode in an upstream MCP JSON registration. Set `REA_IDA_MCP_CONFIG` to that file, then select `ida` once through `REA_ANALYSIS_PROVIDER`, the CLI's `--provider ida`, or MCP's `open_binary.provider_id`. No provider startup occurs during discovery.
## Analyze an already-open GUI target
The attached profile supports upstream **1.4.0 legacy tools**, including `get_metadata`, `list_functions`, and `decompile_function`. Copy the upstream registration's `command`, `args`, and optional `env` into a JSON file. An existing `mcpServers` object with an `ida-pro-mcp` entry is also accepted. For example:
```json
{
"command": "python",
"args": [
"/absolute/path/to/ida_pro_mcp/server.py",
"--ida-rpc",
"http://127.0.0.1:13337"
]
}
```
Use the executable and arguments from your working upstream registration. The legacy GUI's `/mcp` endpoint is its internal RPC endpoint; connect REA through the upstream stdio proxy rather than treating that URL as Streamable HTTP.
```bash
export REA_IDA_MCP_CONFIG=/absolute/path/to/ida-mcp.json
export REA_ANALYSIS_PROVIDER=ida
rea function /absolute/path/to/program main --json
rea decompile /absolute/path/to/program main --json
```
Open the **original input binary** in REA, not an `.idb` or `.i64` database. Its SHA-256 must match IDA's recorded input hash. REA checks the attached identity and image base around each analysis request. A mismatch or target switch fails with recovery guidance. The original input must remain readable by REA; WSL can use its mounted alias while the GUI runs on Windows.
REA's close operation releases its MCP connection or proxy. It never saves or closes the existing GUI database. External edits remain visible because results are live and are not replayed from snapshots. These checks do not lock the GUI or prove that the database is unpatched.
## Give REA a target for headless analysis
The headless profile supports upstream's **database supervisor** API: `idb_open`, `idb_list`, `idb_close`, explicit `database` arguments, and modern analysis tools. Legacy 1.4.0 `idalib-mcp`, which accepts one initial filename and serves SSE, does not implement this profile. Follow the [upstream headless instructions](https://github.com/mrexodia/ida-pro-mcp#headless-idalib-session-model) for an installation with the supervisor API.
```json
{
"command": "idalib-mcp",
"args": ["--stdio", "--max-workers", "1"],
"mode": "headless"
}
```
Use an already-installed executable. When required by your existing idalib installation, include `"env": { "IDADIR": "/absolute/path/to/ida" }`. REA neither edits the global activation configuration nor installs dependencies. A configured local Streamable HTTP supervisor can instead use `"url": "http://127.0.0.1:8745/mcp"` and `"mode": "headless"`.
The same CLI commands work in both modes. For a Windows-native REA process:
```powershell
$env:REA_IDA_MCP_CONFIG = 'D:\analysis\ida-mcp.json'
$env:REA_ANALYSIS_PROVIDER = 'ida'
rea function 'D:\samples\program.exe' main --json
```
Headless REA and the configured supervisor must share the host filesystem and native path syntax. Run REA on Windows when the supervisor runs on Windows. The attached profile can span WSL and a Windows GUI because it binds recorded input hashes instead of asking the GUI to open a WSL path.
REA copies the admitted binary into a private temporary directory, checks its digest, and requests an owned worker with `force_headless`. POSIX workspaces use mode `0700`; Windows uses the matching package's native private-DACL boundary on a local NTFS temporary directory. It injects the returned database ID into analysis requests. It verifies the active owned worker and input path rather than adopting a user database. On close or failed startup, it releases its session with `save: false`, verifies that it is absent from upstream session discovery, and removes its private input and sidecars. An unverified release retains the workspace and reports incomplete cleanup.
One REA session owns one database. Stdio registrations default `IDA_MCP_MAX_WORKERS` to `1`; the explicit upstream `--max-workers 1` also bounds a supervisor. Database size and engine memory depend on the target and upstream; REA does not impose an engine memory limit. The original binary is never executed or overwritten. Optional `workspaceRoot` selects an existing absolute temporary-directory parent on the REA host. Ensure it has space for the input copy and IDA working files.
Headless cleanup uses the upstream database lifecycle. Abruptly terminating REA, its proxy, or the supervisor does not establish worker release. Prefer `close_binary` for MCP sessions; after an interrupted process, inspect upstream session inventory and retained workspace diagnostics before removing working files. This adapter does not claim Windows Job Object containment for the upstream detached workers.
## Connect an agent once
Add the registration path to the **REA** MCP entry's environment, keeping the upstream registration in its separate JSON file:
```json
{
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@<version-with-ida>", "mcp"],
"env": {
"REA_ANALYSIS_PROVIDER": "ida",
"REA_IDA_MCP_CONFIG": "/absolute/path/to/ida-mcp.json"
}
}
}
}
```
Use a package version that actually includes this adapter; repository main can be ahead of the npm release. After reconnecting, call:
```json
{"name":"open_binary","arguments":{"path":"/absolute/path/to/program","provider_id":"ida"}}
{"name":"analyze_function","arguments":{"procedure":"main"}}
{"name":"search_strings","arguments":{"pattern":"license"}}
{"name":"close_binary","arguments":{}}
```
Opening an MCP target starts its selected provider session; headless opening waits for auto-analysis. Set the agent's MCP request deadline long enough for the target. The registration's optional `timeoutMs` defaults to 300000 for upstream requests. Cancellation drains the current upstream request before cleanup; it does not force-stop GUI analysis. An open request that times out without a completed response retains the private workspace because worker inventory alone cannot prove the request has stopped. `binary_session` reports actual tool availability. Repeated doctor/setup calls are unnecessary when the registration already works.
For guided registration, set `REA_IDA_MCP_CONFIG` before `rea setup --client <client>`. Setup includes that file reference in its reviewed plan and preserves it after approval; it does not copy upstream credentials into the agent configuration. `rea doctor --provider ida --json` validates the registration without starting IDA and explicitly leaves runtime compatibility and target binding for analysis.
## Coverage and evidence
Both profiles support procedure/string inventory and literal or regex search, procedure resolution, pseudocode, assembly, raw function instructions, resolved callees, incoming address references, and a function dossier. Inventories and modern pseudocode/assembly follow upstream pagination. Upstream limits on modern xrefs and callees are retained as explicit truncation limitations. A transport-level truncated preview is rejected rather than reported as complete evidence; the error preserves upstream retrieval guidance.
The legacy profile additionally supports direct callers. Its producer may report call-site or interior addresses; REA resolves the reported function symbol to its canonical entry. The modern profile does not prove direct callers from its code/data xref labels, so that capability is unavailable and the dossier marks it unknown.
Complete body ranges, typed reference edges, CFG, referenced data, source locals, comment kinds, and database revision are unavailable. Empty unsupported dossier facets mean **unknown**, not observed absence. `binary_overview`, whole-symbol inventory, mutation, debugging, arbitrary Python forwarding, and GUI navigation are outside this adapter's coverage. Check session availability before composing a workflow that needs those facets.
`provider.version` identifies the REA adapter; the profile explicitly records `version_scope: "rea-ida-adapter"`. The MCP handshake's `serverInfo` is separate and does not establish IDA or Python distribution versions. Raw observations preserve producer metadata and output. Attached hashes identify the recorded original input; headless identity uses the digest-verified private copy and observed worker input path. Neither proves source-code recovery or an unmodified analysis database.
## Verification
Run `npm run verify:ida -- --target /absolute/path/to/program --procedure main` with a configured upstream server. It exercises production CLI and MCP tools, validates their output contracts and Evidence, checks original-input preservation and session cleanup, and keeps local target data out of its console summary. Supply `--package-root` to verify an extracted or installed package. See [testing](testing.md) for the distinction between adapter tests and real-provider evidence.
The initial real workflows use stdio registrations for the legacy Windows GUI profile and a Windows x64 headless supervisor with IDA 9.3. Streamable HTTP is tested against a loopback MCP fixture with the pinned client SDK; a real IDA HTTP-supervisor workflow remains unverified. Linux/macOS headless engine operation, other IDA releases, architectures, and modern attached GUI tools require their corresponding real workflows before support is claimed.
+205 -95
View File
@@ -4,22 +4,27 @@ REA separates installing its CLI from configuring external software and agents.
## Start setup
The recommended setup entrypoint is:
Start setup with:
```bash
npx --yes rea-agents@latest setup
npx rea-agents setup
```
If npm asks to download and run the package, that approval applies only to the
current package-runner invocation. REA still prints its own setup plan and asks
for separate approval before changing agent configuration or installing a
product-owned component.
If npm asks to download and run REA, that approval applies only to downloading
the package. REA shows its own plan and asks before changing agent configuration
or installing Hopper.
The explicit `@latest` request prevents a project dependency or stale npx cache
entry from choosing the setup version.
REA runs the version npm selected and never silently replaces an intentional
version request after launch. To repair registrations written by an older
release, rerun the command above and review the resulting setup plan.
The short command can use a REA version installed in the current project. To
request the latest release explicitly, use:
```bash
npx rea-agents@latest setup
```
REA runs the version npm selects. To update older agent registrations, run the
latest-version command and review its setup plan. For unattended package
downloads, add `--yes` before the package name; this does not approve REA's
setup changes.
For an intentional rollback, make the package request explicit:
@@ -31,11 +36,11 @@ Setup continues to pin persistent MCP registrations to the exact version that
performed setup. Running current setup later migrates unversioned or older
managed registrations through the normal reviewed setup transaction.
REA supports Node.js 22.19+ and 24.11+ (including newer releases). It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew.
REA supports Node.js 22.x (>=22.19), 24.x (>=24.11), and 26+. Node.js 23, 25, and prereleases are unsupported. It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew.
Running `npm install rea-agents` without `--global` installs the executable only
in the current project's `node_modules/.bin`; it does not make `rea` available
on the shell `PATH`. Use the explicit setup command above for the guided setup
on the shell `PATH`. Use the setup command above for the guided setup
journey, `npx -y rea-agents@latest` for unattended one-off commands, or install globally
with `npm install --global rea-agents` for a shell-visible `rea` command.
@@ -56,60 +61,114 @@ curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh |
Supported options are `--version <semver>`, `--dry-run`, `--no-setup`, `--no-prompt`, and `--verbose`. Neither `--no-prompt` nor a non-interactive shell grants permission to install external dependencies.
## Released package and main
Repository main documents its current code and generated catalog. `@latest`
selects the npm release, and persistent MCP registrations are pinned to the
version that performed setup. Installing newer instructions does not update a
running server or its registration.
The release checked on **2026-10-06** was **4.1.0** (125 MCP tools). That artifact
includes main's Windows native control bundle, Android/JADX and firmware
tools, Ghidra function annotation workflow, and retained application-Evidence
references.
Main's catalog describes the current code; a source build or a subsequent
release containing later changes is required for anything newer. Package startup alone does not verify a
provider's real platform workflow.
To check the published version, run `npm view rea-agents dist-tags.latest`.
Use the connected server's actual tool list and advertised input schemas for
feature selection. The same package version string in a development checkout
does not establish that its bytes match the npm tarball. Update a registration
through a reviewed, scoped setup plan, then restart/reconnect the agent.
## Skill-only installation
```bash
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).
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
approve the exact changes before applying that scope. An aligned registration
with tools absent from the active session needs a restart/reconnection;
`doctor` checks files and prerequisites, not the live agent connection.
Guided setup installs the package's matching skill by default. The skills.sh
route can select newer repository instructions, so follow actual server schemas
and the release boundary above. Static JavaScript CLI inspection can proceed
while MCP is unavailable:
```bash
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
It returns the complete Evidence record directly and requires no native engine.
Provider failures in doctor do not prevent unrelated target-free tools.
## Supported agents
Setup can configure these clients for REA's local MCP server:
| Client | `--client` value |
| ------------------ | ---------------- |
| Claude Code | `claude_code` |
| Claude Desktop | `claude_desktop` |
| Codex | `codex` |
| Cursor | `cursor` |
| Gemini CLI | `gemini_cli` |
| Windsurf | `windsurf` |
| Devin | `devin` |
| OpenCode | `opencode` |
| Antigravity | `antigravity` |
| GitHub Copilot CLI | `copilot_cli` |
| Command Code | `commandcode` |
| VS Code | `vscode` |
## Review setup changes
`rea setup` uses an inline, scroll-preserving journey inspired by the clarity of
PostHog's CLI wizard. It begins with the outcome instead of the installer
mechanics:
`rea setup` first offers the supported agents in a multi-select. Existing REA
registrations are selected by default. Newly detected clients remain available
but unselected: detection gives setup context, not permission to add a new
registration. Clients without a detected configuration can still be selected.
Explicit `--client` flags skip this question.
- investigate local applications from a supported agent;
- recover evidence through an available deep-analysis provider;
- use the bundled skill for a repeatable investigation workflow.
Setup adds MCP access for selected clients. It installs REA's bundled workflow
with those integrations by default; use `--skill=false` to omit it. If no agent
is selected, setup offers the workflow separately for CLI use, with No as the
default. Hopper is a separate optional choice: setup shows its proposed
installation or connection and requires its own explicit approval. It can also
save verified paths for an existing Ghidra installation.
REA then summarizes the detected clients and asks which capabilities to set up.
The capability picker pre-selects `Agent integration` (the MCP registration and
matching guided workflow together) when detected agents exist, and presents the
optional Hopper provider alongside it. Selecting agent integration opens a second
checklist for the exact detected agents that should receive a registration; all
detected agents are pre-selected. Detection provides context and pre-selection;
it does not authorize a configuration write until the user confirms.
After selection, review the plan's exact paths and changes and approve before
REA writes files or installs Hopper. You can cancel at any prompt.
Because the user has explicitly invoked `rea setup`, the wizard biases toward the
happy path: capabilities and agents are pre-selected, and the final approval
defaults to **Yes**. The user can still deselect any item or cancel at any prompt.
Choosing no capabilities exits without changes. Selecting agent integration
always includes the bundled skill, even if all agents are deselected. The picker
keeps its navigation, selection, confirmation, and cancellation keys visible
instead of relying on a transient hint.
Before applying changes, REA checks your current configuration. The plan lists:
Every selected path converges on the same exact preflight. REA validates the
current state, prints the proposed effects, and asks for final approval with
**Yes** as the default. Selection alone never authorizes a mutation. The plan
identifies:
- an existing Hopper installation, a validated bring-your-own Ghidra environment, or the official Hopper package it proposes to install;
- an existing Hopper installation, a verified existing Ghidra installation, or the official Hopper package it proposes to install;
- each detected agent configuration path;
- the REA skill destination;
- external software, network origins, integrity evidence, and package-manager
commands.
Malformed or unsafe existing configuration blocks the whole transaction before
Hopper installation or any file write. Declining, pressing Ctrl-C, or selecting
nothing makes no changes. Agent configuration writes preserve unrelated
Hopper installation or any file write. Declining or pressing Ctrl-C makes no
changes. Agent configuration writes preserve unrelated
entries, create backups, use atomic replacement, and verify their result.
Progress remains append-only so completed and failed operations stay visible in
terminal history. After a successful run, the completion message names the
verified capabilities now available—for example configured MCP clients, the
selected analysis provider, and the installed skill—and gives the corresponding
next action. When an agent must restart to load its registration, REA says so;
otherwise it suggests beginning an investigation. It does not advertise a
capability that the final diagnostic check did not verify.
After setup, REA reports which agents, analysis tools, and workflow files passed
its final checks. Restart any agent named in the completion message, then begin
your investigation. Failed steps and diagnostics remain in terminal history.
Select exact clients in scripts with repeatable `--client` flags, or retain
automatic discovery explicitly with `--all-detected`. Use `--skill=false` to
override the normal bundled-skill installation and `--dry-run` for a read-only
plan:
Select exact clients in scripts with repeatable `--client` flags. Each explicit
client skips interactive selection. Use `--all-detected` only when you intend
to configure every detected supported client. `--skill=false` omits the
workflow, and `--dry-run` returns a read-only plan with status `planned` and
exit code 0:
```bash
rea setup --client codex --client cursor --skill=false --dry-run
@@ -117,13 +176,20 @@ rea setup --client codex --client cursor --skill=false --dry-run
Prompt UI and progress are written to stderr so stdout remains available for
structured results and pipelines. `NO_COLOR=1` disables color. Use
`--accessible` for sequential, vertically rendered yes/no prompts.
`--accessible` for sequential, vertically rendered yes/no prompts. Implicit
interactive setup requires stdin, stdout, and stderr to all be terminals; when
any stream is redirected, setup stays non-interactive. Declining or cancelling
returns status `cancelled` with exit code 0.
For automation, `rea setup --json` reports the plan without applying it.
Prefer pairing `--yes` with explicit scope such as `--client codex`,
or `--all-detected`. Legacy unscoped `--yes` remains compatible for
this release but emits a deprecation warning. Installing missing Hopper
non-interactively additionally requires `--install-hopper`:
For automation, `rea setup --json` reports the plan and a compact `.doctor`
readiness projection without applying it. Use `rea doctor --json` for full
health diagnostics and canonical tool catalog details. Pair
`--yes` with explicit scope such as `--client codex`, or use
`--all-detected` when the broad scope is intended. Without a scope flag,
`--yes` is limited to existing REA-owned registrations; it does not select all
detected clients. An unapproved actual apply reports `needs_confirmation` and
exits 1. Installing missing Hopper non-interactively additionally requires
`--install-hopper`:
```bash
rea setup --yes --all-detected --install-hopper --json
@@ -131,11 +197,14 @@ rea setup --yes --all-detected --install-hopper --json
Setup pins package-runner MCP registrations to the exact installed REA version,
installs the matching skill and on-demand references in the same plan, and adds
`startup_timeout_sec = 30` for Codex. Interactive `rea upgrade` installs the new
executable and then opens the updated `rea setup --all-detected` plan for
separate approval. Structured or non-TTY upgrades defer that integration sync.
Use the same setup command to migrate floating, unversioned, or stale
registrations, then restart changed clients.
`startup_timeout_sec = 30` for Codex. `rea update` installs the exact resolved
release into the npm prefix that owns the running package, then checks the new
executable's version before reporting success. It does not reopen onboarding.
Release lookup and installation both use npm's configured registry.
Its maintenance plan selects only existing REA registrations and an already
installed REA skill. Run the returned scoped setup command to review and approve
those changes, then restart affected agents. The plan is returned in terminal,
non-TTY, and JSON modes without applying configuration changes.
## Hopper
@@ -193,15 +262,28 @@ copying license secrets into logs, or killing unrelated Hopper processes.
## Ghidra
REA's Ghidra provider is bring-your-own and supports Linux x64 with the exact
official Ghidra 12.1.4 release and a 64-bit full JDK 21. macOS is not an
admitted Ghidra host. An experimental
Windows x64 P0 supports approved native x86-64 PE applications. It supplies
discovery, analysis-profile commitment, an isolated read-only headless session,
ten inventory/name/search operations and nine function-analysis operations,
for 19 read-only operations total. Function analysis covers metadata,
decompilation, assembly, resolved calls, typed references, xrefs, CFG, and
dossiers. GUI state and analysis mutations remain unavailable through Ghidra.
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
must include the native decompiler for the host architecture; REA does not
build it or change Gatekeeper quarantine settings.
The adapter exposes 25 read-only operations: thirteen inventory/name/search
operations and twelve function-analysis operations. These cover metadata,
decompilation, assembly, resolved calls, typed references, xrefs, function
dossiers, instructions, recovered data types, measured load mappings, loaded
memory bytes, and observed file offsets. Independent load-image attestation
supports DOS MZ and explicitly selected COM; PE returns its measurements with that limitation.
On Linux and macOS, `annotate_native_function` also edits a function name and/or
entry comments atomically and returns refreshed analysis. These session metadata
edits leave executable bytes unchanged and are discarded on close. GUI controls
require Hopper; Windows P0 remains read-only.
Windows P0 admits native x86-64 PE applications on fixed local NTFS volumes.
The npm package bundles native Job Object ownership, protected private runtime
DACLs, and handle-based path admission; no separate addon installation is needed.
See the [Windows Ghidra P0 guide](windows-ghidra-p0.md) for verified scope and
limitations.
Extract Ghidra and install the JDK outside REA, then export absolute paths:
@@ -212,7 +294,7 @@ rea doctor --json
rea setup
```
PowerShell configuration for Windows uses the same non-secret paths:
On Windows, configure the existing installation in PowerShell:
```powershell
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
@@ -221,10 +303,12 @@ rea doctor --json
rea providers --json
```
`rea setup` does not mutate Windows client configuration or install Hopper,
Ghidra, Java, Python, or another package. Register the built `rea mcp` command
manually in the intended client and preserve the two environment variables.
See [Windows Ghidra P0](windows-ghidra-p0.md) for a complete example.
`rea setup` can configure supported Windows agents and install the bundled
REA skill after approval. Direct registrations use Node to launch REA's entry
script; package-runner registrations use the pinned `npx` command. Hopper
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
@@ -237,16 +321,23 @@ installs, upgrades, or modifies Ghidra or Java.
Each verified session uses an ephemeral temporary project and isolated
home/cache/config/temp paths. REA passes `-readOnly`, `-deleteProject`, uses
Ghidra's default analysis and resource settings, and loads its packaged Java
bridge via `-scriptPath`; it never opens an existing user project. Linux uses a
current-user-only local bridge socket and descriptor. Windows P0 uses
token-authenticated IPv4 loopback, a token-free endpoint record, and bounded
process-tree termination. It does not yet prove private DACL,
reparse-point-safe, or Job Object semantics; use only approved non-sensitive
fixtures.
bridge via `-scriptPath`; it never opens an existing user project. Linux and
macOS use a current-user-only local bridge socket and descriptor. The
experimental Windows transport uses authenticated IPv4 loopback with a
private native-owned bearer descriptor and Job Object process ownership.
Operations begin only after default auto-analysis completes. The provider
startup deadline fails the open rather than exposing partial analysis. One
session contains exactly one imported Program; use `provider_id: "ghidra"`, `--provider ghidra`, or
Operations begin only after default auto-analysis completes. `open_binary`
selects and validates the target/provider binding; it does not wait for Ghidra
import and auto-analysis. The first Ghidra-backed query starts that work lazily.
The provider startup deadline is 330,000 ms for import, analysis, bridge, and
health readiness; a startup failure is returned by the query that triggered it,
without exposing partial analysis. This deadline is separate from MCP transport
initialization and the client's deadline for that individual tool call. A client
can time out earlier even when Ghidra would complete within its startup deadline.
See [Ghidra first-query deadlines and recovery](mcp-contracts.md#ghidra-first-query-deadlines-and-recovery)
for client options and the close/reopen recovery flow.
One session contains exactly one imported Program; use `provider_id: "ghidra"`, `--provider ghidra`, or
`REA_ANALYSIS_PROVIDER=ghidra` when both Hopper and Ghidra support the target.
One persistent decompiler is owned by the Program, and a serial queue keeps
Ghidra API calls on the owning Program thread without a fixed queue length.
@@ -255,9 +346,18 @@ is no fixed per-operation or response-size ceiling. Unresolved computed calls
remain unknown, reference-kind provenance is preserved, and provider-specific
pseudocode is never treated as original source or Hopper-equivalent text.
Unexpected request failures retain their original internal cause. CLI and MCP
errors expose Error names, messages, and available codes under
`details.diagnostics.failure_cause`; other rejection values retain their
primitive value or an explicit type. Shutdown warnings include the failure
kind, message, and diagnostics, while process and temporary-project cleanup
continues. These messages redact known bridge authentication tokens and
preserve local paths and other analysis context.
Run `GHIDRA_INSTALL_DIR=... npm run verify:ghidra` from a source checkout to
compile and analyze debug and stripped x86-64 ELF fixtures on Linux x64. This lane needs only a host C compiler
in addition to Ghidra and its JDK.
compile and analyze debug and stripped host-native fixtures (ELF on Linux x64
or Mach-O on macOS), plus a native DWARF 4 type-layout object. This lane needs a
host C compiler in addition to Ghidra and its JDK.
Run `GHIDRA_INSTALL_DIR=... npm run verify:ghidra:cross-format` to add AArch64
ELF, x86-64 PE, and x86-64 Mach-O fixture coverage. This separate lane needs
@@ -267,12 +367,17 @@ host-native Ghidra acceptance lane.
On a controlled Windows x64 runner, use
`npm run verify:ghidra:windows`. The verifier generates a deterministic native
PE fixture from source bytes, exercises every admitted Ghidra operation, checks
target/snapshot/import digest identity, and requires complete runtime cleanup.
PE fixture from source bytes and requires the Windows native authority before
opening the provider. The controls are implemented on main; see the
[release boundary](#released-package-and-main). The lane verifies all 25 admitted
read-only operations, digest identity, and runtime cleanup. The separate
`verify:ghidra:windows:package` lane checks the packed CLI/MCP boundary with an
ordinary user. It requires the built Windows native bundle and an existing
Ghidra/JDK installation; neither is inferred from startup alone.
## Diagnose, update, and remove
`rea doctor --json` is strictly read-only. `rea upgrade` updates only the npm installation that owns the running CLI. `rea uninstall` removes only REA-owned agent registrations and skill files; `--purge-data` additionally removes REA cache and state paths.
`rea doctor --json` is strictly read-only. `rea update` updates only the npm installation that owns the running CLI. Source checkouts and package-runner copies must be updated through the mechanism that owns them; a fresh package-runner invocation can use `npx rea-agents@latest`. `rea uninstall` removes only REA-owned agent registrations and skill files; `--purge-data` additionally removes REA cache and state paths.
## MCP Registry
@@ -286,17 +391,22 @@ server through `npx` with the `mcp` command.
For a client that requires manual configuration, use:
<!-- x-release-please-start-version -->
```json
{
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@latest", "mcp"]
"args": ["-y", "rea-agents@4.1.0", "mcp"]
}
}
}
```
Use an exact `rea-agents@VERSION` in the arguments when a reproducible client
configuration is required. `rea setup` writes the same package-runner shape and
pins it to the exact version that performed setup.
<!-- x-release-please-end -->
Persistent registrations should use one exact package version. `rea setup`
writes the same package-runner shape and pins it to the exact version that
performed setup. Run current setup to refresh an older registration, then
restart the client.
+1 -1
View File
@@ -97,7 +97,7 @@ correspondence is a separately identified cross-layer inference.
Every observation and edge carries all of the following:
- an authority, such as artifact bytes, AST analysis, static relationship
inference, passive CDP runtime, cross-layer reconciliation, controlled replay,
inference, passive CDP runtime, and cross-layer reconciliation,
native analysis, historical reference, or user assertion;
- an epistemic state: `observed`, `inferred`, `unknown`, or `unavailable`;
- confidence independently bounded as exact, high, medium, low, or unknown;
+50 -27
View File
@@ -7,7 +7,7 @@ are `trace_application_feature`, `trace_javascript_semantics`,
`compare_javascript_export_shapes`; their CLI equivalents use the same names
with hyphens.
Both workflows consume Evidence produced by
These workflows consume Evidence produced by
`analyze_javascript_application` or `reconcile_javascript_runtime`. They do not
read an artifact, execute application code, attach to a process, or open a
native-analysis provider. Static artifact observations, passive runtime
@@ -55,6 +55,16 @@ registrations/removals and dispatch, Node timers and cancellation handles, async
defaults, request construction and response consumers, parse/coercion/
validation boundaries, and built-in resource acquisition/release.
Mutation tracking covers explicit member assignments, updates, deletion, and
loop assignment targets, including the supported local aliases and shared
nested references. Calls such as `Object.assign`, `Reflect.set`, and
`Reflect.deleteProperty`, and writes reaching a caller's object through another
function's parameter, are not tracked by this mutation pass. These channels
can leave an initializer-derived literal in the graph even after the runtime
value changes. Treat such a result as an uncovered mutation channel, not as
proof of the current runtime value; a follow-up needs to model that channel
and verify its caller/alias behavior.
Function fingerprints commit normalized syntax, control-flow shape, relation
shape, literal sets, arity, and detected effects without using local names or
source offsets. Equal duplicate fingerprints remain ambiguous. Dynamic
@@ -132,22 +142,48 @@ unique occurrence on each side and pairing is reciprocal. Changes use JSON
Pointer paths with `added`, `removed`, `changed`, or `unknown` status. A missing
field is added or removed only when the relevant parent-property coverage is
complete on both shapes. The output includes exact selector candidates,
omissions, Evidence links, coverage, limitations, and a separate controlled
replay recommendation; it does not execute JavaScript.
omissions, Evidence links, coverage, and limitations; it does not execute
JavaScript. When runtime semantics matter, run behavioral probes directly
against the relevant application versions and capture them through the
available browser, Electron, or process workflows.
## CLI and verification
All five CLI commands accept inline JSON or a path to a JSON file. Put the full
Evidence records in the workflow input, as in the corresponding MCP tool; the
CLI does not resolve Evidence IDs from a separate bundle.
All five CLI commands accept inline JSON or a path to a JSON file. The CLI
returns an Evidence record directly. Put the full records in a later CLI input;
a separate CLI process has no retained MCP connection state.
For a literal string trace, analyze your supplied tree once, then build the
input from the saved Evidence (replace the target and seed):
```bash
rea analyze-javascript-application /absolute/path/to/app --json > application-evidence.json
node --input-type=module -e '
import { readFileSync, writeFileSync } from "node:fs";
const application = JSON.parse(readFileSync("application-evidence.json", "utf8"));
writeFileSync("trace-input.json", JSON.stringify({
application, seed: { kind: "string", value: "search-result" }, direction: "both"
}));'
rea trace-application-feature ./trace-input.json --json
```
MCP analysis returns an envelope containing `result`, `evidence_id`, and full
`evidence`. If the connected server advertises retained references, reuse its
exact returned ID as `{"kind":"retained-evidence","evidence_id":"RETURNED_ID"}`
in `application`, or `left`/`right` for comparisons. `RETURNED_ID` is a template,
not a literal valid ID. Native Evidence arrays still use complete records.
References are scoped to one connection; `close_binary` clears them. Export a
bundle before closing and import it on another connection, or supply the full
inline Evidence there. Versions before 4.1.0 accept full inline Evidence only; installing
newer skill instructions does not change that schema. See
[MCP Evidence inputs](mcp-contracts.md#retained-application-evidence-inputs).
For two operator-provided directories or ASARs, run:
```bash
npm run verify:application-workflows -- \
--left /absolute/path/to/version-a \
--right /absolute/path/to/version-b \
--source-map-read-approved
--right /absolute/path/to/version-b
```
The verifier reconstructs both versions independently, compares them, runs one
@@ -163,23 +199,10 @@ Because source Evidence and derived Evidence are retained by the normal session
ledger, evidence bundles and analysis snapshots can carry these records without
another persistence format.
## Controlled replay boundary
## Runtime evidence
Static graph workflows never execute a graph node or recovered module.
`run_controlled_replay` is the separate extracted-module boundary: it requires
the `javascript_replay` authority, a plan call followed by exact content-bound
approval, and the mandatory Linux OS sandbox fixed by
[ADR-0002](adr/0002-controlled-replay-authority-and-sandbox.md). Browser,
Electron, Process Capture, artifact-read, and static-analysis approvals do not
authorize that execution.
Replay observations retain `controlled-replay` authority. They cannot
promote static inference into passive runtime observation or prove that the
original application, renderer, preload, main process, or remote service
behaved identically.
Module, explicit case, and preset-generated case lists have no fixed
item-count ceiling. Replay plans commit the caller's resource budgets, and the
worker protocol checks the complete request against that declared byte budget
before probing executables or reading modules. Aggregate module-source and
case-input bytes are likewise checked against the caller's declared budgets.
Static graph workflows do not execute recovered code. When runtime semantics
matter, run behavioral probes directly against the relevant application
versions and capture them through the available browser, Electron, or process
workflows. Those observations do not prove behavior in an unobserved app
version or environment.
+21 -3
View File
@@ -52,6 +52,23 @@ manifest and graph commitments, JavaScript Application Graph, static
Electron summary, reconstruction statistics, and explicit limitations. It does
not require a live Hopper, Ghidra, browser, or Electron process.
## Large results
Graph and Evidence identifiers hash canonical JSON incrementally, without
assembling a single string for the whole graph. The canonical bytes and existing
identifiers remain unchanged. The opt-in regression check is
`npm run verify:javascript:digests`;
it hashes a value larger than the running Node engine's single-string limit.
Output formatters and MCP transport serialization still assemble whole strings.
For a large CLI result, select the needed fields
before formatting, for example `--format json --filter-output
evidence_id,normalized_result.statistics`. Complete serialization of a result
beyond the engine's string limit is not established by this digest check.
Client framing limits also apply: the pinned Node MCP SDK's stdio transport
defaults to a 10 MiB buffer. Its caller-selected `maxBufferSize` must accommodate
the complete response, including text and structured Evidence projections.
## What is reconstructed
The projector reuses the content-addressed artifact inventory and safe artifact
@@ -125,9 +142,10 @@ For `.node` bindings, member names mean “requested by JavaScript syntax.” A
resolved add-on path does not convert them into verified native exports. Native
symbol verification remains a separate deep-analysis claim.
Endpoint observations preserve useful local diagnostics while removing URL
credentials, fragments, and query values. Artifact paths, digests, parse
locations, and analysis metadata remain actionable because REA is local-only.
Endpoint observations preserve useful local diagnostics, including query values
and fragments, while removing only URL username/password credentials. Artifact
paths, digests, parse locations, and analysis metadata remain actionable
because REA is local-only.
## Safety boundary
+57 -9
View File
@@ -7,19 +7,35 @@ have equivalent `rea list-javascript-runtime-targets` and
`rea observe-javascript-runtime` commands. Successful calls return
deterministic Evidence.
This authority is disabled by default and is separate from browser CDP,
Electron file-page inspection, Process Capture, and controlled JavaScript
replay:
Each request supplies the loopback Inspector endpoint and, for observation, the
target ID. This surface is separate from browser CDP, Electron file-page
inspection, and Process Capture:
```bash
export REA_V8_INSPECTOR_OBSERVE_ENABLED=true
rea list-javascript-runtime-targets http://127.0.0.1:9229 \
--json
rea observe-javascript-runtime http://127.0.0.1:9229 TARGET_ID \
--runtime-kind node --json
```
## Start a caller-owned Node target
Run your selected application with Inspector bound to loopback:
```bash
node --inspect=127.0.0.1:9229 ./app.mjs
```
Keep it running while listing targets and observing the selected ID. Use a free
port and the same literal endpoint in both REA calls. Stop the application
through its normal lifecycle when finished; REA disconnects but does not own or
terminate it. Node.js must already be installed.
Use `--inspect` for this passive workflow. `--inspect-brk` pauses before the
application starts, and REA never resumes it or sends
`Runtime.runIfWaitingForDebugger`. Resume a paused target with a separate
caller-owned debugger before requesting application runtime observations.
The caller supplies the literal-loopback HTTP endpoint directly. Selecting an endpoint exposes all targets it serves and their local script locations or renderer origins. A target must match the exact requested target ID and expose a same-port WebSocket reported by that endpoint. Targets already marked attached are rejected rather than displacing another debugger. Local paths are canonicalized after symlink resolution.
`list_javascript_runtime_targets` returns every available target in one inline
@@ -27,6 +43,38 @@ array.
For the MCP follow-up call, pass the same literal-loopback endpoint and the selected target ID to `observe_javascript_runtime`. REA rediscovers targets and validates the ID against the live endpoint before observation.
### Discovery file locations
Node's `/json/list` reports a best-effort pathname prefixed with `file://`,
without URI encoding. On POSIX, REA interprets unambiguous paths literally,
including `#` and `%` in filenames. Node also replaces backslashes and double
quotes with underscores. That transformation cannot be reversed reliably:
an underscore may already be part of the original filename.
Windows paths, ambiguous underscore paths, and local discovery paths that
cannot be verified remain eligible by their endpoint and exact target ID.
Their target location preserves the reported value:
```json
{
"kind": "unresolved",
"reported_url": "file://C:_tools_entry.js",
"reason": "unverifiable-file-location"
}
```
This location does not claim that a file exists or authorize reading its
contents. Listing remains HTTP-only. Observation resolves loaded script
locations independently from `Debugger.scriptParsed` file URLs, preserving
the existing checks for local files and encoded separators. Unsupported
schemes, remote file hosts, and a bare `file://` remain excluded. These Node
discovery rules do not apply to page targets or other runtime products.
Reconciliation retains an unresolved `target_location` object and reports
`runtime-location-unresolved` with unknown confidence for that target. It
does not derive a static match from the reported pathname; verified loaded
script locations remain available for independent matching.
## Passive protocol boundary
The provider sends exactly two protocol commands:
@@ -66,14 +114,14 @@ Electron role, so Evidence records the role authority as
`caller-declared-unverified`.
Electron main, preload, and renderer behavior can therefore be observed only
as separate approved Inspector targets. The provider does not infer that two
as separate Inspector targets. The provider does not infer that two
targets belong to the same Electron application.
## Capture and determinism
Every observation records its time window and returns all valid script and
execution-context events received during it. Per-location protocol validation
remains in force. Scripts are authorized after capture, deduplicated by stable
remains in force. Scripts are validated after capture, deduplicated by stable
metadata, and canonically sorted. Wall-clock timestamps and protocol script IDs
are excluded from the durable result, so identical inputs and captured metadata
produce the same Evidence ID.
@@ -88,8 +136,8 @@ behavior does not occur.
Pass the resulting observation Evidence and an
`analyze_javascript_application` Evidence record to
`reconcile_javascript_runtime`. The reconciliation accepts this provider
alongside passive web and Electron page captures. Exact approved file or URL
mappings can correlate script presence with JavaScript Application Graph
alongside passive web and Electron page captures. Exact file or URL mappings
supplied with the request can correlate script presence with JavaScript Application Graph
assets. Because this provider never reads source bytes, matches normally use a
unique authorized location and remain weaker than captured-byte identity.
Builtin `node:` scripts remain runtime-only nodes unless a future explicit
+7 -9
View File
@@ -21,7 +21,7 @@ parameters, and normalized-result schemas are checked before reconciliation.
supplied as additional layers.
2. Run `inspect_web_page`, `inspect_electron_page`, or the attach-only
`observe_javascript_runtime`. Capturing script sources
is optional, but an approved source capture provides the strongest byte
is optional, but a source capture provides the strongest byte
identity.
3. Pass the resulting Evidence records to `reconcile_javascript_runtime`.
Browser URLs normally need an explicit URL-prefix mapping. Extracted
@@ -77,18 +77,16 @@ npm run verify:javascript-runtime -- \
--mapping '{"layer":1,"kind":"file-root","root":"/runtime/cache","artifact_prefix":""}'
```
The verifier analyzes the supplied static paths under a temporary in-process
permission policy, reads bounded Evidence JSON files, and prints graph IDs,
evidence IDs, paths, digests, counts, reasons, and coverage. It does not print
captured source text. `--source-map-read-approved` remains a distinct explicit
approval.
The verifier analyzes the supplied static paths, reads bounded Evidence JSON
files, and prints graph IDs, evidence IDs, paths, digests, counts, reasons, and
coverage. It does not print captured source text.
## Matching rules
Runtime targets, frames, scripts, and workers become capture-scoped graph
nodes. REA evaluates compatible static candidates conservatively:
1. An approved captured-source SHA-256 plus one matching mapped location yields
1. A captured-source SHA-256 plus one matching mapped location yields
a content-and-location match.
2. One unique exact content digest can match even when no location mapping is
available. Whole-asset digests and reconstructed module-factory source
@@ -144,8 +142,8 @@ EventEmitter activity, Electron IPC, or script-unload evidence. A unique
authorized file or URL location can still correlate script presence with a
static asset, but it does not prove module initialization or feature execution.
Source-map declarations and approved original-source reads stay under static
source-map authority. They are reported in `source_map_authority` but never used
Source-map declarations and original-source reads remain static evidence. They
are reported in `source_map_authority` but never used
as primary runtime byte matches. A source name or `sourcesContent` digest is not
silently equated with the generated bytes observed by CDP.
+142 -73
View File
@@ -1,26 +1,45 @@
# Managed-code analysis plan
# Managed-code analysis and planned extensions
This document turns
[ADR-0003](adr/0003-managed-code-evidence-and-provider-boundary.md) into an
implementation and verification plan. REA currently ships read-only PE/CLI
triage and exact identity through `inspect_managed_artifact` and
`rea inspect-managed-artifact`, plus file-backed metadata, signature,
method-body CIL, exception-region, call-edge, and field-access inspection
through `inspect_managed_members` and `rea inspect-managed-members`, and
declared ModuleRef/ImplMap/PInvoke and native implementation boundary inventory
through `inspect_managed_native_boundaries` and
`rea inspect-managed-native-boundaries`, plus obfuscation-resistant member
comparison through `compare_managed_members` and `rea compare-managed-members`,
managed/native export or function Evidence matching through
`verify_managed_native_boundaries` and `rea verify-managed-native-boundaries`,
decompiler reconstruction import through `import_managed_reconstruction` and
`rea import-managed-reconstruction`, and default-disabled runtime-correlation
admission planning through `plan_managed_runtime_correlation` and
`rea plan-managed-runtime-correlation`, plus static managed graph projection
through `project_managed_application_graph` and
`rea project-managed-application-graph`. Native-body bridge mapping and runtime
execution remain planned behavior. The current product inventory remains the
one in [`product-catalog.json`](product-catalog.json).
REA inspects .NET PE/CLI artifacts without loading or executing their code.
It can identify an assembly, inspect metadata and CIL, compare members across
builds, and connect declared native calls to supplied native-analysis evidence.
CIL is Common Intermediate Language, the instruction format used by managed
assemblies.
The seven shipped tools are:
| Task | MCP tool |
| --------------------------------------------------------------------------- | ----------------------------------- |
| Identify an artifact and its managed deployment form | `inspect_managed_artifact` |
| Inspect types, signatures, method bodies, calls, and field access | `inspect_managed_members` |
| List declared native calls and native implementation indicators | `inspect_managed_native_boundaries` |
| Compare members across builds and map build-local tokens | `compare_managed_members` |
| Check native call declarations against supplied export or function evidence | `verify_managed_native_boundaries` |
| Import decompiled code against verified static member identities | `import_managed_reconstruction` |
| Add managed findings to an application graph | `project_managed_application_graph` |
Each has a matching CLI command: replace underscores with hyphens and prefix
the name with `rea`, for example `rea inspect-managed-artifact`.
Native-body bridge mapping remains planned; managed runtime execution is not
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).
## Shipped scope
The canonical parser admits PE/CLI bytes and their metadata/CIL without loading
an assembly. It reports observed implementation markers and unavailable facts;
it does not unpack .NET single-file hosts, decode IL2CPP metadata, or infer a
NativeAOT identity from an ordinary native PE. Inputs without admitted CLI
metadata do not become managed assemblies through naming or routing guesses.
Inspect separately obtained components explicitly and preserve their identities.
The broader deployment classification and native-body mapping below are design
goals from ADR-0003, not claims that every row has an implemented parser. A
valid marker can establish a candidate native boundary, not recovered native
semantics or runtime behavior.
## Analysis objective
@@ -32,15 +51,15 @@ collapsing them:
3. What source-like or behavioral structure can be reconstructed or inferred?
4. Which findings survive an exact-build check or a cross-build structural
comparison?
5. Which remaining questions require native analysis or a separately
authorized runtime experiment?
5. Which remaining questions require native analysis or runtime evidence that
static inspection cannot supply?
The ordinary workflow ends at question four. Static analysis never loads or
executes the target.
## Classification workflow
## Planned deployment classification
Classification proceeds from the outermost authenticated bytes inward:
The intended extended classifier proceeds from outermost authenticated bytes inward:
```text
source path
@@ -53,7 +72,7 @@ source path
-> managed-only, native-only, composed, degraded, or unsupported route
```
The result is a vector rather than one label. For example, a single-file modern
The planned result is a vector rather than one label. For example, a single-file modern
.NET deployment can contain a native host, ordinary CIL assemblies, and
ReadyToRun components. Each component receives its own digest, classification,
coverage, and route while retaining the outer bundle commitment.
@@ -71,6 +90,51 @@ coverage, and route while retaining the outer bundle commitment.
| Obfuscated assembly | Same positive byte observations as its underlying deployment form | Meaning inferred only from names |
| Malformed/unsupported | Exact admitted regions and failure locations | Completeness, successfully skipped rows, or semantics beyond the admitted parser boundary |
### NativeAOT metadata recovery
NativeAOT removes the ordinary CIL method bodies, so a CIL decompiler cannot
reconstruct those bodies as C# source. The executable remains a native target
for Hopper or Ghidra, where decompilation produces pseudocode and analyst
inference. This is a different route from ordinary managed member inspection.
NativeAOT is not limited to Windows `.exe` files. The native image can be PE,
ELF, or Mach-O, and NativeAOT can also publish shared libraries (PE DLL, ELF
`.so`, or Mach-O `.dylib`) with explicitly exported entry points. Identify the
image from its bytes and loader metadata, not its filename extension. When the
image is inside an application bundle, inventory or extract the bundle first
and carry the component identity into native analysis. Matching PDB, ELF debug,
or dSYM sidecars can add symbol evidence when the selected provider supports
them; verify their identity and keep them separate from observations made from
the executable itself. [Microsoft's NativeAOT overview](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/)
lists supported OS/architecture targets and deployment limitations, while its
[native library guide](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/libraries)
describes exported shared-library entry points.
The [Ghidra NativeAOT analyzer](https://github.com/Washi1337/ghidra-nativeaot)
is a useful optional companion for recovering ReadyToRun metadata. It
rehydrates the `DEHYDRATED_DATA` section and annotates method tables, type
relationships, vtable slots, frozen objects, and strings. It does not recover
original C# method bodies. It is a separate Ghidra extension and interactive
metadata browser; REA does not currently install or invoke it through its
headless bridge. Its README describes ReadyToRun header discovery, including
symbol-based and heuristic paths, so a missed header or unsupported binary
should remain an explicit limitation rather than a failed claim about the
binary's contents.
The upstream analyzer currently gates its own analysis to x86-64. NativeAOT's
broader platform matrix does not establish REA coverage: PE/COFF, ELF, Mach-O,
shared libraries, target architectures, and runtime metadata versions each need
their own provider verification. REA's experimental Windows Ghidra boundary is
limited to admitted native x86-64 PE applications, so it does not currently
establish coverage for PE DLLs. Mobile NativeAOT targets should remain
experimental until REA has a matching provider and package-level verification.
For REA's native analysis, preserve the same artifact path and digest used by
`inspect_managed_artifact`, and report recovered type data as provider
observations. Linking those types to native function addresses requires verified
provider evidence; names or vtable similarity alone do not prove that a native
function implements a specific managed method.
## Evidence record shape
Every planned operation returns a provider result and Evidence with four
@@ -151,7 +215,7 @@ UTF-8 encoding, and lowercase hexadecimal output.
### Epistemic commitment
- authority: static bytes, reconstruction, structural inference, independent
validation, native provider, or separately authorized runtime;
validation, native provider, or future runtime observation;
- state: observed, inferred, unknown, or unavailable;
- confidence independent of authority;
- coverage and admitted/dropped counts;
@@ -216,31 +280,34 @@ The workflow produces separate observation, inference, validation, and unknown
tables. It may say that a method is a strong candidate for a role; it cannot
turn that role into the method's durable identity.
Cross-version matching is two-stage:
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.
1. Exact identity requires compatible artifact/module commitments and exact
raw CIL identity; no standalone complete-method-body digest is exposed.
2. Structural identity compares normalized signatures, the limited
decoded-CIL tuple fingerprint, constants, and bounded shape context. It
reports all candidates at the winning score and remains ambiguous when the
evidence does not distinguish them. The digest does not itself remap
metadata tokens.
For a matched method, an unavailable or partial body makes body-shape facets
unknown while preserving observed signature differences. Structural identity
compares normalized signatures, the limited decoded-CIL tuple fingerprint,
constants, and bounded shape context. It reports all candidates at the winning
score and remains ambiguous when the evidence does not distinguish them. The
digest does not itself remap metadata tokens.
Tokens are always remapped through observed structure. A caller cannot carry
`0x06001234` into a new MVID and assume it names the same method.
## Managed/native boundary rules
| Boundary | Managed observation | Native observation | Permitted link |
| ----------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| P/Invoke | Module, entry point, charset/calling-convention flags, declaring method | Import/export/symbol/function evidence from the selected deep provider | Exact declared-name/module link or qualified resolution inference |
| COM | Interop attributes, GUIDs, imported interfaces, method signatures | Native registration/vtable evidence when independently available | Identifier/signature inference with explicit environment limitations |
| C++/CLI | Managed declaration and implementation flags | Native body/function evidence | Only a provider-supported bridge observation; never token-as-address |
| ReadyToRun | Component/header and per-method CIL/native availability | Native section/function evidence | Authenticated image mapping with format/profile version |
| Unmanaged export | Export metadata/attribute when present | PE export and native thunk/function | Exact export identity plus provider-qualified address |
| Single-file host | Bundle entry/component identity | Host and native component evidence | Outer bundle plus component digest/extent commitment |
| Unity IL2CPP | Supported metadata entity with authenticated pairing | Generated native function/type evidence | Versioned IL2CPP mapping only; no invented CIL |
| Runtime-resolved native | API/constant/data-flow candidate | Loaded-module/symbol observation only under separate runtime execution authority | Static candidate remains inference until separately observed |
| Boundary | Managed observation | Native observation | Permitted link |
| ----------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| P/Invoke | Module, entry point, charset/calling-convention flags, declaring method | Import/export/symbol/function evidence from the selected deep provider | Exact declared-name/module link or qualified resolution inference |
| COM | Interop attributes, GUIDs, imported interfaces, method signatures | Native registration/vtable evidence when independently available | Identifier/signature inference with explicit environment limitations |
| C++/CLI | Managed declaration and implementation flags | Native body/function evidence | Only a provider-supported bridge observation; never token-as-address |
| ReadyToRun | Component/header and per-method CIL/native availability | Native section/function evidence | Authenticated image mapping with format/profile version |
| Unmanaged export | Export metadata/attribute when present | PE export and native thunk/function | Exact export identity plus provider-qualified address |
| Single-file host | Bundle entry/component identity | Host and native component evidence | Outer bundle plus component digest/extent commitment |
| Unity IL2CPP | Supported metadata entity with authenticated pairing | Generated native function/type evidence | Versioned IL2CPP mapping only; no invented CIL |
| Runtime-resolved native | API/constant/data-flow candidate | Loaded-module/symbol observation from an independently collected runtime source | Static candidate remains inference until separately observed |
Declared import inventory supports a bounded positive claim. Its absence does
not exclude dynamic resolution, generated code, protected code, native helpers,
@@ -249,17 +316,20 @@ or server-side behavior.
## Tool and packaging boundary
The production parser is REA-owned TypeScript and ships with the existing Node
application. It is verified against the ECMA-335 format and independent pinned
oracles but has no runtime dependency on them.
application. Deterministic conformance uses source-owned byte-built PE/CLI
fixtures and expected semantic facts. An optional real ILSpy lane runs only when
its executable is explicitly supplied; the default lane does not establish
independent pinned-oracle parity. Production inspection needs none of those tools.
- `System.Reflection.Metadata` is the primary independent metadata/CIL oracle.
- `ICSharpCode.Decompiler` and `ilspycmd` are reconstruction and differential
oracles. An admitted BYO reconstruction operation records its exact version
- A pinned `System.Reflection.Metadata` differential oracle is planned; it is
not part of the current verifier.
- `ICSharpCode.Decompiler`/`ilspycmd` can supply reconstruction inference.
A BYO reconstruction import records the supplied version
and remains non-canonical. When `REA_ILSPY_CMD_PATH` points to an absolute
runnable `ilspycmd`, `verify:managed` runs a source-owned real ILSpy oracle
and imports its C# output as reconstruction inference against exact static
member Evidence.
- dnlib and Mono.Cecil may increase differential coverage. Their mutation APIs
- dnlib and Mono.Cecil are potential future differential oracles. Their mutation APIs
are not exposed or included in the production parsing boundary.
- The package contains no .NET runtime, SDK, ILSpy installation, proprietary
assembly, or compiled conformance fixture.
@@ -280,6 +350,11 @@ by the selected Hopper/Ghidra host and format matrix.
## Source-built conformance corpus
The current lane uses source-owned byte-built fixtures and malformed-input
regressions, with optional operator-supplied applications and ILSpy verification.
The table below describes the desired expanded corpus; it is not a report that
all compiler-generated forms have been independently verified.
Fixture sources are intentionally small and behavior-focused. Build outputs
are generated outside tracked fixture directories and must not remain after
verification or packaging.
@@ -397,25 +472,13 @@ The benchmark may prove that REA reproduces selected facts for that exact local
build. It cannot establish general support on its own; source-built conformance
remains the admission requirement.
## Runtime admission boundary
## Runtime behavior boundary
The later runtime track is not an extension flag on a static operation. Its
design must expose whether it attaches to an existing process, launches a new
process, loads an assembly, uses reflection, installs a debugger/profiler, or
instruments code. Each effect needs separate approval and exact-build checks.
At minimum, admission requires target SHA-256, MVID, normalized signature and
body/CIL commitment, CLR family, OS, architecture, tool version, scenario
limits, output policy, and cleanup ownership to match. Any mismatch fails before
the experiment. It must not contact real services/accounts or claim that an
instrumented path represents an ordinary launch unless that proposition is
independently tested.
The shipped `plan_managed_runtime_correlation` path admits only a
default-disabled, permission-gated experiment plan and records that no target
code was executed. Until a separate executor is designed and shipped, runtime
behavior questions remain explicit unknowns with suggested probes; static
support does not perform them.
Managed inspection does not execute assemblies or observe CLR internals.
Questions about actual behavior remain unknown until supported runtime
evidence is collected. A direct process capture can retain declared inputs,
outputs, filesystem and protocol activity for a target run; it does not prove
which managed methods executed or how the CLR resolved them.
## Delivery sequence
@@ -437,9 +500,15 @@ The managed-code track advances as reviewable pull requests:
PE/CLI corpus shipped through `npm run verify:managed`; optional BYO
`ilspycmd` real-tool oracle shipped through `REA_ILSPY_CMD_PATH`; dnSpy and
pinned Windows checks remain planned); and
8. separately authorized runtime-correlation admission planning (shipped; no
runtime execution);
9. managed static Evidence projection into the application graph (shipped).
8. managed static Evidence projection into the application graph (shipped).
Each implementation PR updates generated product facts only for behavior it
actually ships and states which real-tool checks were performed.
### Method metadata during build comparison
Managed member comparisons report a `metadata` dimension when an observed
MethodDef flags or implementation flags value differs, including accessibility
or synchronization changes. This is separate from the CIL/signature matching
tiers: identical instructions do not establish unchanged method metadata. The
reported difference is static metadata evidence, not proof of runtime behavior.
+160 -62
View File
@@ -8,16 +8,18 @@ registrations, reports their command vectors as aligned, stale, missing, or
invalid, and keeps live-server state `unknown` unless the active connection
supplies identity.
Canonical tool names remain stable, while `tools/list` advertises only the
operations callable for the current target, provider, policy, host, and
negotiated client capabilities. `binary_session.tool_availability` remains the
complete inventory: it explains advertised and hidden operations with stable
availability reasons and remediation. Each entry also reports required and
optional negotiated client features plus the currently missing features. Form elicitation is optional for
`capture_process_scenario`: an existing grant remains usable without it, while
a missing grant cannot be elicited by a client that does not advertise it.
Opening or closing a target, reloading policy, or observing a provider health
transition emits `notifications/tools/list_changed`.
`tools/list` returns the complete canonical tool inventory, including tools that
are currently unavailable. Opening or closing a target or observing a provider
health transition leaves that catalog unchanged and does not emit
`notifications/tools/list_changed`.
Call `binary_session` with `{}` and read `result.tool_availability` to choose a
callable operation for the current target, provider, host, and negotiated client
capabilities. The default result includes the complete inventory with each
tool's availability, reason, and remediation. Each entry also reports required
and optional negotiated client features plus the currently missing features.
The optional inputs `expected_package_version`, `expected_catalog_digest`, and
`expected_server_path` compare the live session with the caller's expectations.
`binary_session.analysis_provider_candidates` is authoritative for deep-engine
discovery. Target-free discovery is sorted by provider ID, reports host
@@ -55,12 +57,126 @@ CLI calls work without a progress token and translate SIGINT into the same
AbortSignal used by providers. Existing controlled-process cleanup and provider
shutdown rules still apply; REA never kills a process it cannot prove it owns.
## Ghidra first-query deadlines and recovery
A successful MCP initialize handshake establishes the REA connection.
`open_binary` then selects a target and provider binding; it does not establish
that Ghidra has finished importing the target. The first Ghidra-backed query,
such as `binary_overview`, starts the engine and waits for import, default
auto-analysis, bridge connection, and health readiness before returning analysis.
These deadlines have different owners:
| Deadline | Owner and effect |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| MCP initialize | The client bounds transport/REA connection startup, before any target query. |
| Ghidra startup | REA allows 330,000 ms for engine readiness from the first provider query; startup failure is reported by that query. |
| Individual tool call | The client bounds its wait, including cold engine startup. The pinned client SDK 2.3.1 defaults to 60,000 ms and can cancel earlier than REA's startup deadline. |
For an already connected client using the pinned SDK, request options are the
**second** argument of `callTool`:
```js
const overview = await client.callTool(
{ name: "binary_overview", arguments: {} },
{
timeout: 240000,
onprogress: ({ progress, total, message }) => {
console.error({ progress, total, message });
},
},
);
```
The SDK's `onprogress` option supplies a progress token and handles matching
notifications. A raw MCP client can instead send `_meta.progressToken` in the
request and handle `notifications/progress`. Progress reports do not establish
engine readiness or a percentage of Ghidra auto-analysis. A longer server
startup allowance does not extend the client's deadline; progress also does not
extend it unless the client explicitly implements that policy.
The 240-second setting is a measured example, not a universal timeout: a
controlled Linux x64/WSL2 fixture took about 64 seconds for its first overview
with Ghidra 12.1.4 on a mounted NTFS installation, native JDK 21, two CPUs, and a
512 MiB Java heap. Target size, storage, and analysis work can change that time.
Configure the deadline in the caller's existing request settings; it is not a
tool argument, setup grant, or server-side automatic extension. CLI analysis has
no MCP client request deadline, but keeps the provider startup deadline and
SIGINT cancellation.
If the client timed out or cancelled during startup, the pending query may have
been interrupted. Reissuing `open_binary` for the same active target can reuse
the current client, so it is not a fresh-start recovery. On the same connection:
1. Keep any useful inline Evidence, or export a bundle before closing if retained
records are needed. `binary_session` can show the selected binding and its
recorded state; target selection alone does not prove engine readiness.
2. Call `close_binary` and check its result. It drains owned work, closes the
provider, and clears retained session records. A `cleanup_incomplete` result
must be addressed according to its reported owned resources before retrying.
3. Call `open_binary` with the same caller-selected path and
`provider_id: "ghidra"`, then retry the first query with an appropriate client
deadline. The connection and selected provider do not need to change.
This close/reopen flow was exercised on one real Linux stdio connection after a
controlled startup timeout, followed by a successful overview and function
analysis. It is not a Windows/macOS coverage claim. REA cleans only resources it
owns and never switches to another provider automatically. A provider timeout,
installation failure, or host permission denial needs its own reported recovery;
increasing a client deadline alone does not fix those failures.
## Tool results
Evidence-producing tools return their result and Evidence context inline. Read
that result directly. Use `get_evidence_bundle` when the task needs broader
Evidence-producing tools return `{ result, evidence_id, evidence }` in both
text and structured content. `evidence` is the complete canonical Evidence
record, including `normalized_result`, which equals `result`. The same record
is retained in the session bundle. Read `result` directly, or pass `evidence`
to a compatible comparison tool: `analyze_function` Evidence can be passed
directly to `compare_functions`, and `inspect_artifact` Evidence to
`compare_artifacts`. Use `get_evidence_bundle` when the task needs broader
retained session history or an explicit bundle for transfer.
## Retained application Evidence inputs
`trace_application_feature`, `trace_javascript_semantics`,
`compare_application_versions`, `compare_source_to_bundle`, and
`compare_javascript_export_shapes` accept complete inline application Evidence
or an exact reference to a record already retained by the current connection:
```json
{
"name": "trace_application_feature",
"arguments": {
"application": {
"kind": "retained-evidence",
"evidence_id": "ev_<64 lowercase hex characters>"
},
"seed": { "kind": "module", "value": "search.js", "match": "exact" }
}
}
```
Use the `evidence_id` returned by `analyze_javascript_application` (or another
compatible application-graph producer). Comparisons accept this form in `left`
and `right`; each side can independently be inline or retained. Native
observation arrays continue to take complete inline Evidence. Results and their
Evidence remain complete inline, and both input forms pass the same semantic,
identity, authority, and provenance checks without running the producer again.
References belong to the current connection's Evidence ledger. Opening another
target preserves retained records; `close_binary` clears them, even when no
binary is active. A fresh connection has its own ledger. A missing reference
reports its exact ID and `details.reason: "missing"`; the server cannot infer
whether it was never recorded, cleared, or retained by another connection.
Supply complete inline Evidence, repeat its producer, or import an exported
Evidence bundle before referencing that imported record. Export a bundle before
closing if the investigation needs it later.
CLI application workflows continue to read portable inline Evidence from files
and run the same analysis workflows; a standalone CLI invocation cannot resolve
another MCP connection's retained records. No additional lookup call, provider
selection, or approval step is required for a same-session follow-up.
## Aggregate native context
`get_navigation_context` composes the selected document, current address, and
@@ -74,65 +190,47 @@ unsupported facets are local `unavailable` outcomes. Use `current_document`,
`current_address`, and `current_procedure` for direct single-field lookups; use
the aggregate tools when you need related context together.
## Permission policy
## Request scope and local effects
All local side effects use one scope vocabulary: capability, canonical roots,
executables, environment variable names, network mode, mount permission, exact
operation identity, and grant lifetime. Environment values and captured content
are never part of a grant or denial.
Each tool request names the target and lifecycle it will use. Browser calls
carry a loopback CDP endpoint and target ID, with optional origin filters;
process and Electron scenarios carry the executable, arguments, actions, and
cleanup behavior; artifact tools carry the input path and requested operation.
REA runs the declared request directly and does not infer a broader target or
action from it.
Existing environment settings map to administrator ceilings. They remain the
maximum authority:
REA does not require permission grants or per-call approval flags. Setup still
prints its plan and requires confirmation before changing configuration or
installing Hopper. MCP clients control their own confirmation UI.
- process roots, executables, environment names, and external networking;
- native mount enablement.
Tool annotations describe effects and are hints, not authorization controls
([MCP ToolAnnotations](https://modelcontextprotocol.io/specification/2025-06-18/schema#toolannotations)).
`readOnlyHint` includes session state: an analysis call that records additive
Evidence is marked non-read-only even when it leaves the target unchanged.
`destructiveHint` describes possible data loss, not ordinary Evidence recording.
Effect metadata covers possible behavior across supported inputs: DMG inventory
can launch `hdiutil` and create an owned temporary mount directory; extraction
creates a fresh output directory on every call. Comparing supplied web captures
or PNG artifacts uses local data without contacting the browser.
Evidence bundles and analysis snapshots use the path supplied to the CLI or MCP
tool; they do not require configured roots or separate read/write grants.
Host requirements remain in force. macOS may deny Accessibility,
Screen Recording, or native mounting; provider tools require their selected
analysis runtime. These failures are reported at the operation that needs
them. A configured provider or an endpoint alone does not establish that a
target is supported.
Artifact extraction takes no path or occurrence selectors. It materializes all
regular files into a fresh REA-chosen temporary directory. The write still
passes through the active permission evaluator; the tool caller does not choose
an output path.
Evidence bundles, snapshots, and extraction use the paths and output behavior
declared by their tools. Artifact extraction materializes the selected regular
files into a fresh REA-chosen temporary directory and reports its path.
`rea policy status`, `list`, `explain`, and `revoke` inspect the same evaluator
used by MCP. Optional project grants require both
`REA_PERMISSION_PROJECT_ROOT` and `REA_PERMISSION_PROJECT_STORE`. The store is
atomically written with mode `0600`, bound to the canonical project root, and is
never enabled by default. Send `SIGHUP` to the REA MCP process after a trusted
project-store change; project grants reload without restarting. Environment
settings belong to the process environment, so changing an administrator
ceiling in an MCP registration requires restarting that registered server or
its owning client.
Revocation affects future operations; an already-running operation retains the
decision made at its preflight boundary.
Once and session grants are connection-local overlays. They cannot authorize a
second MCP connection, be consumed by another connection, or be cleared when a
different connection closes. Live connections evaluate those overlays against
the current process-wide ceilings and persisted grants, including successful
SIGHUP reloads.
`rea policy revoke <grant-id>` displays the exact grant and requires interactive
confirmation; automation must pass `--yes` (or `-y`) explicitly.
Denials use the shared `permission_required` schema with requested scope, missing
scope, administrator ceiling, elicitation support, and exact restart status.
Client-provided roots are context only and never grants.
Elicitation can add a once or session grant only inside an existing
administrator ceiling. A request outside that ceiling reports
`elicitation_supported: false` and `restart_required: true`; interactive consent
cannot silently widen administrator policy.
`analyze_javascript_application` reads the supplied local directory or ASAR path directly and returns its result and Evidence inline; no approval flag or root configuration is needed.
`analyze_javascript_application` reads the supplied local directory or ASAR path
and returns its result and Evidence inline.
## Integrity record-and-continue
Artifact integrity remains fail-closed by default. Record-and-continue requires
all three conditions:
1. operator policy `REA_ARTIFACT_INTEGRITY_CONTINUE_ENABLED=true`;
2. `integrity_policy=record-and-continue`;
3. explicit per-call `integrity_continue_approved=true`.
Artifact integrity fails closed by default. A request can explicitly select
`integrity_policy=record-and-continue` when the investigation needs verified
siblings to continue after a mismatch.
Contradictory bytes are quarantined from nested expansion and recorded with
declared and observed hashes, trust, provenance, path, and unpacked state.
+11 -6
View File
@@ -13,7 +13,7 @@ invoke tools by themselves.
| `verify_reconstruction` | Evaluate a finite reconstruction specification against retained comparison Evidence. |
| `trace_crash` | Correlate a crash symptom with static paths and optional process capture evidence. |
| `audit_residual_unknowns` | Audit current residual-unknown heads and evidence-qualified resolution. |
| `prepare_bounded_process_capture` | Design an approval-gated controlled process experiment before execution. |
| `prepare_bounded_process_capture` | Design a bounded controlled process experiment before execution. |
Every rendered prompt provides optional starting points using current REA tool
names. Agents can call tools directly, skip irrelevant suggestions, and inspect
@@ -25,6 +25,13 @@ Tool results include their Evidence inline. Prompts do not require a bundle or
resource fetch to read a result; an Evidence ID is only a reference when a
prompt argument or tool explicitly accepts one.
For a JavaScript/Electron application directory, `investigate_feature` points
directly to `analyze_javascript_application` with `input_path`; it does not
require `open_binary`. The CLI equivalent is `rea analyze <directory>`.
`open_binary` and the doctor target check admit files and macOS app bundles.
A directory outside that opening route is reported with an available analysis
action; this does not establish support for every directory format.
For target paths in `compare_application_versions`, the prompt suggests
opening each target before calling `inspect_artifact`; inspection operates on
the active target and returns the graph and findings together inline.
@@ -108,8 +115,6 @@ session are no longer suggested. Provider errors, unsupported operations, and
malformed provider output produce an empty completion list rather than an
unverified identifier.
Suggestions never grant permission. In particular, selecting an artifact
occurrence does not authorize extraction, selecting an unknown does not
authorize mutation, and selecting a prior capture does not authorize process
execution. The corresponding tools retain their existing approval, policy,
effect, and validation boundaries.
Suggestions are starting points, not tool calls. A tool acts on the target,
operation, and lifecycle fields in its own request; selecting a suggestion does
not execute it or broaden those fields.
+168 -67
View File
@@ -1,78 +1,179 @@
# Native UI and dispatch investigation
REA combines decoded application resources, provider metadata, and native code
analysis into evidence-linked investigation graphs. Each layer has a different
coverage boundary; a graph edge is not automatically proof that a runtime path
executes.
REA joins static resources, native metadata and code facts while keeping runtime
observations separate. Every result reports its evidence, target identity,
coverage and unknowns. The CLI and MCP use the same application workflows.
## Current workflow
## Static inspection
1. `decode_interface_builder` reads compiled Apple storyboard and nib resources
from an artifact graph and returns scenes, objects, outlets, actions,
connections, and available controller/class names. A source-owned AppKit
XIB is compiled by `ibtool` in `verify:interface-builder` and checked for
its hierarchy and target/action edge. Storyboard compilation still depends
on an installed iOS platform; unsupported archive objects remain explicit.
2. `inspect_native_dispatch_metadata` normalizes supported provider name
inventory into typed Objective-C and Swift records. Current extraction is
symbol-name based: symbolized Objective-C method implementations and
mangled Swift names can be reported as partial evidence. It does not yet
decode Objective-C runtime sections, ivar layouts, protocol conformance
records, Swift witness tables, vtables, relative pointers, or closure
thunks from binary metadata.
3. `trace_native_ui_action` accepts one selector or Interface Builder object
ID. REA decodes the active app and reads names from the bound native provider,
verifies that both observations refer to the same target digest, joins an
authored action to a symbolized Objective-C implementation, and follows
bounded direct callees. Ambiguous selectors and unresolved calls remain
explicit. This is a static candidate route, not proof of runtime reachability;
it does not resolve Swift witnesses, vtables, closures, or cross-function
value flow yet. The CLI uses the same query: `rea trace-native-ui-action
<app-or-binary> <selector-or-object-id>`.
`inspect_native_dispatch_metadata` and `rea inspect-native-dispatch-metadata`
expose the provider's typed symbol-derived inventory separately.
4. Ghidra `analyze_function` returns a bounded high-p-code def-use view for the
selected function. This can support manual state/value analysis, but REA
does not yet compose those views into an automatic cross-function slice.
`inspect_macho` retains otool segment `file_offset` values relative to the
selected Mach-O slice. Its evidence file-offset ranges address the original
input file and include the observed lipo slice offset for universal binaries.
If that offset is unavailable, segment evidence locations are omitted with an
explicit limitation; architecture inventory locations remain available.
## Ghidra p-code result boundary
- `inspect_asset_catalog` / `rea inspect-asset-catalog <app>` reads compiled
`Assets.car` metadata through macOS `assetutil --info`. Catalog digests, raw
rendition fields, pagination and exact UI resource-name matches are returned.
Image extraction and undocumented rendition interpretation are unsupported.
- `inspect_keyed_archive` / `rea inspect-keyed-archive <plist>` reads XML or
binary Foundation keyed archives without instantiating classes. For an active
app, provide its relative archive path as the second CLI argument or MCP
`path`. Results retain original `$objects` indices, named `$top` roots,
shared/cyclic references, class descriptors, raw serialized fields and
malformed/unresolved/nil references. Missing fields stay absent; unknown
classes are preserved. Select `root`, `offset` and `limit` explicitly.
UID 0 cannot distinguish conditional nil from ordinary nil. Limits are
64 MiB of input, 200,000 objects/references and 128 nesting levels.
- `decode_interface_builder` / `rea decode-interface-builder <app>` reads
compiled storyboard and nib resources into objects, outlets, actions,
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
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
witness slots, signed-relative pointers and non-generic, non-resilient class
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
existing symbol-based inventory with its narrower coverage.
The dossier retains up to 3,000 p-code operations, up to 64 inputs per
operation, and 12,000 def-use edges. Each operation carries its address,
sequence, opcode, input/output varnodes, and whether Ghidra marks it dead.
Memory reads/writes, conditional and indirect branches, and direct/indirect
calls are reported by opcode with operand indexes. Results expose truncation,
an omitted-operation lower bound, and known omitted-input and omitted-edge
counts.
## Native instruction, call and type primitives
These are decompiler-derived static observations. Def-use edges stop at a
function boundary; memory aliasing, call side effects, unresolved dispatch,
runtime behavior, and application-level meaning require additional evidence.
A p-code `STORE` can describe stack memory and does not by itself identify
persistent application state.
Ghidra supplies three exact-object operations:
Recovered jump-table load sources and case-to-target mappings are reported as
separate facts. On AArch64, REA additionally decodes the verified byte-indexed
relative branch-table form when the selector's unsigned upper bound, `LDRB`
table load, `ADR` branch base, scaled `ADD`, indirect `BR`, and recovered
target set agree. It reads only the proven entry count and reports one direct
mapping per selector value. Other layouts retain unknown case values and
unknown table bounds when Ghidra does not provide aligned labels or load-table
metadata; REA does not extrapolate from neighboring targets.
```bash
rea inspect-native-instruction <binary> <address> --provider ghidra
rea resolve-native-call-targets <binary> <call-site> --provider ghidra
rea inspect-native-data-type <binary> --type /MyStruct --provider ghidra
rea inspect-native-data-type <binary> --address <typed-data-address> --provider ghidra
```
## Provider admission and remaining work
Instruction facts include bytes, length, decoder text, ordered register/scalar/
address tokens, typed references and flow destinations. Mid-instruction, data,
outside-memory and undecodable addresses have separate outcomes. Effective
memory base/index/displacement roles and per-instruction context mode remain
unavailable; `mode` is the program language variant.
Ghidra is admitted on Linux x64; the experimental Windows x64 P0 is limited to
approved native PE applications. macOS is not an admitted Ghidra host. The
optimized AArch64 ELF byte-table fixture has a separate lane so its acceptance
does not depend on PE or Mach-O cross-linkers.
Call resolution reports direct, resolved indirect, ambiguous, unresolved and
non-call outcomes from static call references. It does not establish runtime
execution or classify Objective-C/Swift/vtable/closure mechanisms from names.
The native feature set is still partial. Objective-C/Swift metadata currently
uses symbol names rather than runtime sections, witness tables, vtables, or
relative pointers. UI action traces do not yet resolve Swift dispatch or
closure/thunk routes. Value dependencies remain intra-function, and the
AArch64 switch lane currently proves one optimized byte-indexed ELF layout;
sparse, stripped, Mach-O, and other table patterns remain unknown. The local
Xcode installation has no iOS simulator platform, so compiled storyboard
acceptance could not run here. These boundaries should stay visible until
matching provider-backed readers and fixtures exist.
Type inspection selects one exact database pathname or typed data address.
Struct/union fields, enums, pointers, arrays, typedefs, size, alignment, packing
and bitfields retain database authority. Child types use exact IDs for further
inspection, including recursive layouts. Source/debug authority and flexible
array semantics are not inferred. The real lane uses a native DWARF 4 object
with a struct containing a union, enum and recursive pointer; a stripped linked
binary may have no corresponding recovered layout.
## Bounded static traces
`trace_native_ui_action` / `rea trace-native-ui-action <target> <seed>` accepts
one authored selector/object ID, native symbol or exact function address. It
joins UI wiring to uniquely matched class/selector implementations and follows
bounded typed call references. Direct references, resolved indirect references,
ambiguous candidates, inferred untyped provider callees and targetless call
sites remain distinct. A static route does not establish runtime reachability.
Swift/closure dispatch requires resolved static references; unavailable ABI
forms remain unknown.
`trace_native_values` / `rea trace-native-values <binary> <procedure> --provider
ghidra` composes high-p-code def-use graphs across statically resolved calls.
Logical CALL arguments bind to recovered parameter ordinals; recovered RETURN
values bind to CALL outputs. These edges are decompiler-derived. Constants,
operators, LOAD/STORE and branch operands are included inline. Alias effects,
persistent state, RNG roles, missing/variadic bindings and ambiguous destinations
remain unknown. Budgets control depth, decompilations, call-site resolutions,
nodes and edges. Node payloads are bounded to 8 MiB and serialized results to
32 MiB. Pagination returns edges whose source is on the node page and preserves
stable IDs for endpoints outside that page.
The raw Java `is_dead` flag can remain set after Ghidra decodes a live block;
`block_membership` reports actual syntax-tree membership separately.
The provider dossier itself retains up to 3,000 p-code operations, 64 inputs per
operation and 12,000 def-use edges, with explicit omitted counts. A `STORE`
may describe stack memory and does not itself prove persistent state.
Jump-table evidence keeps numeric `mappings`, explicit `default_targets`, and
backing `data_sources` separate. A null case value denotes an unresolved
case, never a known default. Ghidra pairs typed case/default tokens with the
recovered block entry and its unique indirect dispatch predecessor; it does
not zip unequal label and destination arrays. Shared case bodies retain each
label. Negative numeric tokens are checked against their encoded magnitude;
nonliteral, ambiguous, or nonexact JSON integer labels remain unknown.
Unknown-label diagnostics retain the token text, encoded unsigned magnitude,
target and dispatch so callers can inspect the original observations.
Decompiler load-table metadata supplies observed entry sizes and counts
without assigning every backing table to each mapping. These observations
describe the decompiler's recovered switch, not guaranteed original source.
Legacy records without `default_targets` normalize to an empty array and
retain their existing unresolved mappings.
AArch64 jump-table recovery additionally verifies byte and halfword relative
forms from unsigned bounds, register definitions, table loads, branch bases,
scaled ADD/BR instructions and the recovered target set. It reads exactly the
proven count and preserves unknowns for other forms. Real ELF and host ARM64
Mach-O fixtures check each case against source-owned return values.
## Native desktop observation
`observe_native_ui` captures one explicitly selected existing PID/window ID.
Screenshots use a selected-window ScreenCaptureKit
filter on macOS 14+; accessibility reads stay within that window. Missing Screen
Recording/Accessibility permissions produce actionable errors without broad
capture or automatic permission prompts. Executable bytes and process launch
time guard against a different target or PID reuse. AX selection requires one
unique geometry match; ambiguity fails closed.
`capture_native_ui_scenario` takes AX child-index paths for press, increment/
decrement scrolling and text-value entry, or bounded waits. No global event
injection is used. Unsupported AX actions fail explicitly. The result preserves
ordered before/after captures and gaps; an action may have occurred before a
post-action capture fails. Application state is left as-is; REA does not attempt
to restore it.
Scenarios accept caller-selected action lists and accessibility node counts.
Individual waits cannot exceed the operation's 180-second deadline, and the
complete scenario result has a 64 MiB output budget. Screenshots are scaled to
at most 2,048 pixels and captured only for the selected window. REA compiles one
owned helper per scenario, removes its temporary compiler cache and stops its
helper on cancellation. It does not launch or own the selected application. UI
actions may change application data or trigger network activity.
## Provider and verification boundaries
Install Ghidra 12.1.4 and a full 64-bit JDK 21 separately, 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,
protected runtime DACLs, and handle-based path admission. See [Windows Ghidra P0](windows-ghidra-p0.md) and
[issue #527](https://github.com/morluto/rea/issues/527).
On Linux and macOS, `annotate_native_function` atomically edits a function name
and entry comments in the ephemeral database, returning refreshed analysis
without changing executable bytes. Windows P0 remains read-only. Ghidra has no
GUI authority, and REA never falls back automatically to Hopper.
- `npm run verify:ghidra`: host-native debug/stripped targets, native type layout,
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:native-ui`: one source-owned fixture window, successful
selected-window capture and actions, changed-target rejection, and cleanup.
Missing OS permissions fail this lane.
- `npm run verify:native-ui:permissions`: permits a permission-denial result and
reports `positive_e2e: false` when capture is denied. That result verifies the
OS permission boundary, not successful UI capture or actions.
macOS ARM64 is the real host verified during this implementation. Admission of
macOS Intel does not claim an Intel verification run. Unsupported metadata and
unresolved runtime/value semantics remain visible in results.
+86 -242
View File
@@ -1,79 +1,57 @@
# Process capture
Process Capture records one approved command as deterministic Evidence. It is
intended for authority-versus-reconstruction checks where terminal behavior,
child lifetime, filesystem state, or dependency timing matters.
Process Capture runs one caller-selected command and records bounded evidence
about its terminal behavior, scheduled interactions, process lifetime, and
selected filesystem state. Use it to compare two direct runs of the same
scenario, such as an authority build and a reconstruction. It does not replay a
previous execution or emulate dependencies.
The harness records both raw PTY chunks and terminal states rendered by xterm.
It also records scripted input, resize and signal delivery, sampled process
metadata, named filesystem checkpoints, command-shim invocations, loopback
HTTP/WebSocket exchanges, and process exit ownership. Missing or bounded
observations remain explicit; a truncated capture is never treated as
equivalent to another capture.
The command runs with the current user's permissions and inherits the host
environment before applying the scenario's explicit overrides. This is not a
sandbox. Filesystem observation paths select what REA snapshots; they do not
restrict what the process can read or write. The inherited environment is not
recorded, so its influence may remain unknown.
Every capture includes a run manifest with canonical SHA-256 commitments for
the secret-safe full scenario projection, comparison contract, executable,
normalization rules, command-shim plan, and replay plan. The manifest also
records the REA/provider versions, platform, architecture, PTY backend, and UTC
start/completion timestamps. Capture creation, Evidence import, and comparison
recompute the self-contained commitments and reject invalid lifecycle data.
## Host support
## Enable the capability
Capturing a new scenario currently requires Linux or macOS and a working
native PTY backend. Native Windows capture is unavailable because the PTY
adapter does not yet verify descendant cleanup. The native Job Object controls
used by other REA providers do not establish PTY capture support. Reinstalling
the Windows PTY binary does not enable this workflow.
Process execution is disabled until the operator supplies policy roots and
approves host networking:
For Linux commands, use Linux REA inside WSL. Adapt the scenario to that host;
this does not establish capture of a native Windows process tree. Comparing
existing capture Evidence through the CLI or MCP remains available on Windows
and does not launch a PTY or the captured target.
```bash
export REA_PROCESS_CAPTURE_ENABLED=true
export REA_PROCESS_EXECUTABLE_ROOTS_JSON='["/usr/bin","/absolute/project/bin"]'
export REA_PROCESS_WORKING_ROOTS_JSON='["/absolute/project"]'
export REA_PROCESS_ALLOWED_ENV_JSON='["PATH"]'
export REA_PROCESS_ALLOW_EXTERNAL_NETWORK=true
On supported capture hosts, a missing or incompatible native PTY binary has a
different recovery: reinstall REA for the active platform, architecture, and
Node.js version with optional dependencies enabled. Capability diagnostics
distinguish this from the Windows capture-adapter limitation.
## Capture a command
Write a JSON scenario and pass its path to the CLI:
```sh
rea capture-process ./scenario.json --json > capture.json
```
By default, enabling process capture also installs an administrator-lifetime
grant for the configured boundary. Set
`REA_PROCESS_CAPTURE_AUTO_GRANT=false` to install the same boundary only as an
administrator ceiling. Captures then fail with a structured permission-required
result until a narrower grant is established; this is the prerequisite mode for
connection-scoped MCP elicitation. It does not itself authorize a capture.
Each MCP connection owns its elicited once and session grants. A grant accepted
on one connection is not visible to another connection, and disconnect cleanup
removes only the grants owned by the connection that closed. Reloaded
administrator ceilings and persisted grants still apply immediately to every
live connection.
`--json` is required when saving input for JSON consumers; the default terminal
format is TOON. The file contains the complete capture Evidence record.
Choose an output file distinct from the scenario input: shell redirection opens
and truncates the output before REA reads the scenario.
REA offers the once-or-session grant form only after the connection negotiates
an MCP revision with multi-round tool results and advertises form elicitation.
Older connections keep returning the structured
permission-required result and never wait for an unsupported elicitation
response. On the pinned `@modelcontextprotocol/server` 2.0.0 runtime, this is
the official `2026-07-28` `inputRequired` continuation: the SDK seals the
request state and validates the continuation envelope, while REA validates the
untrusted input response before creating a connection-owned grant. The older
push-style `elicitation/create` path is not used for this protocol revision.
Executables, working directories, filesystem roots, and requested environment
variables are checked against operator policy before launch. Process Capture is an observation tool,
not a security sandbox: the target runs with the current user's permissions.
## Capture a scenario
`rea capture-process` reads a JSON scenario and writes process capture
Evidence to stdout:
```bash
rea capture-process ./scenario.json > capture.json
```
A representative scenario is:
For example:
```json
{
"executable": "/absolute/path/to/node",
"executable": "node",
"arguments": ["./signup.mjs"],
"working_directory": "/absolute/project",
"filesystem_roots": ["/absolute/project/state"],
"working_directory": ".",
"environment": { "APP_MODE": "test" },
"filesystem_observation_paths": ["./state"],
"terminal": { "columns": 80, "rows": 24, "scrollback": 1000 },
"events": [
{
@@ -82,197 +60,63 @@ A representative scenario is:
"data": "user@example.test\r",
"sensitive": true
},
{ "type": "resize", "at_ms": 500, "columns": 100, "rows": 30 }
{ "type": "resize", "at_ms": 500, "columns": 100, "rows": 30 },
{ "type": "signal", "at_ms": 1000, "signal": "SIGINT" }
],
"checkpoints": [
{
"name": "signup_ready",
"trigger": { "type": "terminal_literal", "value": "All set!" }
},
{ "name": "root_exited", "trigger": { "type": "root_exit" } }
],
"command_shims": [
{
"name": "codex",
"routes": [
{
"arguments": ["--version"],
"outputs": [
{ "at_ms": 0, "stream": "stdout", "data": "codex 1.2.3\n" }
],
"termination": { "type": "exit", "code": 0 },
"max_calls": 1
}
]
}
]
"timeout_ms": 30000,
"idle_timeout_ms": 30000,
"settle_ms": 100
}
```
Input events marked `sensitive` are dispatched to the PTY but stored only as a
byte-count placeholder. Environment values listed in `secret_aliases` are also
redacted from recorded text. The command-shim directory is placed first in the
captured process `PATH`; any remaining `PATH` must be explicitly supplied or
inherited under operator policy.
`executable` is required. `arguments`, `working_directory`, `environment`,
`filesystem_observation_paths`, terminal settings, timed `events`, timeouts,
resource limits, and normalization settings have defaults; see
`processScenarioSchema` for the exact contract. Event times are milliseconds
from launch, must be ordered, and must fall within `timeout_ms`. Inputs marked
`sensitive` are sent to the process but persisted as a byte-count placeholder.
Environment overrides are recorded in the scenario commitment; inherited
values are not copied into Evidence.
Scenario arguments, environment names, timed interactions, shim routes and
output chunks, filesystem checkpoints, and static HTTP/WebSocket scripts are
accepted without fixed item-count ceilings. The run still obeys its timeout
and the configured output-byte, process, filesystem, protocol-event, body-byte,
and connection budgets; each filesystem checkpoint uses those same
bounded snapshot limits.
The capture contains raw PTY output chunks and rendered terminal states,
interaction dispatch outcomes, exit reason, sampled process-tree observations,
and settlement status. When filesystem paths are selected, REA records
`files_before` and `files_after`, then classifies observed entries as created,
deleted, modified, or unchanged. These are bounded snapshots, not a syscall
trace; short-lived changes between snapshots may be missed. With no selected
paths, filesystem effects remain unknown.
## Reactive process scenarios
Output, file count, file size, process sampling, filesystem depth, total
runtime, idle time, and post-exit settlement are bounded by the scenario's
limits. The result marks truncated observations and residual unknowns rather
than treating missing data as proof of equivalence. Cancellation and timeout
run the same owned-process cleanup path. Settlement reports whether the
sampled process group quiesced or whether cleanup was needed or unverifiable;
sampling cannot prove that every short-lived or detached descendant was seen.
Set `reactive` to a process reactive scenario declaration when interaction
must follow observed output instead of guessed delays. The graph declares an
initial state, absolute scenario and state deadlines, explicitly prioritized
transitions, caller-chosen use and visit counts, trigger predicates, ordered
actions, and either a next state or `passed` outcome.
## Compare two captures
The admitted runtime slice matches decoded UTF-8 terminal literals and exact
`terminal_raw`, `interaction`, `process`, `filesystem`, `http`, `websocket`, or
`shim` journal events. Process events are sampled observations, not an
event-complete lifecycle. Filesystem events identify named checkpoints; shim
events identify a command and route. Live predicates receive the same normalized
and redacted payload later persisted in Evidence. Predicates may compose with
`all`, `any`, `sequence`, and `repeat` nodes and may select a frontier at
scenario start, state entry, a prior checkpoint, or an exact event ID. Actions
can write PTY input, resize it, signal the root process, or capture a named
filesystem checkpoint. `terminal_rendered`, `lifecycle`, and `replay_transition`
event triggers and non-root signal selectors fail during preflight before the
target launches.
Compare saved capture Evidence with:
Reactive results are recorded in `reactive_run`, including the terminal outcome,
active state, matched event IDs, action evidence IDs, and deterministic
transition journal. The reducer retains and evaluates every admitted
observation through the scenario deadline; it has no fixed state, transition,
predicate, action, repeat, or history-count ceilings. Recursive input depth has
a stack safety guard. Terminal controls also commit the last admitted
observation order, so later output cannot be moved ahead of a timeout,
cancellation, or target loss during validation. If the process exits before the graph finishes,
the outcome is `target_lost`; state/scenario deadlines remain distinct
`predicate_timeout`/`scenario_deadline` outcomes. Timed `events` remain
available for fixed schedules, but they are not silently translated into the
reactive graph.
## Checkpoints and deterministic shims
Checkpoint triggers may use an elapsed time, a terminal literal and occurrence
count, root exit, or settlement. REA always includes `before` and
`after_settlement` snapshots. Snapshot effects are relative to the preceding
checkpoint, so transient files remain visible even when the final tree matches
the initial tree.
Each command shim matches an exact argument array. Routes can emit timed stdout
or stderr chunks and then exit or receive `SIGINT`, `SIGTERM`, or `SIGKILL`.
Unmatched and exhausted calls are recorded. Shim observations are included in
the capture without a separate fixed count ceiling. Other configured
observation and byte budgets remain explicit in the capture result when they
affect completeness.
## Run a replay machine directly
Use `rea run-replay-machine ./run.json` to validate a finite replay machine
against an ordered `events` array without launching a process or opening a
socket. MCP clients use `run_replay_machine` with the same `{ machine, events }`
input. The result retains one decision per offered event, a transition journal
with capture alias metadata, and one redacted action-table entry per used
transition. It never returns request bodies, request headers, or captured
values. It also reports the initial and final states, whether the final state is
terminal, configured limits, and committed usage.
The direct evaluator has no fixed state, transition, event, guard, capture,
action, or action-byte count ceiling. It indexes transitions by protocol and
path, evaluates events in order, and returns a decision for each offered event
plus actions for used transitions. Caller-declared `max_uses`, `max_visits`,
`max_transitions`, and protocol budgets determine the finite run; there is no
upper schema cap on those values. Result size follows the input and is not
truncated by an arbitrary item quota.
Refused events remain data rather than aborting the run. Outcomes distinguish
unmatched events, invalid states, failed guards, exhausted transitions, invalid
captures, unexpected reconnects, and exhausted limits. This direct runner
evaluates the same domain runtime used by loopback Process Capture, but performs
no network or target execution.
## Compare captures
Compare two saved Evidence records with:
```bash
```sh
rea compare-process-captures authority.json reconstruction.json
```
Pass an optional third path, `rea compare-process-captures authority.json
reconstruction.json trace.json`, when valid executions may differ in declared
concurrent ordering. MCP callers pass the same object as `trace_spec` to
`compare_process_captures`. A trace specification declares exact named events
and chooses one language:
The comparison checks terminal, interaction, exit, filesystem, and
process observations under a shared comparison contract. It reports observed
differences with their locations, while residual unknowns or truncated
observations prevent a complete-equivalence claim. Matching scenario
commitments do not reveal whether redacted sensitive inputs were equal.
- `partial_order` requires every event pair to be related by a
`happens_before` edge (including transitive edges) or an explicit
`unordered_groups` declaration. `not_before` expresses negative ordering
constraints without inferring causality. Optional `prefix` and `suffix`
arrays remain exact.
- `finite_traces` lists the complete accepted variants. An `unordered` token
accepts permutations of exactly its declared event multiset; it does not
drop or broadly sort events.
For scenarios where independent scheduling can change event order, the CLI
also accepts an optional trace-specification JSON file as the third argument.
That specification must state the exact events and ordering constraints to
accept; it does not discard unmatched observations or make timestamps into
causal evidence. Trace comparison returns `unknown` when a capture lacks the
required complete event journal or contains relevant residual unknowns.
Each event uses one source family (`terminal_raw`, `terminal_rendered`,
`interaction`, `lifecycle`, `process`, `filesystem`, `http`, `websocket`,
`shim`, or `replay_transition`), an exact JSON payload, and a discriminated
cardinality: `required`, `optional`, `exact`, or `range`. Specifications with
duplicate or overlapping predicates, unknown references, cycles, implicit
ordering gaps, ordered/unordered conflicts, or finite variants that violate
cardinality are rejected before comparison.
Event payloads are exact by default. To compare schedules whose recorder
metadata necessarily changes, an event may explicitly list top-level
`ignore_fields` from the bounded set `sequence`, `at_ms`, `scheduled_at_ms`,
`dispatched_at_ms`, and `elapsed_ms`; its `exact` object must omit those fields.
All predicates for one source use the same ignore set, so relaxed metadata
cannot create declaration-order matching ambiguity. Other payload fields are
never dropped or normalized by trace comparison.
Process Capture records a monotonic `event_journal` at observation time. Trace
comparison uses only this capture order for cross-source causality; timestamps
are retained as event data but never promoted into happens-before evidence.
Older captures without a complete journal, truncated captures, and relevant
residual unknowns return a trace verdict of `unknown`. A passing result retains
each side's raw journal locations and identifies either the matched finite
variant or the complete satisfied constraint set. A failure returns the first
declaration-ordered predicate, cardinality, edge, prefix, suffix, or language
violation with its relevant event locations.
If both captures have the same invalid declared trace, the nested verdict is
`nonconforming` while the top-level capture comparison remains unchanged. Side
statuses and the diagnostic distinguish conformance from pairwise difference.
The result classifies terminal, interaction, exit, filesystem, protocol,
process, and shim behavior. `first_divergence` points to the earliest observed
difference. Residual unknowns prevent a claim of complete equivalence, but they
do not hide an observed difference in another dimension.
Captures must have equal comparison-contract commitments, but may identify
different scenarios and executables. MCP callers may set `max_capture_age_ms`;
the application clock then rejects stale captures before comparison.
## Limits and lifecycle behavior
Scenarios bound output bytes, frames, files, file bytes, process observations,
protocol/shim events, connections, filesystem depth, total runtime, idle time,
and settlement time. Process trees are sampled rather than syscall-traced, and
short-lived descendants may be missed. Filesystem observations are snapshots,
not filesystem event streams.
REA tags launched processes with a private run identifier. During cleanup it
revalidates that identifier before signaling sampled process groups, including
groups detached from the original parent. Temporary HOME, replay, shim,
terminal, checkpoint, and sampling resources are released on success, failure,
timeout, and cancellation.
After root exit, REA checks token-owned process groups every 50 ms. Two
consecutive empty observations establish `quiesced`; the settlement deadline
otherwise produces `alive_at_deadline`, while inspection or identity failures
produce `unverifiable`. Sampling can still miss short-lived or early-detached
descendants, which remains a residual unknown.
Each capture carries commitments for the selected scenario, executable,
comparison contract, and normalization rules. Comparison rejects captures
whose comparison contracts differ. Captures are local Evidence files; keep
their source artifacts and invocation context available when interpreting a
difference.
+166 -43
View File
@@ -1,30 +1,34 @@
{
"package": {
"name": "rea-agents",
"version": "3.2.1",
"version": "4.1.0",
"sdk": {
"server": "2.0.0",
"client": "2.0.0",
"core": "2.0.0"
"server": "2.3.1",
"client": "2.3.1",
"core": "2.3.1"
},
"skill_version": "23"
"skill_version": "26"
},
"tools": {
"total": 114,
"total": 126,
"families": [
{
"id": "direct",
"surface": "official-proxy",
"count": 36,
"count": 41,
"tools": [
"address_name",
"address_to_file_offset",
"annotate_native_function",
"comment",
"current_address",
"current_document",
"current_procedure",
"goto_address",
"inline_comment",
"inspect_native_data_type",
"inspect_native_instruction",
"inspect_native_load_image",
"list_bookmarks",
"list_documents",
"list_names",
@@ -43,6 +47,7 @@
"read_bytes",
"read_function_instructions",
"resolve_containing_procedure",
"resolve_native_call_targets",
"search_procedures",
"search_strings",
"set_address_name",
@@ -64,7 +69,6 @@
"analyze_swift_types",
"batch_decompile",
"binary_overview",
"find_code_for_string",
"find_xrefs_to_name",
"get_call_graph",
"get_objc_classes",
@@ -73,46 +77,68 @@
"inspect_native_dispatch_metadata",
"trace_call_path",
"trace_feature",
"trace_native_ui_action"
"trace_native_ui_action",
"trace_native_values"
]
},
{
"id": "native",
"surface": "native-provider",
"count": 5,
"count": 7,
"tools": [
"capture_native_ui_scenario",
"demangle_swift",
"inspect_macho",
"inspect_plist",
"inspect_signature",
"list_architectures"
"list_architectures",
"observe_native_ui"
]
},
{
"id": "artifact",
"surface": "artifact-provider",
"count": 3,
"count": 5,
"tools": [
"decode_interface_builder",
"extract_artifact",
"inspect_artifact"
"inspect_artifact",
"inspect_asset_catalog",
"inspect_keyed_archive"
]
},
{
"id": "managed",
"surface": "managed",
"count": 8,
"count": 7,
"tools": [
"compare_managed_members",
"import_managed_reconstruction",
"inspect_managed_artifact",
"inspect_managed_members",
"inspect_managed_native_boundaries",
"plan_managed_runtime_correlation",
"project_managed_application_graph",
"verify_managed_native_boundaries"
]
},
{
"id": "firmware",
"surface": "firmware-provider",
"count": 2,
"tools": ["extract_firmware", "inspect_firmware_regions"]
},
{
"id": "android",
"surface": "android-provider",
"count": 5,
"tools": [
"inspect_android_class",
"inspect_android_method",
"inspect_android_package",
"search_android_classes",
"trace_android_references"
]
},
{
"id": "browser",
"surface": "browser-provider",
@@ -153,16 +179,14 @@
{
"id": "application",
"surface": "application-workflow",
"count": 10,
"count": 8,
"tools": [
"build_reconstruction_obligation_ledger",
"compare_application_versions",
"compare_javascript_export_shapes",
"compare_source_to_bundle",
"evaluate_reconstruction_coverage",
"execute_node_characterization",
"prepare_node_characterization",
"run_controlled_replay",
"export_web_scripts",
"trace_application_feature",
"trace_javascript_semantics"
]
@@ -170,7 +194,7 @@
{
"id": "session",
"surface": "session",
"count": 22,
"count": 21,
"tools": [
"binary_session",
"build_call_path",
@@ -190,7 +214,6 @@
"list_unknowns",
"open_binary",
"record_unknown",
"run_replay_machine",
"update_unknown",
"verify_reconstruction",
"verify_unknown_resolution"
@@ -199,13 +222,24 @@
]
},
"providers": [
{
"id": "binwalk",
"name": "Binwalk",
"version": "3.1.0",
"capabilities": ["inspect_firmware_regions"]
},
{
"id": "ghidra",
"name": "Ghidra",
"version": null,
"capabilities": [
"address_name",
"address_to_file_offset",
"analyze_function",
"annotate_native_function",
"inspect_native_data_type",
"inspect_native_instruction",
"inspect_native_load_image",
"list_documents",
"list_names",
"list_procedures",
@@ -218,8 +252,10 @@
"procedure_info",
"procedure_pseudo_code",
"procedure_references",
"read_bytes",
"read_function_instructions",
"resolve_containing_procedure",
"resolve_native_call_targets",
"search_procedures",
"search_strings",
"xrefs"
@@ -269,16 +305,49 @@
"xrefs"
]
},
{
"id": "ida",
"name": "IDA Pro MCP adapter",
"version": "1",
"capabilities": [
"analyze_function",
"list_procedures",
"list_strings",
"procedure_address",
"procedure_assembly",
"procedure_callees",
"procedure_callers",
"procedure_pseudo_code",
"read_function_instructions",
"search_procedures",
"search_strings",
"xrefs"
]
},
{
"id": "jadx",
"name": "JADX headless",
"version": "0.7.1",
"capabilities": [
"inspect_android_class",
"inspect_android_method",
"inspect_android_package",
"search_android_classes",
"trace_android_references"
]
},
{
"id": "native-macos",
"name": "macOS native inspection utilities",
"version": null,
"capabilities": [
"capture_native_ui_scenario",
"demangle_swift",
"inspect_macho",
"inspect_plist",
"inspect_signature",
"list_architectures"
"list_architectures",
"observe_native_ui"
]
},
{
@@ -288,7 +357,9 @@
"capabilities": [
"decode_interface_builder",
"extract_artifact",
"inspect_artifact"
"inspect_artifact",
"inspect_asset_catalog",
"inspect_keyed_archive"
]
},
{
@@ -329,7 +400,6 @@
"capabilities": [
"compare_managed_members",
"import_managed_reconstruction",
"plan_managed_runtime_correlation",
"project_managed_application_graph",
"verify_managed_native_boundaries"
]
@@ -350,9 +420,6 @@
"compare_javascript_export_shapes",
"compare_source_to_bundle",
"evaluate_reconstruction_coverage",
"execute_node_characterization",
"prepare_node_characterization",
"run_controlled_replay",
"trace_application_feature",
"trace_javascript_semantics"
]
@@ -383,6 +450,18 @@
"list_javascript_runtime_targets",
"observe_javascript_runtime"
]
},
{
"id": "rea-web-script-export",
"name": "REA captured script export",
"version": "1",
"capabilities": ["export_web_scripts"]
},
{
"id": "unblob",
"name": "Unblob",
"version": "26.6.4",
"capabilities": ["extract_firmware"]
}
],
"setup_clients": [
@@ -425,20 +504,53 @@
{
"id": "devin",
"display_name": "Devin",
"format": "unsupported",
"configuration": "detect-only"
"format": "json",
"configuration": "managed"
},
{
"id": "opencode",
"display_name": "OpenCode",
"format": "opencode",
"configuration": "managed"
},
{
"id": "antigravity",
"display_name": "Antigravity",
"format": "json",
"configuration": "managed"
},
{
"id": "copilot_cli",
"display_name": "GitHub Copilot CLI",
"format": "copilot_cli",
"configuration": "managed"
},
{
"id": "commandcode",
"display_name": "Command Code",
"format": "commandcode",
"configuration": "managed"
},
{
"id": "vscode",
"display_name": "VS Code",
"format": "vscode",
"configuration": "managed"
}
],
"cli": {
"primary_count": 67,
"primary_count": 81,
"commands": [
"address-to-file-offset",
"analyze",
"analyze-javascript-application",
"analyze-web-bundle",
"annotate-native-function",
"build-reconstruction-obligation-ledger",
"capabilities",
"capture-browser-scenario",
"capture-electron-scenario",
"capture-native-ui-scenario",
"capture-process",
"capture-web-screenshot",
"compare",
@@ -457,20 +569,30 @@
"evaluate-reconstruction-coverage",
"evidence-export",
"evidence-import",
"execute-node-characterization",
"export-web-scripts",
"extract-artifact",
"extract-firmware",
"function",
"import-managed-reconstruction",
"import-reference-source",
"inspect",
"inspect-android-class",
"inspect-android-method",
"inspect-android-package",
"inspect-artifact",
"inspect-asset-catalog",
"inspect-electron-page",
"inspect-firmware-regions",
"inspect-keyed-archive",
"inspect-macho",
"inspect-managed-artifact",
"inspect-managed-members",
"inspect-managed-native-boundaries",
"inspect-native-api",
"inspect-native-data-type",
"inspect-native-dispatch-metadata",
"inspect-native-instruction",
"inspect-native-load-image",
"inspect-plist",
"inspect-signature",
"inspect-web-page",
@@ -480,23 +602,24 @@
"list-electron-targets",
"list-javascript-runtime-targets",
"observe-javascript-runtime",
"observe-native-ui",
"observe-web-session",
"plan-managed-runtime-correlation",
"policy",
"prepare-node-characterization",
"project-managed-application-graph",
"providers",
"read-bytes",
"reconcile-javascript-runtime",
"run-controlled-replay",
"run-replay-machine",
"resolve-native-call-targets",
"search",
"search-android-classes",
"setup",
"trace",
"trace-android-references",
"trace-application-feature",
"trace-javascript-semantics",
"trace-native-ui-action",
"trace-native-values",
"uninstall",
"upgrade",
"update",
"verify-managed-native-boundaries",
"xrefs"
],
@@ -509,15 +632,15 @@
},
"runtime_catalog": {
"counts": {
"cli_commands": 67,
"mcp_tools": 114,
"cli_commands": 81,
"mcp_tools": 126,
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "eb79567cc08f4afec223254c3977dc167d498f766b920077db0e1eead8be67b9",
"prompts_sha256": "fa889fc671185ac9c968c6dc8309cf609e709cdc837dbbf39b7cdfadb84ac2d7",
"combined_sha256": "3e9ab4e05563ea2609b7cf4db67638a79cd8a74188c292718d99a63451d5b9db",
"providers_sha256": "03498d8adb7d941f8293eea8d71375871fb30a278e310fdc19b98df6781fc1a7"
"tools_sha256": "569db1182f19869a3bdb22cf217bc75526f317b59635c8866d0e62741077cd52",
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
"combined_sha256": "e21455dd7d64af94e0b07aff55ab82f03a37e46729ece51be38b50083d2d5b55",
"providers_sha256": "7949ddcf2eb5ba41b022bb3dfa1951ab96dbf5640285fa7fd5d35b0e4a5bdab3"
}
}
}
+74 -66
View File
@@ -1,26 +1,27 @@
# Static-analysis provider evaluation
Status: the Ghidra read-only analysis provider is shipped on Linux x64 and has
an experimental Windows x64 P0 for approved native PE applications. macOS is
not an admitted Ghidra host. The provider
validates an exact bring-your-own Ghidra 12.1.4/JDK 21 environment, resolves a
provider/version/profile commitment, runs one isolated read-only headless
import, and publishes 19 operation-level capabilities after the authenticated
post-analysis handshake.
This guide describes repository main. Check the [released package boundary](installation.md#released-package-and-main) when using npm.
The provider-neutral target, provider registry, deterministic target binding,
analysis-profile commitment, Evidence provenance, snapshot caching, and bounded
provider-process lifecycle foundations are implemented. The Ghidra launcher,
packaged Java bridge, doctor/setup projection, bounded client lifecycle, and
multi-target real Linux verifier, curated Windows CI, and controlled real
Windows verifier are also implemented. Program identity,
procedure/string/symbol inventory, memory blocks, address/name and
containing-procedure resolution, complete inventory search, function metadata,
decompilation, assembly, resolved calls, typed references, xrefs, CFG, and
function dossiers are admitted. A provider is not a drop-in replacement for
Hopper: every capability is mapped explicitly, with truthful `unavailable`,
unknown, or degraded results where the engine cannot provide equivalent
semantics.
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.
REA imports one target into a temporary project and exposes 25 read-only
operations after analysis completes. They cover inventories, search,
decompilation, assembly, function metadata, resolved calls, references, control
flow, instructions, and recovered types. Results identify the provider version,
analysis settings, target digest, and any unavailable or incomplete facts.
On Linux and macOS, `annotate_native_function` additionally edits names and entry
comments atomically, verifies readback, and returns refreshed function analysis.
Changes live only in the session database and leave executable bytes unchanged.
Ghidra does not expose GUI controls; Windows P0 remains read-only.
Windows x64 P0 supports native x86-64 PE applications on local NTFS with
bundled Job Object ownership, protected runtime DACLs, and handle-based path
admission. Real ordinary-user verification covers all 25 read-only operations
through the packaged CLI and MCP, target integrity, and cleanup. This remains
an experimental boundary; see [Windows Ghidra P0](windows-ghidra-p0.md) and
[issue #527](https://github.com/morluto/rea/issues/527).
[ADR-0001](adr/0001-provider-selection-and-analysis-profiles.md) fixes the
provider registry, deterministic selection, target binding, analysis profile,
@@ -32,8 +33,8 @@ follow.
- Keep the existing provider-neutral CLI and MCP tool names.
- Bind one deep-analysis provider to a target for the target's lifetime; never
fail over silently between Hopper and Ghidra.
- Start with bring-your-own Ghidra and a compatible Java runtime. Setup must not
install or upgrade Java.
- Use an existing Ghidra installation and a compatible Java runtime. Setup must
not install or upgrade Ghidra or Java.
- Run Ghidra headlessly in an owned process with a private temporary project,
startup and cleanup deadlines, caller cancellation, and complete replies.
- Prefer a packaged Java bridge loaded through Ghidra's script path. PyGhidra
@@ -54,10 +55,11 @@ follow.
`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`. Linux accepts x86, x86-64,
ARM, and ARM64 ELF, PE, and Mach-O executable targets. Windows P0 accepts only
native x86-64 PE applications and rejects DLLs, managed images, unknown roles,
other formats, and other architectures before launch.
`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
native authority available before launch.
The launcher creates one ephemeral runtime root with project,
home/cache/config/data/temp, logs, descriptor, endpoint, target snapshot, and
@@ -66,19 +68,23 @@ It passes `-readOnly` and `-deleteProject`, and uses Ghidra's default analysis
and resource settings; inherited Java option injection variables are cleared.
On Linux, the mode-0600 descriptor carries the random token without
exposing it in argv or environment and the Java bridge binds a mode-restricted
Unix socket. Windows uses authenticated IPv4 loopback with a strict token-free
endpoint record. Both transports report actual
Ghidra/language/compiler/analysis/import-digest metadata, accept only the exact
authenticated `ping`, `shutdown`, and ten
inventory plus nine function-analysis methods, and deletes the socket. Close,
cancellation, timeout,
malformed protocol, or process exit stops the owned process resources and
removes the entire runtime root. Windows P0 uses bounded `taskkill` tree
termination and explicitly does not claim Job Object, private-DACL, or
reparse-point authority; see [Windows Ghidra P0](windows-ghidra-p0.md).
Unix socket. macOS uses the same local transport. The bridge reports actual
Ghidra/language/compiler/analysis/import-digest metadata and accepts only
authenticated `ping`, `shutdown`, thirteen read-only inventory/load-image methods,
and twelve function-analysis methods. Linux/macOS also admits atomic function
annotation changes in the ephemeral database. Close, cancellation, timeout, malformed protocol,
or process exit stops the owned process resources, closes the socket, and
removes the runtime root.
The provider catalog lists only the 19 proved Ghidra operations. GUI cursor,
navigation, and mutation operations remain absent; the router therefore
The experimental Windows transport uses authenticated IPv4 loopback with a
token-free endpoint record. The bundled native authority establishes Job Object
ownership, protected runtime DACLs, and handle-based path admission before
launch. If the bundle or an admission constraint is unavailable, the provider
remains unavailable; a `taskkill` fallback does not establish that authority.
The provider catalog lists 25 read-only Ghidra operations and the Linux/macOS
session annotation operation. GUI cursor/navigation and persistent mutations
remain unavailable; Windows P0 admits no database mutation. The router therefore
reports them unavailable instead of borrowing Hopper semantics or inferring
capability from a successful import.
@@ -98,21 +104,21 @@ capability from a successful import.
## Admitted function-analysis semantics
| Concern | Ghidra contract |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Decompiler lifetime | One persistent `DecompInterface` is opened for the imported Program and disposed during bridge shutdown. Native decompilation has no fixed per-function deadline. External functions or functions without bodies return `null`; cancellation and native failure remain distinct. |
| Serialization | A FIFO sends one Program request at a time. Caller cancellation removes queued work promptly and is passed to active socket waits. This is an adapter safety commitment, not a claim that every Ghidra API is thread-safe. |
| Function identity | Every function result carries the entry address and Ghidra FunctionManager classification for external, thunk, and resolved thunk target. These are observations, not proof that unresolved targetless calls have been recovered. |
| Assembly and pseudocode | Assembly is complete Ghidra Listing text; pseudocode is Ghidra decompiler output. Neither is original source, and cross-provider comparison never treats Hopper and Ghidra text as equal or unequal semantic facts. |
| Instruction fast path | `read_function_instructions` returns every raw Listing instruction for the requested function without invoking the decompiler or whole-program name/string inventories. |
| Calls and references | Callers/callees contain only resolved functions. Reference edges preserve exact ReferenceManager type and call/jump/data/read/write/indirect/computed/conditional/terminal/external facts. Targetless computed flow remains unknown. Synthetic entry-point references without actionable memory sources are omitted explicitly. |
| CFG | Dossiers use `BasicBlockModel` and retain only non-call successors inside the function body. CFG topology is address-normalized for comparison; provider-specific block construction remains a declared difference. |
| P-code value flow | Function dossiers include up to 3,000 high-p-code operations, at most 64 inputs per operation, and 12,000 def-use edges. Results identify memory reads/writes, branches, and calls by p-code opcode, and mark decompiler-dead operations. Large results report truncation, an omitted-operation lower bound, and known omitted-input/edge counts. This is an intra-function decompiler graph: it does not resolve cross-function data flow, memory aliasing, call side effects, runtime behavior, persistent state, or business meaning. |
| Result extent | Function instruction scans and native API boundary observations remain complete. P-code flow is the explicit bounded exception; its count fields distinguish exact retained counts from lower-bound or known-omitted counts. Caller cancellation and provider failures remain distinct. |
| Concern | Ghidra contract |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Decompiler lifetime | One persistent `DecompInterface` is opened for the imported Program and disposed during bridge shutdown. Native decompilation has no fixed per-function deadline. External functions or functions without bodies return `null`; cancellation and native failure remain distinct. |
| Serialization | A FIFO sends one Program request at a time. Caller cancellation removes queued work promptly and is passed to active socket waits. This is an adapter safety commitment, not a claim that every Ghidra API is thread-safe. |
| Function identity | Every function result carries the entry address and Ghidra FunctionManager classification for external, thunk, and resolved thunk target. These are observations, not proof that unresolved targetless calls have been recovered. |
| Assembly and pseudocode | Assembly is complete Ghidra Listing text; pseudocode is Ghidra decompiler output. Neither is original source, and cross-provider comparison never treats Hopper and Ghidra text as equal or unequal semantic facts. |
| Instruction fast path | `read_function_instructions` returns every raw Listing instruction for the requested function without invoking the decompiler or whole-program name/string inventories. |
| Calls and references | Callers/callees contain only resolved functions. Reference edges preserve exact ReferenceManager type and call/jump/data/read/write/indirect/computed/conditional/terminal/external facts. Targetless computed flow remains unknown. Synthetic entry-point references without actionable memory sources are omitted explicitly. |
| CFG | Dossiers use `BasicBlockModel` and retain only non-call successors inside the function body. CFG topology is address-normalized for comparison; provider-specific block construction remains a declared difference. |
| P-code value flow | Function dossiers include up to 3,000 high-p-code operations, at most 64 inputs per operation, and 12,000 def-use edges. Results identify memory reads/writes, branches, and calls by p-code opcode, and mark decompiler-dead operations. Large results report truncation, an omitted-operation lower bound, and known omitted-input/edge counts. The dossier is an intra-function graph; `trace_native_values` composes derived parameter/argument and return/output dependencies across resolved calls. It does not resolve memory aliasing, call side effects, runtime behavior, persistent state, or business meaning. |
| Result extent | Function instruction scans and native API boundary observations remain complete. P-code flow is the explicit bounded exception; its count fields distinguish exact retained counts from lower-bound or known-omitted counts. Caller cancellation and provider failures remain distinct. |
`npm run verify:ghidra` compiles the versioned C oracle into debug and stripped
host-native targets (x86-64 ELF on Linux x64). It proves all
19 operations, external functions, resolved thunks, exports, stripped-name
host-native targets (x86-64 ELF on Linux x64 or Mach-O on macOS), plus a native
DWARF 4 type-layout object. It proves the admitted operations, external functions, resolved thunks, exports, stripped-name
behavior, direct and targetless indirect calls, typed references, strings/xrefs,
multi-block CFG, bounded p-code def-use/effect links, semantic enhanced workflows, cancellation, startup deadlines,
serialized concurrency, malformed-target rejection, profile identity, and
@@ -126,24 +132,26 @@ Mach-O target coverage. It requires `clang`, LLD, and `lld-link`; set
these tools before compilation. Keeping this matrix separate lets Linux
host/provider acceptance run with only the host compiler.
`npm run verify:ghidra:windows` generates a deterministic source-owned native
x86-64 PE application, proves all 19 operations through the production Windows
launcher and loopback transport, checks target/snapshot/import SHA-256 linkage,
and requires project, endpoint, process, and runtime cleanup. Passing that lane
proves only the documented P0 boundary, not the remaining Windows security
gates.
`npm run verify:ghidra:windows` checks the source-owned native x86-64 PE
fixture, all 25 read-only operations, target/snapshot/import SHA-256 linkage,
and project, endpoint, process, and runtime cleanup. The independent native
lane checks DACLs, handle admission, cancellation, and Job Object lifecycle.
`npm run verify:ghidra:windows:package` packs and installs REA into an isolated
prefix, then checks ordinary-user CLI/MCP operations against canonical contracts.
All lanes require matching native controls; package startup alone establishes
only package compatibility.
## Shared provider-process foundation
`src/process/` now provides the mechanisms that a long-lived Hopper or Ghidra
adapter genuinely shares: POSIX run-token-authenticated process-group ownership,
adapter shares: POSIX run-token-authenticated process-group ownership,
ephemeral temporary runtime roots, one absolute startup deadline, correlated
request cancellation and lifecycle-deadline cleanup, bounded stdout and stderr retention with
exact byte counts, process-exit diagnostics, and bounded TERM-to-KILL shutdown.
Reusable fixtures exercise exit, timeout, cancellation, graceful termination,
forced termination, double-close, spawn failure, and resource release. Windows
P0 adds bounded process-tree termination as an explicitly weaker host backend;
Job Object ownership remains unimplemented.
Ghidra uses its native Job Object, private runtime, and path-admission boundary;
generic process-tree termination alone cannot establish provider availability.
The foundation does not define a bridge schema, socket framing, health payload,
analysis model, or shutdown acknowledgement. Hopper keeps its authenticated
@@ -153,12 +161,12 @@ only the generic process mechanisms.
## Shortlist
| Provider | License / automation surface | What it brings | REA fit and blockers |
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Broad static analysis, many processors and formats, scripting, and project/database workflows | Read-only analysis shipped: exact BYO checks, isolated state and provider lifecycle, serial API queue, packaged bridge, 19 inventory/function operations, and real ELF/PE/Mach-O x86-64 plus AArch64 conformance. GUI and mutation semantics remain intentionally unavailable. |
| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. |
| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. |
| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. |
| Provider | License / automation surface | What it brings | REA fit and blockers |
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Static analysis, multiple processors and formats, scripting, and project/database workflows | 25 read-only operations shipped on Linux/macOS with installation checks, temporary projects, a serial API queue, and real ELF/PE/Mach-O conformance. Experimental Windows x64 P0 supports native x86-64 PE on local NTFS with bundled native controls and ordinary-user CLI/MCP verification; Linux/macOS additionally support atomic session function annotations; Windows mutation and all Ghidra GUI operations are unavailable. |
| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. |
| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. |
| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. |
## Recommended order
+2 -2
View File
@@ -36,7 +36,7 @@ check project-owned conformance fixtures and fail-closed report behavior.
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `discover-classify` | target-free startup, artifact inventory, compatible workflow selection, and exact visible limits |
| `diagnose-environment` | broken runtime separated from analysis failure, version skew detected, and revision changed after recovery |
| `acquire-authority` | exact missing scope, narrow grant, expansion re-prompt, and no launch after denial/cancellation |
| `declare-request` | exact target, operation, and lifecycle are carried by the request before launch |
| `static-analysis` | native and JavaScript routing plus preserved ambiguity, truncation, unsupported syntax, and residual unknowns |
| `reactive-scenarios` | predicate-driven interaction, descendant settlement, Electron correlation, and cancellation cleanup Evidence |
| `compare-authority-candidate` | explicit partial-order/finite-trace semantics, primary divergence localization, and unknown for incomplete comparison |
@@ -63,7 +63,7 @@ The evaluator rejects or withholds readiness when:
- contradiction Evidence or its affected comparison is missing;
- the reconstruction obligation ledger is not `ready`, has no required
obligation, or retains an unowned/unverified required obligation;
- denied authority launches a process, cancellation loses diagnostics, or owned
- a process exceeds its declared target/lifecycle, cancellation loses diagnostics, or owned
resources remain;
- replay is nondeterministic, tampering/staleness is not detected, or the
expected source digest differs.
+69 -32
View File
@@ -1,32 +1,69 @@
# Installation roadmap
# REA roadmap
## Tool direction
REA provides inspection, capture, and comparison tools that return evidence
directly. Agents choose commands, paths, browser actions, scripts, and local
fixture servers, then compose experiments from those results. CLI and MCP must
expose equivalent behavior and preserve observations, inferences, and unknowns.
[#555](https://github.com/morluto/rea/pull/555) removed REA permission grants,
scope ceilings, elicitation, and repeated approval fields.
[#572](https://github.com/morluto/rea/pull/572) removed the replay engines,
Node characterization prepare/execute flow, and plan-only managed runtime
correlation tool. Those systems are retired roadmap items. Existing evidence
authority labels remain readable as provenance; they do not imply a current
replay executor.
Local operations use the current user's OS permissions. Input and protocol
validation, provider prerequisites, target identity, cancellation, and owned
resource cleanup remain part of each tool's contract. Setup still discloses
installation changes and requires approval before writing or installing.
## Remaining evidence and provider work
The [platform roadmap](https://github.com/morluto/rea/issues/32),
[process and Hopper fidelity tracker](https://github.com/morluto/rea/issues/48),
and [browser tracker](https://github.com/morluto/rea/issues/39) track remaining
capabilities and proof. Their scope follows direct tools and agent-composed
experiments; custom replay languages and grant systems are not completion gates.
[Inline Evidence composition](https://github.com/morluto/rea/pull/585) now returns
the complete record accepted by comparison tools. Managed inspection uses an
explicit PE/CLI target independently of the native provider selection. Comparisons
preserve unknowns when observation is incomplete, and PTY diagnostics retain the
actual loader or startup failure. Browser cleanup reports incomplete teardown
alongside the primary failure; transient documents invalidate only the affected
frame's WebMCP registrations. Real-provider and platform claims still require
their corresponding verification lanes.
## Shipped behavior
REA setup currently configures the capabilities that exist today: its bundled
skill, detected agent integrations, optional Hopper provider, and validated
bring-your-own Ghidra paths. It detects
Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, and Devin. The
first six have documented local MCP configuration boundaries and can be updated
additively; Devin is reported but left unchanged.
REA setup lets you select agent integration and optional Hopper installation.
It installs the bundled workflow, configures detected agents, and can save
verified paths for an existing Ghidra installation. It configures Claude Code,
Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity,
GitHub Copilot CLI, Command Code, and VS Code using each client's configuration format.
The Ghidra foundation supports Linux x64 with exact Ghidra 12.1.4 and a 64-bit
full JDK 21. macOS is not an admitted Ghidra host. An experimental
Windows x64 P0 admits approved native x86-64 PE
applications through the same 19-operation Java bridge. Doctor validates those coordinates; approved Linux setup propagates them
to MCP registrations without installing or modifying either dependency. REA's
packaged Java bridge then proves an isolated read-only headless import,
post-analysis handshake, complete cleanup, ten admitted read-only inventory
operations, and nine function-analysis operations. Real source-owned x86-64
debug/stripped ELF, AArch64 ELF, PE, and Mach-O fixtures cover program,
procedure, string, symbol, external/thunk, memory, resolution, search,
decompilation, assembly, calls, typed references, xrefs, and CFG semantics.
Function dossiers also expose bounded Ghidra high-p-code def-use and memory or
control-flow effects, mark decompiler-dead operations, and report truncation
plus known omitted counts. This is intra-function
decompiler evidence; cross-function state flow remains unimplemented.
Hosted Windows CI covers build, package, target admission, transport, and
lifecycle seams; a controlled self-hosted workflow covers the real Ghidra P0
claim.
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
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
is discarded on close and executable bytes stay unchanged. Approved setup saves verified
installation paths in agent configurations without installing or changing
Ghidra or Java.
Real-provider verification uses host-native debug and stripped fixtures plus
a native type-layout object. A separate lane covers AArch64 ELF, PE, and
Mach-O cross-target fixtures. See [testing](testing.md) for the prerequisites
and exact commands.
Experimental Windows x64 P0 now supplies bundled Job Object ownership,
protected runtime DACLs, and handle-based admission for native x86-64 PE
applications on local NTFS. Real ordinary-user CLI/MCP verification covers
all 25 read-only operations and cleanup. The [Windows P0 guide](windows-ghidra-p0.md)
describes this boundary and unverified broader coverage.
## Ghidra maintenance boundary
@@ -36,16 +73,16 @@ not expected to match; unresolved targetless flow remains unknown. Automatic
Ghidra acquisition, if ever added, remains a separately planned and approved
related-tool change; setup must never install Java.
Before Windows advances beyond P0, add current-user-only DACL creation and
readback, handle-based reparse-point-safe path authority, a DACL-protected IPC
backend, and Job Object assignment before provider execution. Process capture,
controlled replay, Hopper, and broad filesystem-sensitive workflows remain
separate Windows projects rather than implied parity.
Maintain Windows P0 with independent DACL readback, handle-based admission,
private authenticated IPC, and Job Object assignment before provider execution.
Process capture, Hopper, and broad filesystem-sensitive workflows remain
separate Windows projects; P0 does not imply their parity.
## Capability-selective setup
After REA ships at least two real optional toolchains, setup can evolve into a
capability-oriented wizard. It may ask whether the operator wants to investigate
Setup already lets users select agent integration and Hopper installation.
Once REA supports installing additional analysis tools, it can expand these
choices to match the work a user wants to do. It may ask whether the operator wants to investigate
native binaries, websites, mobile applications, firmware, or runtime protocols,
then propose only the tools needed for the selected work.
+169
View File
@@ -0,0 +1,169 @@
# 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.
+296 -37
View File
@@ -1,8 +1,15 @@
# Testing REA
REA's test suite follows one rule: use the lowest behavioral depth that can
prove a claim. Tests are grouped into native Vitest projects so ownership,
allowed dependencies, and runtime cost are visible from their paths.
Prefer evidence in this order: full end-to-end workflows with real production
providers, integration tests across data/API boundaries, then golden regressions
from real captured inputs. Keep focused module tests for distinct failure or
semantic cases that these workflows cannot reliably reproduce. A test's path
or suite name does not establish its behavioral depth.
Tests are grouped into Vitest projects so ownership, allowed dependencies and
runtime cost are visible from their paths. Before pruning a test, identify the
replacement scenario and its assertions; passing success journeys do not
replace malformed input, cancellation, permission or cleanup coverage.
## Behavioral depths
@@ -12,7 +19,7 @@ allowed dependencies, and runtime cost are visible from their paths.
| Composition | `tests/composition/**` | Provider-neutral session and registry wiring; filesystem access is limited to fixture materialization |
| Boundary | `tests/boundary/**` | Exactly one production filesystem, process, network, browser, CLI, or provider boundary |
| MCP boundary | `tests/boundary/mcp/**` | MCP transport and tool-contract boundaries with isolated sessions and explicit cleanup |
| Acceptance | `tests/acceptance/**` | A complete compiled CLI or MCP workflow |
| Acceptance | `tests/acceptance/**` | A compiled CLI or MCP journey; injected providers still make it integration |
| Conformance | `tests/conformance/**` | Shared provider contracts, parameterized by declared capabilities and explicit opt-outs |
| Evaluation | `tests/evaluation/**` | Deterministic evaluator parsing, scoring, and report generation |
@@ -31,17 +38,76 @@ with the suite and excluded from package builds; broader runtime and provider
fixtures remain under `tests/fixtures/**`.
`tests/process-global/**` is reserved for cases with a demonstrated dependency
on process-global state. Those tests run without file parallelism. Reusable,
on process-global state. Those tests use isolated forks so environment and
exit-status changes cannot leak between files. Serialize a case only when it
demonstrably shares an external resource that cannot be isolated. Reusable,
test-scoped fixtures live under `tests/support/**`; immutable source artifacts
remain under `tests/fixtures/**`.
The process-global Vitest configuration contract rejects new direct temporary-root
creation outside the workspace seam and its narrowly documented boundary/package
exceptions.
Real Hopper, Ghidra, browser, package, managed-code, and controlled-replay
claims belong to their explicit `npm run verify:*` lanes. They are not inferred
from mocks or folded into the deterministic local gate. Real model trials are
manual; Vitest covers only deterministic evaluator logic.
Real Hopper, Ghidra, IDA, browser, package, and managed-code claims belong to their
explicit `npm run verify:*` lanes. The reconstruction-readiness lane also
checks deterministic rerun, tamper, and stale-input handling; those checks do
not execute extracted JavaScript modules. When application runtime behavior is
needed, exercise the actual target through browser, Electron, or process
capture. Real model trials are manual; Vitest covers deterministic evaluator
logic.
## End-to-end, integration and golden evidence
Full E2E tests invoke the production command dispatcher and real providers,
without fake launchers, runners or responses. `verify:keyed-archive` writes an
actual Foundation binary archive, runs the CLI and a separate stdio MCP
subprocess, checks parity and pagination, rejects an escaping path and checks
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.
MCP SDK transport tests with recording providers remain integration tests.
They are useful for schema drift and failure projection but do not prove that
Hopper, Ghidra or another substituted engine works. `verify:package` proves
packaging/install behavior and fake-provider integration; use the corresponding
real-provider lanes for engine claims. Real Apple dispatch and Interface
Builder verifiers currently prove format integration through production readers.
Golden tests use immutable captured text inputs with producer/source provenance
under `tests/fixtures/golden/`. Expected results are reviewed for the semantic
claim; capture commands do not automatically approve new expected outputs.
Do not call handcrafted utility output or synthetic binary builders real-data
goldens. Keep unsupported binary layouts and malformed boundaries as targeted
regressions until a real fixture establishes equivalent coverage.
`verify:browser` also captures a source-owned noise canvas as a real PNG above
8 MiB through the CLI and stdio MCP, with complete byte/digest parity and real PNG
decoding. Its SDK client explicitly permits the larger inline JSON response;
this lane does not establish large image-comparison request transport coverage.
`verify:browser:network` is a focused real-browser lane for transaction identity,
selected request/response bytes, binary and compressed responses, duplicate
headers, credential and declared-secret redaction, redirects, streaming cutoff,
CLI/MCP parity, and owned-profile cleanup. Set `REA_BROWSER_EXECUTABLE` to an
installed Chrome-family browser. An optional script argument selects an already
installed package's `scripts/rea.mjs` entry point for packaged-artifact checks.
The complete `verify:browser` lane includes these same checks.
After building, `verify:browser:dom` checks empty and HTML-whitespace form
destinations against a native Chrome DOM-property oracle through CLI and stdio
MCP. It requires `REA_BROWSER_EXECUTABLE` and accepts an optional installed REA
entrypoint. The full browser lane includes the same public-adapter assertions
before other fixtures navigate the selected page.
`verify:browser:scripts` checks active script capture → exact-byte export →
existing static JavaScript analysis through CLI and stdio MCP, including
manifest readback, competing query variants, and resolved relative imports.
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.
## Real-toolchain verification lanes
@@ -54,16 +120,152 @@ that a host or target is covered when it was skipped.
| Ghidra lane | Supported runner/target | Additional local tools |
| ------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `npm run verify:ghidra` | Linux x64 with x86-64 ELF | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 Linux ELF relocatable fixture | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra` | Linux x64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:switch` | Linux x64 ELF; GCC/Clang optimized and stripped switch fixtures | GCC, Clang, GNU nm/objdump/strip, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 ELF; byte/halfword tables; host ARM64 Mach-O | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:cross-format` | Any supported Ghidra host; also analyzes AArch64 ELF, x86-64 PE, and x86-64 Mach-O | `clang`, LLD, and `lld-link` in addition to host-lane prerequisites |
| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE | Ghidra 12.1.4, full JDK 21, and the Windows P0 fixture toolchain |
| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE | Ghidra 12.1.4, full JDK 21, and the matching native artifact |
Windows native conformance runs with `npm run verify:windows-native` and does
not require Ghidra or Java. An optional independently compiled Windows fixture
adds in-place reparse, breakaway, environment, and token observations.
`npm run verify:ghidra:windows:package` additionally packs and installs REA into
an isolated prefix and runs ordinary-user CLI/MCP operations against their
canonical schemas. Run the controlled fixture generator before that lane, or
supply an installed package root and an explicit fixture as arguments.
The host-native Ghidra lane also verifies native value tracing through the
production CLI and a separate stdio MCP process. It compares complete dependency
graphs, validates Evidence and upstream/workflow profiles, checks capability
discovery, and closes the MCP session. No provider or transport is mocked.
The Linux switch lane checks dense, sparse-with-holes, shared-body, nonzero,
negative, and nonexact JSON integer labels plus a comparison-only control.
Independent source labels, ELF file bytes, table slots, and bounds branches
define expected case/default destinations. Production CLI and MCP must agree;
debug labels must retain their signed values. For stripped negative fixtures,
the independently checked 32-bit dispatch permits equivalent unsigned labels
only alongside the reported low-confidence `undefined4` parameter; original
source signedness remains unknown and the ABI residual must remain visible.
unsafe labels remain unresolved and every recovered destination is retained.
The compiler oracle also injects malformed records to check that its assertions
reject missing/default-confused labels, wrong destinations, and numeric guesses.
It also invokes the actual bridge methods on detached Ghidra model objects to
check ambiguous dispatches, signed literals, precision bounds, shared targets,
and conflicting labels. This reflection fixture depends on the pinned Ghidra
model and does not claim a compiler produced those synthetic graph shapes.
Pass `--entrypoint /path/to/installed/rea-agents/scripts/rea.mjs` directly to
`scripts/verify-real-ghidra-switch.mjs` to verify an installed package through
the same compiler oracles and CLI/MCP checks.
The cross-format Ghidra lane also analyzes an optimized AArch64 ELF switch
fixture. It checks the recovered case values against the source cases and
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
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.
## Android APK analysis
`npm run verify:android` requires an existing Java 17+ and an explicit
`REA_JADX_MCP_JAR` for jadx-headless-mcp 0.7.1. Set `REA_ANDROID_TEST_APK` to the
fixed public ApiDemos v6.0.18 fixture. Obtain both with the explicit
`npm run fixtures:android` command; files are SHA-256 verified and
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.
Synthetic producer regressions run independently:
```sh
npm run test:focused -- tests/boundary/android/jadxIntegration.test.ts tests/boundary/mcp/androidAnalysisMcp.test.ts
```
## Optional NativeAOT Ghidra analysis
This lane is separate from the default native lane. `build:fixtures:nativeaot`
requires an existing .NET SDK 8.0.416, the platform NativeAOT compiler/linker and
runtime pack 8.0.22. It builds benign sources into ignored `_reference/` and
records independent symbol/directory/SHA oracles; it never executes the target.
Linux also builds stripped, ordinary-native and small malformed/ambiguous/layout
negative inputs. The optional Windows fixture workflow builds a PE on a Windows
runner; analyzing that PE on Linux does not verify a Windows Ghidra host.
Build the clean pinned upstream adapter with `build:ghidra:nativeaot`, then set
`REA_GHIDRA_NATIVEAOT_JAR`. Run `verify:ghidra:nativeaot -- symbols`, `-- stripped`,
`-- ordinary`, `-- unsupported`, `-- malformed`, `-- ambiguous`, and
`-- loader-failure`, and `-- default-native` separately. The loader-failure mode source-builds
a deliberately failing JDK 21 initializer and checks the actual loader cause and
cleanup. The default-native mode verifies ordinary analysis with the optional
extension disabled.
The real MCP lane checks source identity, inline format discovery, metadata
relationships/slots against independent compiler symbols, frozen strings,
pseudocode and owned cleanup. Set `REA_NATIVEAOT_PROOF_CLI=1` for one equivalent
CLI type inspection; this costs an additional full import. Select an unpacked
installed package with `REA_NATIVEAOT_PROOF_PACKAGE_ROOT`, a fixture directory
with `REA_NATIVEAOT_PROOF_FIXTURE_ROOT`, and optional evidence capture directory
with `REA_NATIVEAOT_PROOF_CAPTURE_DIR` (absolute paths).
Keep builds/imports sequential on small hosts; scope `GHIDRA_HEADLESS_MAXMEM`
(e.g. `768M`) to this command and use CPU affinity if needed. REA does not install
or upgrade Java, Ghidra, .NET or native toolchains. See
[the supported layout and provenance](ghidra-nativeaot.md).
## IDA MCP adapter
`npm run verify:ida -- --target /absolute/path/to/program --procedure main`
uses the existing `REA_IDA_MCP_CONFIG` registration. It installs no engine,
Python package, or compiler. The target must already be open in the GUI for
the legacy attached profile; the database-supervisor headless profile opens
a digest-verified private copy. A caller-supplied fixture keeps prerequisites
limited to the selected engine and host. `tests/conformance/ida/inventory.c`
provides an optional small native fixture source with an exported
`rea_fixture_add` function.
The lane invokes the production CLI dispatcher and connects the pinned MCP
client SDK to the production REA server. It verifies function Evidence and
CLI/MCP parity, inventory/search, pseudocode, instructions, xrefs, malformed
input, original-input preservation, and lifecycle cleanup. For headless
analysis it confirms that the owned database IDs disappear from upstream
discovery and private workspaces are removed. For attached analysis it confirms
the existing GUI target remains reachable with the same input identity.
`--package-root` selects an installed/extracted REA artifact. `--report` writes
private local observations with mode `0600`; the console summary contains no
target paths or upstream output.
Adapter and composition tests cover producer parsing, pagination, canonical
entries, external callees, target switches, cancellation draining, snapshot
replay exclusion, ownership failures, and incomplete cleanup. They do not
establish real IDA operation. The initial real workflows cover legacy upstream
1.4.0 on a Windows GUI and the modern supervisor at upstream commit
`c133c3853faa111a9b00ee615c013b720d0c4acd` with Windows x64 IDA 9.3.
Linux/macOS headless, modern attached GUI tools, other engine versions and
architectures remain unverified; see the [provider guide](ida-provider.md).
## DOS Ghidra analysis
`npm run verify:ghidra:dos` requires the supported Ghidra and JDK installation
on Linux x64 or macOS x64/arm64. It generates a source-owned MZ fixture without
a DOS emulator or compiler, then checks real 16-bit decoding, segment
relocation, near/far calls, decompilation, disjoint function body ranges,
stable CLI/MCP observations, unchanged source bytes, and owned process/project
cleanup. Raw p-code address-space selector tokens are reported separately from
the stable observation comparison. Linux x64 is verified; macOS DOS remains
unverified. This lane is separate from host-native and optional cross-format
verification. See [DOS analysis](ghidra-dos.md).
`npm run verify:ghidra:com` uses a generated headerless fixture with no compiler,
DOS emulator or game data. It exercises explicit admission, BinaryLoader entry
preparation, measured register context, whole-file byte readback, source offsets,
unmapped PSP/partial reads, actual decompilation, CLI/MCP parity and owned cleanup.
It has the same Ghidra/JDK prerequisites as the MZ lane. Neither lane claims DOS
runtime or PC-98 device execution.
## Apple Interface Builder archives
`npm run verify:interface-builder` compiles the source-owned AppKit XIB into a
@@ -77,30 +279,61 @@ not make native host acceptance unavailable.
## Developer commands
`npm test` runs every deterministic project once. The narrower feedback loops
are:
Use source feedback while editing, explicit boundary checks for the changed
behavior, and complete CI evidence before merging.
| Command | Scope |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `npm run test:fast` | Domain, service, adapter, composition, boundary, MCP boundary, and conformance projects |
| `npm run test:local` | Changed and related tests without the build step; lightweight local feedback loop |
| `npm run test:boundary` | Boundary, MCP boundary, and process-global projects |
| `npm run test:mcp` | MCP boundary project only |
| `npm run test:acceptance` | Complete CLI and MCP acceptance workflows |
| `npm run test:changed` | Changed tests in non-serial projects via Vitest's import graph |
| `npm run test:watch` | Changed domain, service, adapter, composition, boundary, MCP boundary, conformance, and evaluation tests in watch mode |
| `npm run test:watch:all` | Changed tests from every deterministic project in watch mode |
| `npm run check:changed` | Cached static checks followed by changed tests |
| `npm run check:pr` | Static, generated-document, and complete deterministic PR gate |
| Command | Scope |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `npm run test:local` | Dirty source tests via the import graph, without build; explicit source paths run regardless of Git status |
| `npm run test:focused -- PATH...` | Exact existing test files; compiled boundaries build first, and unmatched paths fail |
| `npm run test:changed` | Source tests affected since the merge base with `origin/main`, including committed and dirty changes |
| `npm run test:fast` | All domain, service, adapter, composition, conformance, and evaluation tests without build |
| `npm run test:boundary` | Boundary, MCP boundary, process boundary, and process-global projects |
| `npm run test:mcp` | MCP boundary project |
| `npm run test:acceptance` | Complete compiled CLI and MCP acceptance workflows |
| `npm run test:watch` | Dirty source tests in watch mode, without build |
| `npm run test:watch:all` | Changed tests from every project; builds at startup, so rebuild after production edits before relying on compiled tests |
| `npm run check:changed` | Cached typecheck/lint and branch-related source feedback |
| `npm run check:pr` | Opt-in complete local deterministic gate and generated-file checks |
| `npm run docs:check` | Committed generated-document freshness |
Changed-test selection is a fast feedback aid, not release evidence. It can
miss behavior connected through runtime registration, generated data, shell
entrypoints, or other relationships that are absent from the import graph.
Use `npm run check:pr` before handing off a contribution.
For example:
Local full-suite Vitest runs are intentionally capped at one worker and one
project at a time. Boundary fixtures own real subprocesses, and this local cap
keeps aggregate memory predictable; CI retains the existing two-worker budget.
```bash
npm run test:local -- src/config.test.ts
npm run test:focused -- tests/acceptance/applications/runtime.test.ts
npm run test:changed -- --base origin/main
npm run test:changed -- --dry-run
```
`test:focused` accepts exact repository-relative test file paths. Source-only
paths do not build; boundary, acceptance, process-global, or unfamiliar `tests/`
paths build conservatively. Tests use Vitest concurrency and isolated
workspaces; commands do not hold a broad test lock. Build and documentation
writers retain checkout-local locks for their shared output files. Explicit
selections do not use `--changed` or permit zero-test success.
The dry-run option reports the chosen merge base, scope and build prerequisite
without executing tests or building. A missing Git base reports how to fetch
it or select another revision.
Changed selection can miss runtime registration, generated data, shell
entrypoints, bridges, or other relationships absent from the import graph.
An empty changed selection means no tests were selected, not verified
correctness. Select relevant boundary files and real-provider lanes explicitly.
Source projects also contain large capacity regressions; `test:fast` promises
no compiled-runtime prerequisite, not a fixed time budget.
Routine iterations and rebases need focused regressions and relevant checks.
Before handing off a PR, run `npm run check` and generated-document checks when
applicable; CI owns the full suite and coverage. Use the full local gate for
broad changes or diagnosing CI, rather than after every edit. Package/install
changes additionally need package verification; provider changes need actual
provider evidence.
Local full-suite Vitest runs use up to two workers and schedule projects one
at a time. Process, acceptance and process-global projects serialize their
files to prevent competing lifecycle observations. CI retains its two-worker
budget.
The pure domain/contracts and recording-port service projects share one worker
module context because their tests own no mutable runtime resources. MCP
boundary files also share the immutable server module graph while creating and
@@ -109,8 +342,7 @@ process-global, and other boundary projects retain per-file isolation.
`npm test`, `npm run docs:check`, and `npm run docs:generate` share
repository-local locks and fail fast when the same class of command is already
running. The `npm test` build is inside that lock. `check:pr` runs its test task
before starting documentation validation, so TypeDoc does not compete with the
full suite for memory.
before starting generated-document validation.
Vitest and Node persistent compile caches are deliberately not enabled by
default. To evaluate repeated local runs, opt in for both cold and warm
@@ -144,5 +376,32 @@ for another.
The PR acceptance target is a median `npm run check:pr` wall time below three
minutes across three warm-build runs on the benchmark host. Keep Vitest caches
cold unless separately identified. A PR that touches packaging or real-system
behavior also requires `npm run verify:package` and the applicable
`verify:*` lanes.
behavior requires the applicable `verify:*` lanes; packaging and installation
changes also require `npm run verify:package`. The full-gate benchmark measures
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.
### 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,
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.
+10 -9
View File
@@ -8,14 +8,14 @@ protocol, authority, and resource-safety constraints, and report their effects.
## Choose the tool shape
| Shape | Use it for | Contract should return |
| -------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `inspect` | Facts about one explicit target, address, object, or resource | Relevant fields, source locations, and facet-level availability |
| `search` / `list` | Finding candidate targets or entities | Stable ordering and useful result context; expose pagination when the result format or caller's query needs it |
| `trace` | Relationships across code, metadata, UI resources, or observations | Typed edges, supporting evidence, and unresolved paths; identify any genuine traversal boundary |
| `compare` | Two explicitly identified artifacts, versions, or evidence sets | Paired identity, comparable coverage, and deltas with evidence |
| `workflow` | A distinct analyst outcome that benefits from composition inside REA | A useful inline result, contributing evidence, and partial/unavailable facets |
| `observe` / `capture` / `replay` | A question that requires runtime behavior | Required authority, launch/attach behavior, real operational constraints, lifecycle, and cleanup status |
| Shape | Use it for | Contract should return |
| --------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `inspect` | Facts about one explicit target, address, object, or resource | Relevant fields, source locations, and facet-level availability |
| `search` / `list` | Finding candidate targets or entities | Stable ordering and useful result context; expose pagination when the result format or caller's query needs it |
| `trace` | Relationships across code, metadata, UI resources, or observations | Typed edges, supporting evidence, and unresolved paths; identify any genuine traversal boundary |
| `compare` | Two explicitly identified artifacts, versions, or evidence sets | Paired identity, comparable coverage, and deltas with evidence |
| `workflow` | A distinct analyst outcome that benefits from composition inside REA | A useful inline result, contributing evidence, and partial/unavailable facets |
| `observe` / `capture` | A question that requires runtime behavior | Required authority, launch/attach behavior, real operational constraints, lifecycle, and cleanup status |
These are task shapes, not required prefixes. Name a tool for the action and
object agents reason about; keep the name distinct from nearby alternatives.
@@ -72,7 +72,8 @@ or model-authored loop when one call can answer the question.
- Separate observed facts from derived and inferred edges. Cite the evidence
supporting every important relationship; preserve unresolved edges.
- Declare read-only, mutation, process, filesystem, network, and UI effects
truthfully in the contract and permission boundary.
truthfully in the contract and enforce the target and lifecycle named in the
request.
- Keep provider-specific types out of provider-neutral domain and application
layers. Normalize supported provider results without implying equal coverage.
+6 -6
View File
@@ -7,7 +7,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
6,src/domain/errors.ts:AnalysisProtocolError,Application protocol fails,Raw constructor message,Can expose protocol internals,"Analysis returned an unreadable result. Retry once; if it continues, run `rea doctor`.",implemented,focused assertions passed; full `npm run check` passed
7,src/domain/errors.ts:HopperProcessError,Analysis subprocess stops,"Hopper bridge stopped unexpectedly with code {exitCode}",Exposes provider bridge and process code,"Analysis could not start or stopped unexpectedly. Run `rea doctor`, then try again.",implemented,focused assertions passed; full `npm run check` passed
8,src/domain/errors.ts:HopperStartError,Analysis subprocess cannot start,Hopper application bridge could not be started,Exposes provider and bridge implementation,"Analysis could not start or stopped unexpectedly. Run `rea doctor`, then try again.",implemented,focused assertions passed; full `npm run check` passed
9,src/domain/errors.ts:BinaryTargetError,Target cannot open,"Cannot open artifact: {reason}",May expose raw reason and lacks checks,"REA could not open that app or binary. Check that the path exists, is readable, and points to a supported file.",implemented,focused assertions passed; full `npm run check` passed
9,src/domain/errors.ts:BinaryTargetError,Target cannot open,"Cannot open artifact: {reason}",May expose raw reason and lacks checks,"Preserve the target admission reason; directories name analyze_javascript_application and rea analyze as available alternatives.",implemented,focused assertions passed; full `npm run check` passed
10,src/domain/errors.ts:ConfigurationError,Environment configuration invalid,Raw constructor message,May expose provider-specific variables and lacks recovery,"REA configuration is invalid. Run `rea doctor` and fix the reported setting.",implemented,focused assertions passed; full `npm run check` passed
11,src/domain/errors.ts:ArtifactOperationError,Artifact inventory or extraction fails,"Artifact {operation} failed: {reason} at {logicalPath} (declared_sha256=...)",Exposes paths hashes and internal reason,"Reason-specific plain message with safe recovery step.",implemented,focused assertions passed; full `npm run check` passed
12,src/domain/errors.ts:EvidenceFileError,Evidence file operation fails,"Evidence bundle {operation} failed: {reason}",Internal reason token gives weak recovery,"Reason-specific plain message with safe recovery step.",implemented,focused assertions passed; full `npm run check` passed
@@ -55,7 +55,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
45.5,src/application/Uninstall.ts:removeClient restore,Configuration update fails and restoring the original also fails,"Configuration could not be updated or restored. Restore its `.rea.backup` manually, then rerun uninstall.","High risk; persistent configuration may be damaged, but retained backup is authoritative",Keep new recovery copy,implemented,verified by injected restoration failure test
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/Doctor.ts:node,Runtime version is outside supported ranges,Install Node.js 22.19+ or 24.11+.,Low risk; exact dependency recovery,Keep current copy,unchanged,verified by doctor unit 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
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
@@ -65,7 +65,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
55,src/application/Setup.ts:optional engine,Setup completes without optional deep-analysis engine,Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis.,Exposes provider terminology but this is an explanatory setup status rather than an error; recovery command is exact,Keep current copy,unchanged,verified by setup unit test
56,src/application/Setup.ts:doctor fallback,Post-setup diagnostics remain unhealthy,Run rea doctor and apply each reported remediation.,Low risk; delegates to structured per-check recovery,Keep current copy,unchanged,verified by exact setup unit test
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/Setup.ts:unsupported Node,Runtime version cannot run setup,Install Node.js 22.19+ or 24.11+ and rerun setup.,Low risk; exact dependency and retry,Keep current copy,unchanged,verified by setup host tests
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
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
@@ -83,11 +83,11 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
74,install.sh:unknown option,Installer receives an unsupported flag,unknown option with caller-provided flag,Low risk; identifies offending input,Keep current copy,unchanged,verified by shell branch inspection
75,install.sh:platform,Installer runs outside macOS or supported Linux,REA supports macOS and selected 64-bit Linux distributions.,Low risk; states support boundary,Keep current copy,unchanged,verified by shell branch inspection
76,install.sh:curl missing,curl is absent,curl is required.,No recovery instruction,"curl is required. Install curl, then rerun this installer.",implemented,verified by exact missing-curl installer test
77,install.sh:node missing,node is absent,Node.js 22.19+ or 24.11+ is required; install it from nodejs.org and retry.,Low risk; exact versions and recovery,Keep current copy,unchanged,verified by shell branch inspection
77,install.sh:node missing,node is absent,"Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from nodejs.org and retry.",Low risk; exact versions and recovery,Keep current copy,unchanged,verified by shell branch inspection
78,install.sh:npm missing,npm is absent,npm is required; install it with Node.js and retry.,Low risk; exact recovery,Keep current copy,unchanged,verified by shell branch inspection
79,install.sh:node unreadable,node exists but does not report a parseable version,could not determine the active Node.js version.,No cause-local recovery,"The active Node.js version could not be read. Check that node works and is on PATH, then retry.",implemented,verified by exact unreadable-Node-version installer test
80,install.sh:Node 22 floor,Node 22 minor version is below 19,Node.js VERSION is unsupported; use Node.js 22.19+ or 24.11+.,Low risk; exact detected version and supported alternatives,Keep current copy,unchanged,verified by exact installer test using Node 20 and branch inspection
81,install.sh:Node 24 floor,Node 24 minor version is below 11,Node.js VERSION is unsupported; use Node.js 24.11 or newer.,Low risk; exact detected version and floor,Keep current copy,unchanged,verified by shell branch inspection
80,install.sh:Node 22 floor,Node 22 minor version is below 19,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact detected version and supported alternatives,Keep current copy,unchanged,verified by exact installer test using Node 20 and branch inspection
81,install.sh:Node 24 floor,Node 24 minor version is below 11,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact detected version and floor,Keep current copy,unchanged,verified by shell branch inspection
82,install.sh:version syntax,Explicit version is not exact semantic version,version must be an exact semantic version.,Low risk; exact correction,Keep current copy,unchanged,verified by shell branch inspection
83,install.sh:release lookup,Latest GitHub release request fails,could not resolve the latest GitHub release.,No fallback despite supporting pinned versions,"The latest REA release could not be resolved. Check network access or pass --version VERSION, then retry.",implemented,verified by exact release-lookup installer test
84,install.sh:release response,Latest release response is not valid expected JSON,Node JSON parser stack followed by the safe release-response message,Leaks a parser stack and buries recovery guidance,The release response was invalid. Retry later or pass --version VERSION.,implemented,verified by exact malformed-release test with parser stderr suppressed
1 harm_rank location trigger current_copy user_risk proposed_replacement implementation_status verification_result
7 6 src/domain/errors.ts:AnalysisProtocolError Application protocol fails Raw constructor message Can expose protocol internals Analysis returned an unreadable result. Retry once; if it continues, run `rea doctor`. implemented focused assertions passed; full `npm run check` passed
8 7 src/domain/errors.ts:HopperProcessError Analysis subprocess stops Hopper bridge stopped unexpectedly with code {exitCode} Exposes provider bridge and process code Analysis could not start or stopped unexpectedly. Run `rea doctor`, then try again. implemented focused assertions passed; full `npm run check` passed
9 8 src/domain/errors.ts:HopperStartError Analysis subprocess cannot start Hopper application bridge could not be started Exposes provider and bridge implementation Analysis could not start or stopped unexpectedly. Run `rea doctor`, then try again. implemented focused assertions passed; full `npm run check` passed
10 9 src/domain/errors.ts:BinaryTargetError Target cannot open Cannot open artifact: {reason} May expose raw reason and lacks checks REA could not open that app or binary. Check that the path exists, is readable, and points to a supported file. Preserve the target admission reason; directories name analyze_javascript_application and rea analyze as available alternatives. implemented focused assertions passed; full `npm run check` passed
11 10 src/domain/errors.ts:ConfigurationError Environment configuration invalid Raw constructor message May expose provider-specific variables and lacks recovery REA configuration is invalid. Run `rea doctor` and fix the reported setting. implemented focused assertions passed; full `npm run check` passed
12 11 src/domain/errors.ts:ArtifactOperationError Artifact inventory or extraction fails Artifact {operation} failed: {reason} at {logicalPath} (declared_sha256=...) Exposes paths hashes and internal reason Reason-specific plain message with safe recovery step. implemented focused assertions passed; full `npm run check` passed
13 12 src/domain/errors.ts:EvidenceFileError Evidence file operation fails Evidence bundle {operation} failed: {reason} Internal reason token gives weak recovery Reason-specific plain message with safe recovery step. implemented focused assertions passed; full `npm run check` passed
55 45.5 src/application/Uninstall.ts:removeClient restore Configuration update fails and restoring the original also fails Configuration could not be updated or restored. Restore its `.rea.backup` manually, then rerun uninstall. High risk; persistent configuration may be damaged, but retained backup is authoritative Keep new recovery copy implemented verified by injected restoration failure test
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/Doctor.ts:node src/application/DoctorDiagnostics.ts:node Runtime version is outside supported ranges Install Node.js 22.19+ or 24.11+. 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. 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
65 55 src/application/Setup.ts:optional engine Setup completes without optional deep-analysis engine Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis. Exposes provider terminology but this is an explanatory setup status rather than an error; recovery command is exact Keep current copy unchanged verified by setup unit test
66 56 src/application/Setup.ts:doctor fallback Post-setup diagnostics remain unhealthy Run rea doctor and apply each reported remediation. Low risk; delegates to structured per-check recovery Keep current copy unchanged verified by exact setup unit test
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/Setup.ts:unsupported Node src/application/SetupHost.ts:unsupported Node Runtime version cannot run setup Install Node.js 22.19+ or 24.11+ and rerun 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. 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
83 74 install.sh:unknown option Installer receives an unsupported flag unknown option with caller-provided flag Low risk; identifies offending input Keep current copy unchanged verified by shell branch inspection
84 75 install.sh:platform Installer runs outside macOS or supported Linux REA supports macOS and selected 64-bit Linux distributions. Low risk; states support boundary Keep current copy unchanged verified by shell branch inspection
85 76 install.sh:curl missing curl is absent curl is required. No recovery instruction curl is required. Install curl, then rerun this installer. implemented verified by exact missing-curl installer test
86 77 install.sh:node missing node is absent Node.js 22.19+ or 24.11+ is required; install it from nodejs.org and retry. Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from nodejs.org and retry. Low risk; exact versions and recovery Keep current copy unchanged verified by shell branch inspection
87 78 install.sh:npm missing npm is absent npm is required; install it with Node.js and retry. Low risk; exact recovery Keep current copy unchanged verified by shell branch inspection
88 79 install.sh:node unreadable node exists but does not report a parseable version could not determine the active Node.js version. No cause-local recovery The active Node.js version could not be read. Check that node works and is on PATH, then retry. implemented verified by exact unreadable-Node-version installer test
89 80 install.sh:Node 22 floor Node 22 minor version is below 19 Node.js VERSION is unsupported; use Node.js 22.19+ or 24.11+. Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Low risk; exact detected version and supported alternatives Keep current copy unchanged verified by exact installer test using Node 20 and branch inspection
90 81 install.sh:Node 24 floor Node 24 minor version is below 11 Node.js VERSION is unsupported; use Node.js 24.11 or newer. Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Low risk; exact detected version and floor Keep current copy unchanged verified by shell branch inspection
91 82 install.sh:version syntax Explicit version is not exact semantic version version must be an exact semantic version. Low risk; exact correction Keep current copy unchanged verified by shell branch inspection
92 83 install.sh:release lookup Latest GitHub release request fails could not resolve the latest GitHub release. No fallback despite supporting pinned versions The latest REA release could not be resolved. Check network access or pass --version VERSION, then retry. implemented verified by exact release-lookup installer test
93 84 install.sh:release response Latest release response is not valid expected JSON Node JSON parser stack followed by the safe release-response message Leaks a parser stack and buries recovery guidance The release response was invalid. Retry later or pass --version VERSION. implemented verified by exact malformed-release test with parser stderr suppressed
@@ -1,5 +1,5 @@
{
"ledger_id": "ecl_b01c0b28362b48128765d176113471a5a82637fca0acfcd3182971155942e0d4",
"ledger_id": "ecl_132e857a8a93f0b366cd72f9cfa1c338ffd4100460a7834ef69abff936c84472",
"records": [
{
"claim_id": "managed.application-graph-projection",
@@ -78,13 +78,6 @@
"ev_f974f4b9d039cda841894c6173cae86427ecef48f4e7a4d6c73edf7b23af2b2a"
]
},
{
"claim_id": "managed.runtime-correlation-admission-plan",
"status": "pass",
"evidence_ids": [
"ev_3e546c20493faf9696502171a5cfe9504569cffb22f0561ee3000f0027b9bbd8"
]
},
{
"claim_id": "managed.unicode-obfuscated-identifiers",
"status": "pass",
@@ -101,8 +94,8 @@
}
],
"summary": {
"total": 14,
"pass": 12,
"total": 13,
"pass": 11,
"fail": 0,
"unsupported": 2,
"truncated": 0,
@@ -1,5 +1,5 @@
{
"manifest_id": "ecm_26db97ee1f42255040febd543bb2f3feac773f48d5ef38bd6651cd8770b36013",
"manifest_id": "ecm_d68a1575bdaa1a044ccce8e27c5dd335612f909bc81c52ce3089b576fd772940",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "28122d8fd448db430249431aedfa9d3d865da8a323b703631fec5c44041d9d77"
"sha256": "d2d971cc67bb0f7b80b4e39382a6bd1146445ed1dbf52043f89e2da2985c97ba"
}
],
"claims": [
@@ -169,20 +169,6 @@
"version": "1"
}
},
{
"claim_id": "managed.runtime-correlation-admission-plan",
"scenario": {
"id": "runtime-correlation-admission-plan",
"version": 1
},
"artifact_sha256s": [
"cead4cc99733e4e56a5fd0cec8a0813bbc24e249aa72a10eca4c47ee3b0e129d"
],
"provider": {
"id": "rea-managed-conformance",
"version": "1"
}
},
{
"claim_id": "managed.unicode-obfuscated-identifiers",
"scenario": {
+86
View File
@@ -0,0 +1,86 @@
# Captured website scripts and static JavaScript analysis
`export_web_scripts` bridges retained browser sources into the existing local
JavaScript analyzer. Both CLI and MCP use the same workflow. It reads a saved
`inspect_web_page` or `capture_browser_scenario` capture and writes scripts plus a
verified manifest into one caller-selected, absent directory.
## Capture and export
For passive inspection, select `include_script_sources: true` (CLI:
`--include-script-sources`). For scenarios, select
`capture.network.response_body: true`. Save either the complete Evidence record
or its normalized result as UTF-8 JSON. The compact MCP wrapper's `evidence`
field contains the complete Evidence record.
```sh
rea export-web-scripts /analysis/capture.json /analysis/exported-scripts --json
rea analyze-javascript-application /analysis/exported-scripts/files --json
```
MCP uses `export_web_scripts` with `capture_path` and `output_directory` absolute
filesystem paths. Its inline result includes `analysis_input`, which can be
passed directly to `analyze_javascript_application`. When no script bytes are
exportable, `analysis_input` is null and the manifest retains the unavailable
sources and their reasons. Capture again with the corresponding content option.
The destination must not exist. Use a new directory for each capture; an
existing directory or symlink is rejected without overwriting its contents.
REA creates private output, verifies every written file by digest and durable
readback, and removes its own partial output on failure or cancellation.
A failed rollback reports residual paths.
## Sources and mappings
Every source is returned inline with its URL and its script/frame identity or
request transaction and event sequence references. The result also contains
the input capture's byte digest, original coverage, source Evidence link when
provided, and the persisted manifest's digest and byte count. A saved Evidence
envelope must have a valid semantic identifier and matching operation.
Exported `relative_path` values are relative to `analysis_input.input_path`.
The persisted `manifest.json` contains the same inline manifest fields, excluding
its own descriptor. It does not duplicate script bytes.
- Passive sources are the Debugger-exposed text encoded as UTF-8, with the
verified source digest and source-map declaration retained. Unavailable or
explicitly non-JavaScript sources remain unavailable.
- Scenario sources use observed requests classified as `script`, matched to
completed response bytes by transaction and event identity. URLs never select
the response. Binary and empty retained bytes are written exactly.
- Ordinary query-free `.js`, `.mjs`, and `.cjs` URL paths are arranged under a
directory for each origin, preserving their relative module layout when it
is unambiguous.
- Competing versions, query/fragment variants, case and file/directory prefix
collisions, inline sources, and unrepresentable paths use unique isolated
names with a stated reason. Missing competing sources also prevent selection
of an apparently canonical version.
## Interpretation and limits
Export does not fetch URLs or execute JavaScript. Retained Debugger sources and
response bytes do not establish execution or a complete website. Response bytes
are browser-decoded bytes; they do not represent transfer encoding. Declared
redaction is preserved and can change JavaScript syntax; passive captures do
not report a byte-redaction flag, so that value remains unknown.
The existing analyzer inventories and parses the actual exported files. It can
resolve ordinary captured relative modules, but does not implement browser
import maps, root-relative URL loading, remote imports, or runtime behavior.
Its local resolver may strip import queries/fragments; isolated variants are
kept away from canonical paths to prevent those from selecting a different
observed version. Missing dependencies remain unknown. Source maps are not
fetched or reconstructed during export.
The export Evidence describes a derivation from the saved capture. Subsequent
application-analysis Evidence describes the local exported artifact. Their
identities and authorities remain separate.
## Verification
`npm run verify:browser:scripts` exercises active capture, exact source export,
manifest readback, query variants, and relative-module analysis through a real
browser, CLI, and stdio MCP. Set `REA_BROWSER_EXECUTABLE` to an installed
Chrome-family browser. The verifier accepts an optional installed REA entrypoint
argument for package verification. `npm run verify:browser` additionally covers
passive `inspect_web_page` export through both public adapters.
+117 -100
View File
@@ -1,156 +1,173 @@
# Windows Ghidra P0
Status: experimental. This boundary is useful for approved local fixtures, but
it is not general Windows REA support and must not be used for hostile,
sensitive, security-decision, or concurrently mutable targets.
This guide describes repository main. npm 4.1.0 includes the Windows
native bundle; check the [release boundary](installation.md#released-package-and-main)
before applying these instructions to an older published package.
Status: experimental Windows x64 support for the read-only P0 boundary. Windows
package builds bundle native process, filesystem, and DACL controls. REA
uses them automatically when the analyst selects a Ghidra operation; no
additional permission flag or degraded mode is required.
## Supported boundary
The P0 accepts exactly:
- a Windows x64 host;
- Node.js 22.19+ or 24.11+;
- 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 explicit native, non-managed, non-DLL x86-64 PE application; and
- the existing 19 read-only Ghidra inventory and function-analysis operations.
- the 25 read-only Ghidra inventory, memory, and function-analysis operations.
Hopper, Ghidra GUI state, mutation, controlled JavaScript replay, process
capture, browser/Electron observation, artifact extraction, and general
Windows feature parity are not implied by this boundary. Managed PE/CLI
inspection remains a separate execution-free provider and is not routed
through Windows Ghidra P0.
Loaded memory reads and file offsets preserve Ghidra's observed source mappings.
PE load-image inspection returns measurements with an explicit unsupported
attestation status; independent format-specific verification currently supports
DOS MZ only.
REA does not install or upgrade Ghidra, Java, Python, npm, Node.js, Hopper, or
another package on Windows. The production adapter uses the packaged Java
`HeadlessScript`; Python and PyGhidra are not prerequisites.
This boundary does not establish general Windows feature parity. Ghidra GUI
state, annotations, other target architectures and formats, and Hopper-only
operations remain unavailable through this provider. Managed PE/CLI inspection
uses its separate execution-free provider.
REA does not install or upgrade Ghidra, Java, Python, npm, Node.js, Hopper, or a
compiler. The adapter uses the packaged Java `HeadlessScript`; Python and
PyGhidra are not prerequisites. Users do not build the native addon.
REA selects its packaged bridge scripts by explicit path. Same-named files or
directories in the caller's working directory do not select a different
script or require users to clean that directory before analysis.
## Configuration
Install REA, Ghidra, and JDK 21 separately. In PowerShell:
```powershell
npm install --global rea-agents
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
$env:JAVA_HOME = "C:\Program Files\Java\jdk-21"
$env:JAVA_HOME = "C:\tools\jdk-21"
$env:REA_ANALYSIS_PROVIDER = "ghidra"
rea doctor --json
rea providers --json
rea inspect "C:\approved-fixtures\sample.exe" --provider ghidra --format json
rea inspect "C:\fixtures\sample.exe" --provider ghidra --format json
```
`rea doctor` checks the x64 host, exact Ghidra release,
`support\analyzeHeadless.bat`, `java.exe`, `javac.exe`, JDK bitness, and JDK
major version. `rea setup` remains unavailable on Windows and makes no changes.
`rea doctor` distinguishes installation prerequisites from native artifact or
OS failures. It checks the Ghidra release, headless launcher, full JDK, x64
host, and native controls. A missing or mismatched addon requires the matching
Windows x64 package; changing MCP registration cannot supply that artifact.
A filesystem or access failure identifies the failed native constraint.
Other doctor checks, including unconfigured agent integrations, can remain
unready while Ghidra itself is available.
For an MCP client, register the resolved Node entry point and preserve the
three environment variables above. A representative configuration is:
`rea setup` can register supported agent integrations and the bundled skill.
Its existing preview and approval apply to configuration writes. Setup
preserves valid Ghidra and Java settings even when a separate runtime
prerequisite is unavailable.
For MCP, register the resolved Node entry point and the same provider settings:
```json
{
"mcpServers": {
"rea-windows-ghidra-p0": {
"command": "C:\\Program Files\\nodejs\\node.exe",
"args": [
"C:\\Users\\analyst\\AppData\\Roaming\\npm\\node_modules\\rea-agents\\scripts\\rea.mjs",
"mcp"
],
"rea": {
"command": "C:\\tools\\node\\node.exe",
"args": ["C:\\tools\\rea\\scripts\\rea.mjs", "mcp"],
"env": {
"REA_ANALYSIS_PROVIDER": "ghidra",
"GHIDRA_INSTALL_DIR": "C:\\tools\\ghidra_12.1.4_PUBLIC",
"JAVA_HOME": "C:\\Program Files\\Java\\jdk-21"
"JAVA_HOME": "C:\\tools\\jdk-21"
}
}
}
}
```
Resolve the actual global package path on the intended machine; do not copy the
example path blindly. Restart the MCP client after changing its registration.
These paths are examples. Use the actual package location and restart the MCP
client after registration changes. Cold Ghidra import can exceed a client's
short default request timeout; the real verification lane allows the existing
330-second provider startup deadline to complete.
## Target and import identity
## Native authority and target identity
PE parsing records architecture, COFF executable role, and whether the CLI
header data-directory entry is non-empty. Header classification and SHA-256
are read from the same open file handle. Windows admission then rejects:
The verified boundary excludes SUBST aliases and changing drive-letter/DOS-device
namespaces. REA does not claim a separate SUBST detector. Mounted-folder entries
that expose reparse tags fail component admission; broader namespace variants
have not been established by this lane. Requested paths, final handle paths,
volume serials, and file IDs remain separate observations.
- non-PE formats;
- x86, ARM, ARM64, or unknown architectures;
- DLL/shared-library and non-executable image roles;
- managed or incompletely classified PE targets; and
- malformed optional headers or data-directory declarations.
The loader validates package version, ABI, Node-API compatibility, PE machine,
and artifact SHA-256 before loading the package-owned addon. Native failure
reasons remain distinct from a valid installation and an unsupported target.
After admission, REA copies the target into the ephemeral Ghidra runtime and
requires the snapshot digest to match the admitted SHA-256. Ghidra imports only
that snapshot. Before serving a request, the Java bridge requires Ghidra's
`Program.getExecutableSHA256()` observation to match the same digest. A path
replacement that changes bytes therefore fails before Evidence is created.
PE header classification and SHA-256 come from the same open file. The Windows
provider admits native x86-64 PE applications and rejects unsupported roles,
architectures, managed images, and malformed headers. The original selected
source coordinate is preserved for native admission.
This linkage does not establish Windows reparse-point or hostile-path safety.
The current implementation has no handle-based `CreateFileW` authority backend
and no verified no-follow equivalent for every path component.
Admission opens and checks every path component without following a reparse
point. It retains handles, verifies local NTFS semantics, and observes volume
serial, 128-bit file ID, final handle path, requested path, and size. Reparse
paths, alternate streams, ambiguous coordinates, UNC paths, and other
filesystems are rejected with the actual constraint.
## Local transport and lifecycle
The snapshot streams from the admitted source handle on a background worker.
Its digest must match the parsed target digest. Ghidra imports that private,
immutable snapshot, and the bridge requires the imported program SHA-256 to
match before serving operations. Source and snapshot identities remain
separate diagnostic observations. Snapshot cancellation is checked between
64 KiB reads and cleanup waits for worker settlement.
Linux continues to use a mode-restricted Unix socket. Windows uses a Java
listener bound to `127.0.0.1` on an ephemeral port. The bridge atomically
publishes a bounded endpoint record containing only the literal loopback host
and port. The random 256-bit request token remains only in the
ephemeral session descriptor and every request is authenticated.
## Private runtime, transport, and lifecycle
`analyzeHeadless.bat` is invoked through an absolute `cmd.exe` with a fixed
argument sequence, command extensions enabled for the Ghidra script, delayed
expansion disabled, and Node shell execution disabled. Every batch token is
quoted. Paths containing command-interpreter metacharacters are rejected
instead of escaped heuristically.
Runtime creation installs a protected DACL permitting exactly the current user
and SYSTEM. Readback checks the actual owner and ACEs by handle. Descriptors
and snapshots remain locked until owner cleanup. Child files inherit the
private boundary. Analysis runs with the caller's authority; these controls
are not a sandbox for the Ghidra engine or other processes using the same user.
Close, cancellation, timeout, protocol failure, or provider exit removes the
project and runtime root. Windows P0 uses bounded `taskkill /T /F` process-tree
termination. It does not assign Ghidra to a Job Object and cannot make the same
PID-reuse and descendant-ownership claim as the POSIX process-group backend.
Windows uses an authenticated listener bound to `127.0.0.1` on an ephemeral
port. The endpoint record contains only the literal loopback host and port.
The bridge writes a new record and closes its writer before the native reader
can admit it. The reader refuses concurrent writers and verifies its opened
object's private DACL. The random 256-bit bearer token stays in the private,
immutable session descriptor and is redacted from diagnostic output.
## Known security gates
The batch launcher uses an absolute `cmd.exe`, fixed switches, delayed
expansion disabled, and quoted tokens. Command-interpreter metacharacters in
launch paths are unsupported. JVM home and temporary properties use quoted
`JDK_JAVA_OPTIONS`, preserving spaces. `APPDATA` and `LOCALAPPDATA` point into
the private runtime, isolating Ghidra preferences, logs, and caches.
The following remain explicitly unavailable:
- verified current-user-only DACLs for runtime directories, descriptors, logs,
snapshots, and endpoint records;
- a DACL-protected Windows named pipe or equivalent OS-identity-restricted IPC;
- reparse-point-safe, handle-based input and output authority;
- Job Object assignment before child execution and kill-on-close proof;
- general extraction containment and filesystem-sensitive workflows; and
- independent security acceptance for hostile or mutable targets.
Successful symlink creation or ConPTY availability does not satisfy any of
these gates. Inspect the machine-readable report with:
```powershell
npm run probe:windows-capabilities
```
Process creation assigns a suspended child atomically to an owned Job Object.
Membership and kill-on-owner-close policy are verified before resume.
Breakaway is disabled. Only the child's default file owner is normalized;
the caller token stays unchanged. Caller CPU affinity and priority are
preserved. Close, cancellation, timeout, startup failure, and provider failure
terminate the job and verify all members have settled before runtime cleanup.
Cleanup removes admitted objects by handle and unlinks reparse entries without
following their targets. Abrupt owner death kills the job but can leave
private runtime files; crash-time transactional deletion is not claimed.
## Verification
Hosted CI builds, type-checks, runs the curated Windows tests, records the named
host capability report, packs the real npm artifact, installs it in isolation,
and compares the packaged MCP catalog with `TOOL_CONTRACTS`.
The controlled real-engine lane is intentionally separate:
See [native controls](../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.
```powershell
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
$env:JAVA_HOME = "C:\Program Files\Java\jdk-21"
npm ci
npm run verify:windows-native
npm run verify:ghidra:windows
npm run verify:ghidra:windows:package -- C:\fixtures\installed-rea C:\fixtures\sample.exe
```
The verifier creates an ignored deterministic x86-64 PE fixture, checks its
fixed SHA-256, opens Ghidra 12.1.4 through the production Java bridge, exercises
all 19 admitted operations, verifies the target/snapshot/import digest chain,
and checks endpoint, project, process, and runtime cleanup. The GitHub workflow
accepts only the fixed `real-ghidra-windows` repository-dispatch event against
default-branch `main`, a protected `real-ghidra-windows` environment, and a
self-hosted runner labelled `Windows`, `x64`, and `ghidra-12-1-2`.
The package lane must run with an ordinary, non-elevated user token. It exercises
CLI inspection and every admitted MCP operation, validates named canonical
input/output contracts, checks the source digest stays unchanged, and verifies
runtime cleanup. Use a source-owned fixture, one analysis process at a time,
and a bounded heap for real-provider verification.
Passing this verifier proves the bounded P0 operation claim. It does not close
the DACL, reparse-point, named-pipe, or Job Object gates above.
The controlled GitHub workflow runs only default-branch code on its protected
self-hosted Windows environment. A separate Linux job builds the matching
native artifact. Hosted Windows CI verifies native controls and packaged
startup separately from real Ghidra acceptance. Broader hostile-target,
concurrently mutable-path, extraction, and IPC-security claims require their
own acceptance and are not implied by this P0 result.
-28
View File
@@ -1,28 +0,0 @@
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist/**", "node_modules/**"] },
eslint.configs.recommended,
...tseslint.configs.strictTypeChecked,
{
files: ["**/*.ts"],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-confusing-void-expression": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-unnecessary-condition": "error",
"@typescript-eslint/no-unsafe-type-assertion": "error",
"@typescript-eslint/only-throw-error": "error",
"@typescript-eslint/require-await": "error",
"@typescript-eslint/switch-exhaustiveness-check": "error",
},
},
);
+4 -6
View File
@@ -55,17 +55,15 @@ fi
platform="$(uname -s)"
[[ "$platform" == "Darwin" || "$platform" == "Linux" ]] || fail "REA supports macOS and selected 64-bit Linux distributions."
command -v curl >/dev/null 2>&1 || fail "curl is required. Install curl, then rerun this installer."
command -v node >/dev/null 2>&1 || fail "Node.js 22.19+ or 24.11+ is required; install it from https://nodejs.org and retry."
command -v node >/dev/null 2>&1 || fail "Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from https://nodejs.org and retry."
command -v npm >/dev/null 2>&1 || fail "npm is required; install it with Node.js and retry."
node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
IFS=. read -r node_major node_minor _node_patch <<<"$node_version"
[[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "the active Node.js version could not be read. Check that node works and is on PATH, then retry."
if ! ((node_major == 22 && node_minor >= 19 || node_major >= 24)); then
fail "Node.js $node_version is unsupported; use Node.js 22.19+ or 24.11+."
fi
if ((node_major == 24 && node_minor < 11)); then
fail "Node.js $node_version is unsupported; use Node.js 24.11 or newer."
if ! [[ "$_node_patch" =~ ^[0-9]+(\+[0-9A-Za-z.-]+)?$ ]] ||
! ((node_major == 22 && node_minor >= 19 || node_major == 24 && node_minor >= 11 || node_major >= 26)); then
fail "Node.js $node_version is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+."
fi
if [[ -n "$version" ]]; then
-7
View File
@@ -3,14 +3,7 @@
"entry": ["src/main.ts", "src/cli.ts", "src/contracts/errorSchemas.ts"],
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
"ignoreDependencies": ["electron"],
"ignoreBinaries": ["ps"],
"ignoreExportsUsedInFile": true,
"ignoreIssues": {
"src/domain/mobileApplicationInvestigation.ts": ["types"],
"src/domain/objcSwiftMetadata.ts": ["types"],
"src/domain/interfaceBuilderGraph.ts": ["types"],
"src/domain/nativeInvestigationGraph.ts": ["types"]
},
"rules": {
"files": "error",
"dependencies": "error",
+107
View File
@@ -0,0 +1,107 @@
# Windows native controls
REA ships one Windows x64 Node-API 8 addon. Users do not compile it or install
another runtime. Ghidra and a full JDK remain bring-your-own prerequisites.
The addon uses Windows 10+ APIs and admits fixed local NTFS volumes only.
The package owns the artifact path. The loader checks the package version,
ABI, Node-API compatibility, PE architecture, and SHA-256 before loading it.
There is no environment override for loading an arbitrary native module.
## Build
The artifact lane requires Linux, MinGW-w64 `x86_64-w64-mingw32-g++`,
`x86_64-w64-mingw32-dlltool`, and `tar`. It never installs these commands.
Node-API headers are reused from the build cache or downloaded from the
pinned official Node release with its published SHA-256 checked.
```sh
npm run build:windows-native
npm run verify:windows-native:artifact
npm pack
```
`build/` is ignored by Git. The npm file inventory includes only the addon and
its manifest. Release CI builds both, and `prepublishOnly` rejects a missing,
stale, mismatched, or wrong-architecture artifact. A development tarball built
without this lane reports Windows native controls as unavailable.
## Boundaries
- Opaque Node objects carry a native type tag, resource kind, and closed state.
A pathname, serialized object, or forged handle cannot grant authority.
- Input admission opens every component with `OPEN_REPARSE_POINT`, verifies
the actual NTFS object, and retains handles that deny replacement and source
writing. An admitted child keeps each ancestor nonempty; ancestor handles
permit unrelated writes while NTFS rejects in-place reparse conversion. Volume serial, 128-bit file ID, final handle path,
requested path, size, and digest observations are preserved separately.
- New runtime roots and private child objects are created relative to admitted
parent handles with `NtCreateFile`. A protected DACL and current-user owner
are installed at creation. Readback permits exactly the current user and SYSTEM. Child
files inherit this boundary; private files are also checked by handle.
- Snapshots stream through a 64 KiB buffer on a Node async worker. Cancellation
is checked between reads, cleanup waits for settlement, and the digest
commits to the bytes written. Descriptor and snapshot handles remain held
until runtime cleanup.
Snapshot ownership is single-flight per runtime: an overlapping request fails
with `ERROR_BUSY` before acquiring cancellation ownership. The TypeScript owner
records only the accepted promise and abort listener; aborting a rejected copy
must not cancel the active copy.
- `PROC_THREAD_ATTRIBUTE_JOB_LIST` assigns a suspended child atomically to an
unnamed, non-inherited Job Object. Membership and kill-on-close policy are
checked before resume. Breakaway is disabled, and cleanup waits for all
job members and captured output, including detached descendants.
- Only the suspended child's default file owner is normalized. Caller token
privileges and ownership remain unchanged. Caller CPU affinity and process
priority are preserved in the child. Environment names are sorted and
deduplicated using Windows ordinal comparison, preserving their native Unicode
semantics and caller-selected values.
- Cleanup walks locked directory objects, removes files by handle, and unlinks
reparse entries without traversing their targets. Normal close, cancellation,
startup failure, and timeout release provider resources. Abrupt owner death
kills the owned job; it can leave private runtime files for later operator
cleanup and does not claim transactional deletion after a crash.
Windows failures retain their constraint, requested coordinate, Win32 code,
and system message. Unsupported filesystems and reparse paths are distinct
from OS access denial and missing packaged controls.
Ordinary drive-absolute paths accept backslashes, forward slashes, or mixed
separators. File identity retains the original requested spelling. Separator
translation does not resolve dot components, repeated separators, or extend
the supported device/UNC namespaces; existing admission checks still apply.
The bearer-token descriptor remains under its immutable private lease until
`runtime_close` on Windows; POSIX removes the descriptor after the bridge reads
it. Job ownership guarantees process termination on owner death, not deletion
of private files after a crash. SUBST drive aliases and changing DOS-device
namespaces are outside the verified P0 scope and are not detected as a separate
alias policy. Mounted-folder paths that report a reparse tag are rejected by
component admission; broader namespace variants remain unverified. Preserve
requested and final handle coordinates rather than treating them as one identity.
## Real verification
```powershell
npm run verify:windows-native
npm run verify:ghidra:windows
```
The native lane checks DACL readback independently, source write/replacement
rejection, unrelated sibling renames, reparse rejection, immutable snapshots,
asynchronous cancellation, cleanup containment,
descendant termination, unrelated-process preservation, normal exit, owner
close, and forced owner exit. An optional independently built
`tests/fixtures/windows/processBoundary.cc` executable adds breakaway and
caller/child token observations and an in-place ancestor reparse attempt:
```powershell
node scripts/verify-windows-native.mjs . C:\fixtures\process-boundary.exe
```
`nonAdminRunner.cc` is a test-only launcher for an elevated development shell.
It duplicates an existing non-elevated Explorer token belonging to the same
user, creates private temporary window-station objects, and launches the test
inside a kill-on-close fixture job. It does not create accounts, modify host
policy, or change the caller's token. A real-provider claim must additionally
verify the packaged CLI and MCP on that ordinary-user token.
+123
View File
@@ -0,0 +1,123 @@
#include "authority.hpp"
namespace rea {
static const napi_type_tag RESOURCE_TAG = {0x45fcbf0b49e84fe3ULL, 0x9e3e5074130de843ULL};
void check(napi_status status) {
if (status != napi_ok) throw Failure("Invalid native boundary value", L"", ERROR_INVALID_PARAMETER);
}
napi_value object(napi_env env) { napi_value value; check(napi_create_object(env, &value)); return value; }
napi_value null(napi_env env) { napi_value value; check(napi_get_null(env, &value)); return value; }
napi_value string(napi_env env, const std::string& text) {
napi_value value; check(napi_create_string_utf8(env, text.data(), text.size(), &value)); return value;
}
napi_value string(napi_env env, const std::wstring& text) {
static_assert(sizeof(wchar_t) == sizeof(char16_t));
napi_value value;
check(napi_create_string_utf16(env, reinterpret_cast<const char16_t*>(text.data()), text.size(), &value));
return value;
}
napi_value number(napi_env env, double numeric) {
napi_value value; check(napi_create_double(env, numeric, &value)); return value;
}
napi_value boolean(napi_env env, bool flag) {
napi_value value; check(napi_get_boolean(env, flag, &value)); return value;
}
void set(napi_env env, napi_value target, const char* key, napi_value value) {
check(napi_set_named_property(env, target, key, value));
}
std::wstring wide(napi_env env, napi_value value) {
size_t size = 0;
check(napi_get_value_string_utf16(env, value, nullptr, 0, &size));
require(size <= 32767, "Windows value exceeds the native string limit", L"", ERROR_INVALID_PARAMETER);
std::vector<char16_t> buffer(size + 1);
check(napi_get_value_string_utf16(env, value, buffer.data(), buffer.size(), &size));
std::wstring result(reinterpret_cast<const wchar_t*>(buffer.data()), size);
require(result.find(L'\0') == std::wstring::npos, "Windows value contains NUL", L"", ERROR_INVALID_PARAMETER);
return result;
}
double numeric(napi_env env, napi_value value) {
double result; check(napi_get_value_double(env, value, &result)); return result;
}
std::vector<napi_value> array(napi_env env, napi_value value) {
bool valid; check(napi_is_array(env, value, &valid));
require(valid, "Native arguments must be an array", L"", ERROR_INVALID_PARAMETER);
uint32_t length; check(napi_get_array_length(env, value, &length));
std::vector<napi_value> result(length);
for (uint32_t index = 0; index < length; ++index)
check(napi_get_element(env, value, index, &result[index]));
return result;
}
static void finalize(napi_env, void* data, void*) { delete static_cast<Resource*>(data); }
napi_value wrap(napi_env env, std::unique_ptr<Resource> value) {
auto result = object(env);
check(napi_type_tag_object(env, result, &RESOURCE_TAG));
check(napi_wrap(env, result, value.get(), finalize, nullptr, nullptr));
value.release();
return result;
}
Resource& resource(napi_env env, napi_value value, Kind kind) {
bool tagged = false;
check(napi_check_object_type_tag(env, value, &RESOURCE_TAG, &tagged));
require(tagged, "Native handle does not belong to REA", L"", ERROR_INVALID_HANDLE);
void* pointer = nullptr; check(napi_unwrap(env, value, &pointer));
auto& result = *static_cast<Resource*>(pointer);
require(result.kind == kind && !result.closed, "Native handle is closed or has the wrong kind", L"", ERROR_INVALID_HANDLE);
return result;
}
napi_value failureValue(napi_env env, const Failure& failure) {
wchar_t* message = nullptr;
FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, failure.win32, 0, reinterpret_cast<wchar_t*>(&message), 0, nullptr);
std::wstring detail = message == nullptr ? L"" : message;
if (message != nullptr) LocalFree(message);
const std::wstring constraint(failure.constraint.begin(), failure.constraint.end());
auto text = string(env, constraint + (failure.path.empty() ? L"" : L" [" + failure.path + L"]") +
L" (Win32 " + std::to_wstring(failure.win32) + L"): " + detail);
napi_value error; napi_create_error(env, nullptr, text, &error);
const char* code = failure.win32 == ERROR_FILE_NOT_FOUND || failure.win32 == ERROR_PATH_NOT_FOUND ? "ENOENT"
: failure.win32 == ERROR_ACCESS_DENIED ? "EACCES" : "ERR_REA_WINDOWS_NATIVE";
set(env, error, "code", string(env, std::string(code)));
set(env, error, "constraint", string(env, failure.constraint));
set(env, error, "requestedPath", string(env, failure.path));
set(env, error, "win32Code", number(env, failure.win32));
set(env, error, "win32Message", string(env, detail));
return error;
}
static napi_value call(napi_env env, napi_callback_info info) {
try {
std::array<napi_value, 2> args;
size_t count = args.size();
check(napi_get_cb_info(env, info, &count, args.data(), nullptr, nullptr));
require(count == 2, "Native call requires operation and arguments", L"", ERROR_INVALID_PARAMETER);
const auto operation = wide(env, args[0]);
const auto values = array(env, args[1]);
if (operation == L"inspect") {
require(values.empty(), "Inspect takes no arguments", L"", ERROR_INVALID_PARAMETER);
return inspect(env);
}
if (operation.rfind(L"process_", 0) == 0) return processCall(env, operation, values);
return filesystemCall(env, operation, values);
} catch (const Failure& failure) {
auto error = failureValue(env, failure);
napi_throw(env, error);
return nullptr;
} catch (const std::exception& failure) {
napi_throw_error(env, "ERR_REA_WINDOWS_NATIVE", failure.what());
return nullptr;
}
}
static napi_value initialize(napi_env env, napi_value exports) {
napi_value function;
if (napi_create_function(env, "call", NAPI_AUTO_LENGTH, call, nullptr, &function) != napi_ok) return nullptr;
if (napi_set_named_property(env, exports, "call", function) != napi_ok) return nullptr;
return exports;
}
} // namespace rea
NAPI_MODULE(NODE_GYP_MODULE_NAME, rea::initialize)
+130
View File
@@ -0,0 +1,130 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include <aclapi.h>
#include <bcrypt.h>
#include <node_api.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <cstdint>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace rea {
// Translate ordinary DOS drive separators without resolving components or
// changing extended, device, UNC, relative, or drive-relative namespaces.
inline std::wstring ordinaryDriveSeparators(std::wstring path) {
if (path.size() >= 3 && path[1] == L':' &&
((path[0] >= L'A' && path[0] <= L'Z') || (path[0] >= L'a' && path[0] <= L'z')) &&
(path[2] == L'\\' || path[2] == L'/'))
std::replace(path.begin(), path.end(), L'/', L'\\');
return path;
}
struct Failure : std::runtime_error {
DWORD win32;
std::wstring path;
std::string constraint;
Failure(std::string constraint, std::wstring path = L"", DWORD code = GetLastError())
: std::runtime_error(constraint), win32(code), path(std::move(path)),
constraint(std::move(constraint)) {}
};
inline void require(bool condition, const char* constraint, const std::wstring& path = L"") {
if (!condition) {
const auto code = GetLastError();
throw Failure(constraint, path, code);
}
}
inline void require(bool condition, const char* constraint, const std::wstring& path, DWORD code) {
if (!condition) throw Failure(constraint, path, code);
}
class Handle {
HANDLE value_ = INVALID_HANDLE_VALUE;
public:
Handle() = default;
explicit Handle(HANDLE value) : value_(value) {}
~Handle() { reset(); }
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
Handle(Handle&& other) noexcept : value_(other.release()) {}
Handle& operator=(Handle&& other) noexcept { reset(other.release()); return *this; }
HANDLE get() const { return value_; }
bool valid() const { return value_ != INVALID_HANDLE_VALUE && value_ != nullptr; }
HANDLE release() { return std::exchange(value_, INVALID_HANDLE_VALUE); }
void reset(HANDLE next = INVALID_HANDLE_VALUE) {
if (valid()) CloseHandle(value_);
value_ = next;
}
};
enum class Kind { File, Runtime, Process };
struct Resource {
const Kind kind;
bool closed = false;
explicit Resource(Kind value) : kind(value) {}
virtual ~Resource() = default;
};
struct File : Resource {
std::wstring requestedPath;
std::wstring path;
std::vector<Handle> handles;
explicit File(std::wstring path) : Resource(Kind::File), requestedPath(path), path(std::move(path)) {}
HANDLE get() const { return handles.back().get(); }
};
struct Runtime : Resource {
std::wstring path;
std::vector<Handle> parents;
Handle directory;
std::vector<Handle> immutableFiles;
// Main-thread admission is single-flight; only the accepted copy owns the
// atomic cancellation flag consumed by its async worker. ERROR_BUSY rejects
// another copy before it can reset that flag or replace pending ownership.
bool snapshotPending = false;
std::atomic_bool snapshotCancelled{false};
Runtime() : Resource(Kind::Runtime) {}
};
struct Process : Resource {
Handle job, process, stdoutRead, stderrRead;
DWORD pid = 0;
Process() : Resource(Kind::Process) {}
};
void check(napi_status status);
napi_value failureValue(napi_env env, const Failure& failure);
napi_value object(napi_env env);
napi_value string(napi_env env, const std::string& value);
napi_value string(napi_env env, const std::wstring& value);
napi_value number(napi_env env, double value);
napi_value boolean(napi_env env, bool value);
napi_value null(napi_env env);
void set(napi_env env, napi_value object, const char* key, napi_value value);
std::wstring wide(napi_env env, napi_value value);
double numeric(napi_env env, napi_value value);
std::vector<napi_value> array(napi_env env, napi_value value);
napi_value wrap(napi_env env, std::unique_ptr<Resource> resource);
Resource& resource(napi_env env, napi_value value, Kind kind);
std::unique_ptr<File> openFile(const std::wstring& path, DWORD access = GENERIC_READ,
bool directory = false);
napi_value identity(napi_env env, HANDLE handle, const std::wstring& path);
std::unique_ptr<Runtime> createRuntime(const std::wstring& parent, const std::wstring& prefix);
void closeRuntime(Runtime& runtime);
napi_value filesystemCall(napi_env env, const std::wstring& operation,
const std::vector<napi_value>& args);
std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::wstring& commandLine,
const std::wstring& cwd, const std::vector<std::wstring>& environment);
napi_value processCall(napi_env env, const std::wstring& operation,
const std::vector<napi_value>& args);
napi_value inspect(napi_env env);
} // namespace rea
+532
View File
@@ -0,0 +1,532 @@
#include "authority.hpp"
#include <sddl.h>
#include <winternl.h>
namespace rea {
static std::wstring hex(const unsigned char* bytes, size_t count) {
constexpr wchar_t digits[] = L"0123456789abcdef";
std::wstring result;
for (size_t index = 0; index < count; ++index) {
result += digits[bytes[index] >> 4]; result += digits[bytes[index] & 15];
}
return result;
}
static std::wstring localPath(std::wstring path) {
path = ordinaryDriveSeparators(std::move(path));
if (path.rfind(L"\\\\?\\", 0) == 0) path.erase(0, 4);
require(path.size() >= 3 && path[1] == L':' && path[2] == L'\\' &&
((path[0] >= L'A' && path[0] <= L'Z') || (path[0] >= L'a' && path[0] <= L'z')),
"Only absolute local NTFS drive paths are supported", path, ERROR_NOT_SUPPORTED);
require(path.find(L'/', 0) == std::wstring::npos && path.find(L':', 2) == std::wstring::npos,
"Alternate streams and non-local path syntax are unsupported", path, ERROR_NOT_SUPPORTED);
require(GetDriveTypeW(path.substr(0, 3).c_str()) == DRIVE_FIXED,
"Only fixed local NTFS volumes are supported", path, ERROR_NOT_SUPPORTED);
if (path.size() > 3 && path.back() == L'\\') path.pop_back();
require(path.size() == 3 || path.back() != L'\\', "Repeated path separators are unsupported", path, ERROR_NOT_SUPPORTED);
size_t begin = 3;
while (begin < path.size()) {
const auto end = path.find(L'\\', begin);
const auto component = path.substr(begin, end == std::wstring::npos ? end : end - begin);
require(!component.empty() && component != L"." && component != L".." &&
component.back() != L'.' && component.back() != L' ',
"Ambiguous Windows path component is unsupported", path, ERROR_NOT_SUPPORTED);
if (end == std::wstring::npos) break;
begin = end + 1;
}
return path;
}
static Handle openComponent(const std::wstring& path, DWORD access, bool directory, DWORD sharing = FILE_SHARE_READ) {
// Attribute-only opens do not participate in Windows share-access checks.
// Directory guards need FILE_LIST_DIRECTORY to prevent replacement and
// in-place reparse conversion while descendant paths are being consumed.
Handle handle(CreateFileW((L"\\\\?\\" + path).c_str(), access | FILE_READ_ATTRIBUTES | READ_CONTROL |
(directory ? FILE_LIST_DIRECTORY : 0),
sharing, nullptr, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr));
require(handle.valid(), "Open Windows path component failed", path);
FILE_ATTRIBUTE_TAG_INFO info{};
require(GetFileInformationByHandleEx(handle.get(), FileAttributeTagInfo, &info, sizeof(info)),
"Read Windows path attributes failed", path);
if ((info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
throw Failure("Reparse path component is unsupported (tag " + std::to_string(info.ReparseTag) + ")",
path, ERROR_NOT_SUPPORTED);
require(((info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) == directory,
directory ? "Expected a directory path component" : "Target is not a regular file", path,
directory ? ERROR_PATH_NOT_FOUND : ERROR_DIRECTORY);
require(GetFileType(handle.get()) == FILE_TYPE_DISK, "Windows object is not a disk file", path, ERROR_NOT_SUPPORTED);
return handle;
}
std::unique_ptr<File> openFile(const std::wstring& requested, DWORD access, bool directory) {
auto result = std::make_unique<File>(localPath(requested));
result->requestedPath = requested;
const auto& path = result->path;
require(directory || path.size() > 3, "Target is not a regular file", requested, ERROR_DIRECTORY);
result->handles.push_back(openComponent(path.substr(0, 3), 0, true));
wchar_t filesystem[64]{};
DWORD flags = 0;
require(GetVolumeInformationByHandleW(result->handles.front().get(), nullptr, 0, nullptr, nullptr,
&flags, filesystem, 64), "Read volume semantics failed", path);
require(std::wstring(filesystem) == L"NTFS" && (flags & FILE_PERSISTENT_ACLS) != 0,
"Backing volume must be NTFS with persistent ACLs", path, ERROR_NOT_SUPPORTED);
size_t offset = 3;
while (offset < path.size()) {
auto end = path.find(L'\\', offset);
const bool final = end == std::wstring::npos;
auto child = openComponent(path.substr(0, end), final ? access : 0, !final || directory);
// Once the next component is pinned against deletion, its parent cannot
// become empty and NTFS cannot convert that parent into a reparse point.
// Relax only ancestor write sharing so unrelated child renames can proceed.
// Independent evidence: tests/fixtures/windows/processBoundary.cc attempts
// FSCTL_SET_REPARSE_POINT on the nonempty pinned ancestor and observes denial.
// SUBST aliases and mounted-folder/reparse namespace variants are outside
// the admitted P0 evidence; requested and final handle paths remain distinct.
const auto parentPath = offset == 3 ? path.substr(0, 3) : path.substr(0, offset - 1);
result->handles.back() = openComponent(parentPath, 0, true, FILE_SHARE_READ | FILE_SHARE_WRITE);
result->handles.push_back(std::move(child));
if (final) break;
offset = end + 1;
}
return result;
}
napi_value identity(napi_env env, HANDLE handle, const std::wstring& path) {
FILE_ID_INFO info{};
require(GetFileInformationByHandleEx(handle, FileIdInfo, &info, sizeof(info)),
"Stable 128-bit Windows file identity is unavailable", path);
std::array<wchar_t, 32768> finalPath;
const auto length = GetFinalPathNameByHandleW(handle, finalPath.data(), finalPath.size(), FILE_NAME_NORMALIZED);
require(length > 0 && length < finalPath.size(), "Read final handle path failed", path);
LARGE_INTEGER size{};
FILE_STANDARD_INFO standard{};
require(GetFileInformationByHandleEx(handle, FileStandardInfo, &standard, sizeof(standard)),
"Read handle metadata failed", path);
size = standard.EndOfFile;
require(size.QuadPart >= 0 && size.QuadPart <= 9007199254740991LL, "File size exceeds exact numeric representation", path,
ERROR_NOT_SUPPORTED);
auto result = object(env);
set(env, result, "requestedPath", string(env, path));
set(env, result, "finalPath", string(env, std::wstring(finalPath.data(), length)));
set(env, result, "filesystem", string(env, std::string("NTFS")));
std::array<unsigned char, 8> serial;
for (size_t index = 0; index < serial.size(); ++index)
serial[index] = static_cast<unsigned char>(info.VolumeSerialNumber >> ((7 - index) * 8));
set(env, result, "volumeSerial", string(env, hex(serial.data(), serial.size())));
set(env, result, "fileId", string(env, hex(info.FileId.Identifier, 16)));
set(env, result, "size", number(env, static_cast<double>(size.QuadPart)));
set(env, result, "directory", boolean(env, standard.Directory != FALSE));
return result;
}
struct Security {
std::vector<unsigned char> user;
std::array<unsigned char, SECURITY_MAX_SID_SIZE> system{};
std::vector<unsigned char> acl;
SECURITY_DESCRIPTOR descriptor{};
PSID userSid() const { return reinterpret_cast<const TOKEN_USER*>(user.data())->User.Sid; }
Security() {
Handle token;
HANDLE raw;
require(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw), "Read current user token failed");
token.reset(raw);
DWORD size = 0;
GetTokenInformation(token.get(), TokenUser, nullptr, 0, &size);
user.resize(size);
require(GetTokenInformation(token.get(), TokenUser, user.data(), size, &size), "Read current user SID failed");
DWORD systemSize = system.size();
require(CreateWellKnownSid(WinLocalSystemSid, nullptr, system.data(), &systemSize), "Create SYSTEM SID failed");
acl.resize(sizeof(ACL) + 2 * (sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD)) + GetLengthSid(userSid()) + systemSize);
auto access = reinterpret_cast<PACL>(acl.data());
require(InitializeAcl(access, acl.size(), ACL_REVISION), "Initialize private DACL failed");
for (PSID sid : {userSid(), static_cast<PSID>(system.data())})
require(AddAccessAllowedAceEx(access, ACL_REVISION, OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE,
FILE_ALL_ACCESS, sid), "Populate private DACL failed");
require(InitializeSecurityDescriptor(&descriptor, SECURITY_DESCRIPTOR_REVISION) &&
SetSecurityDescriptorOwner(&descriptor, userSid(), FALSE) &&
SetSecurityDescriptorDacl(&descriptor, TRUE, access, FALSE) &&
SetSecurityDescriptorControl(&descriptor, SE_DACL_PROTECTED, SE_DACL_PROTECTED),
"Create protected security descriptor failed");
}
};
static Handle createRelative(HANDLE parent, const std::wstring& name, DWORD access, bool directory,
Security& security, const std::wstring& path) {
require(!name.empty() && name.find_first_of(L"\\/:\"<>|?*") == std::wstring::npos && name.size() < 32768,
"Private object requires one filename component", path, ERROR_INVALID_PARAMETER);
UNICODE_STRING coordinate{};
coordinate.Buffer = const_cast<wchar_t*>(name.data());
coordinate.Length = static_cast<USHORT>(name.size() * sizeof(wchar_t));
coordinate.MaximumLength = coordinate.Length;
OBJECT_ATTRIBUTES attributes;
InitializeObjectAttributes(&attributes, &coordinate, OBJ_CASE_INSENSITIVE, parent, &security.descriptor);
IO_STATUS_BLOCK observation{};
HANDLE raw = INVALID_HANDLE_VALUE;
const auto status = NtCreateFile(&raw, access | SYNCHRONIZE, &attributes, &observation, nullptr,
FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_CREATE,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT |
(directory ? FILE_DIRECTORY_FILE : FILE_NON_DIRECTORY_FILE), nullptr, 0);
if (status < 0) throw Failure("Create private object relative to its admitted parent failed", path,
RtlNtStatusToDosError(status));
Handle result(raw);
require(result.valid() && observation.Information == FILE_CREATED,
"Private creation did not return a new object handle", path, ERROR_ACCESS_DENIED);
return result;
}
static void verifyPrivate(HANDLE handle, const std::wstring& path, bool root) {
Security expected;
PSID owner = nullptr;
PACL acl = nullptr;
PSECURITY_DESCRIPTOR raw = nullptr;
const DWORD code = GetSecurityInfo(handle, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
&owner, nullptr, &acl, nullptr, &raw);
if (code != ERROR_SUCCESS) throw Failure("Read private DACL by handle failed", path, code);
const auto descriptor = std::unique_ptr<void, decltype(&LocalFree)>(raw, LocalFree);
require(owner != nullptr && EqualSid(owner, expected.userSid()), "Private object owner is not the current user", path,
ERROR_ACCESS_DENIED);
SECURITY_DESCRIPTOR_CONTROL control;
DWORD revision;
require(GetSecurityDescriptorControl(raw, &control, &revision), "Read DACL control flags failed", path);
require(acl != nullptr && (control & SE_DACL_PRESENT) != 0 && (!root || (control & SE_DACL_PROTECTED) != 0),
"Private object requires a present protected DACL", path, ERROR_ACCESS_DENIED);
require(acl->AceCount == 2, "Private DACL contains unexpected trustees", path, ERROR_ACCESS_DENIED);
bool user = false, system = false;
for (DWORD index = 0; index < acl->AceCount; ++index) {
void* rawAce;
require(GetAce(acl, index, &rawAce), "Read private DACL entry failed", path);
const auto ace = static_cast<ACCESS_ALLOWED_ACE*>(rawAce);
require(ace->Header.AceType == ACCESS_ALLOWED_ACE_TYPE && ace->Mask == FILE_ALL_ACCESS &&
(ace->Header.AceFlags & INHERIT_ONLY_ACE) == 0 &&
(!root || (ace->Header.AceFlags & (OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE)) ==
(OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE)),
"Private DACL contains unsupported rights or ACE type", path, ERROR_ACCESS_DENIED);
PSID sid = const_cast<DWORD*>(&ace->SidStart);
if (EqualSid(sid, expected.userSid())) user = true;
else if (EqualSid(sid, expected.system.data())) system = true;
else throw Failure("Private DACL permits an unrelated principal", path, ERROR_ACCESS_DENIED);
}
require(user && system, "Private DACL must allow only current user and SYSTEM", path, ERROR_ACCESS_DENIED);
}
std::unique_ptr<Runtime> createRuntime(const std::wstring& parentPath, const std::wstring& prefix) {
auto parent = openFile(parentPath, 0, true);
require(!prefix.empty() && prefix.find_first_of(L"\\/:\"<>|?*") == std::wstring::npos,
"Runtime prefix must be one filename component", parentPath, ERROR_INVALID_PARAMETER);
std::array<unsigned char, 16> random;
require(BCryptGenRandom(nullptr, random.data(), random.size(), BCRYPT_USE_SYSTEM_PREFERRED_RNG) == 0,
"Generate runtime identity failed", parentPath, ERROR_GEN_FAILURE);
auto result = std::make_unique<Runtime>();
result->path = parent->path + L"\\" + prefix + hex(random.data(), random.size());
Security security;
result->directory = createRelative(parent->get(), prefix + hex(random.data(), random.size()),
DELETE | FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | READ_CONTROL,
true, security, result->path);
try {
verifyPrivate(result->directory.get(), result->path, true);
// The owned child pins the parent nonempty before write sharing is relaxed.
parent->handles.back() = openComponent(parent->path, 0, true, FILE_SHARE_READ | FILE_SHARE_WRITE);
result->parents = std::move(parent->handles);
} catch (...) {
FILE_DISPOSITION_INFO deletion{TRUE};
SetFileInformationByHandle(result->directory.get(), FileDispositionInfo, &deletion, sizeof(deletion));
throw;
}
return result;
}
static std::wstring relativePath(Runtime& root, const std::wstring& relative) {
require(!relative.empty() && relative[0] != L'\\' && relative.find(L':') == std::wstring::npos,
"Runtime coordinate must be a relative path", relative, ERROR_INVALID_PARAMETER);
return localPath(root.path + L"\\" + relative);
}
static std::unique_ptr<File> runtimeDirectory(Runtime& root, const std::wstring& path) {
auto result = std::make_unique<File>(path);
HANDLE duplicate;
require(DuplicateHandle(GetCurrentProcess(), root.directory.get(), GetCurrentProcess(), &duplicate, 0, FALSE,
DUPLICATE_SAME_ACCESS), "Duplicate private directory handle failed", path);
result->handles.emplace_back(duplicate);
size_t offset = root.path.size() + 1;
while (offset < path.size()) {
const auto end = path.find(L'\\', offset);
result->handles.push_back(openComponent(path.substr(0, end), 0, true));
if (end == std::wstring::npos) break;
offset = end + 1;
}
return result;
}
static void directory(Runtime& root, const std::wstring& relative) {
const auto path = relativePath(root, relative);
const auto parentPath = path.substr(0, path.find_last_of(L'\\'));
auto parent = runtimeDirectory(root, parentPath);
verifyPrivate(parent->get(), parentPath, parentPath == root.path);
Security security;
try {
auto created = createRelative(parent->get(), path.substr(path.find_last_of(L'\\') + 1),
FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | READ_CONTROL,
true, security, path);
verifyPrivate(created.get(), path, false);
} catch (const Failure& failure) {
if (failure.win32 != ERROR_ALREADY_EXISTS && failure.win32 != ERROR_FILE_EXISTS) throw;
auto existing = runtimeDirectory(root, path);
verifyPrivate(existing->get(), path, false);
}
}
static Handle createPrivateFile(Runtime& root, const std::wstring& relative) {
const auto path = relativePath(root, relative);
const auto parentPath = path.substr(0, path.find_last_of(L'\\'));
auto parent = runtimeDirectory(root, parentPath);
verifyPrivate(parent->get(), parentPath, parentPath == root.path);
Security security;
auto file = createRelative(parent->get(), path.substr(path.find_last_of(L'\\') + 1),
GENERIC_WRITE | GENERIC_READ | READ_CONTROL, false, security, path);
verifyPrivate(file.get(), path, false);
return file;
}
static void writeBytes(HANDLE file, const void* bytes, DWORD count, const std::wstring& path) {
DWORD written;
require(WriteFile(file, bytes, count, &written, nullptr) && written == count, "Write private file failed", path);
}
static void dispose(HANDLE handle, const std::wstring& path) {
FILE_BASIC_INFO info{};
require(GetFileInformationByHandleEx(handle, FileBasicInfo, &info, sizeof(info)), "Read cleanup attributes failed", path);
if ((info.FileAttributes & FILE_ATTRIBUTE_READONLY) != 0) {
info.FileAttributes &= ~FILE_ATTRIBUTE_READONLY;
require(SetFileInformationByHandle(handle, FileBasicInfo, &info, sizeof(info)), "Clear owned readonly attribute failed", path);
}
FILE_DISPOSITION_INFO deletion{TRUE};
require(SetFileInformationByHandle(handle, FileDispositionInfo, &deletion, sizeof(deletion)), "Delete owned object by handle failed", path);
}
void closeRuntime(Runtime& root) {
if (root.closed) return;
// Windows keeps the bearer descriptor and snapshot leases until this owner
// cleanup. POSIX removes its consumed descriptor earlier. Abrupt owner death
// closes process jobs but cannot run this walk, so private files can remain.
root.immutableFiles.clear();
struct ReleaseRuntime {
Runtime& root;
~ReleaseRuntime() { root.directory.reset(); root.parents.clear(); root.closed = true; }
} release{root};
// Each traversed directory stays locked against replacement. Reparse entries
// are unlinked as objects; their targets are never enumerated or deleted.
struct Frame {
Handle directory;
HANDLE search = INVALID_HANDLE_VALUE;
std::wstring path;
WIN32_FIND_DATAW next{};
bool pending = false;
Frame(Handle handle, std::wstring path) : directory(std::move(handle)), path(std::move(path)) {
search = FindFirstFileExW((L"\\\\?\\" + this->path + L"\\*").c_str(), FindExInfoBasic, &next,
FindExSearchNameMatch, nullptr, 0);
pending = search != INVALID_HANDLE_VALUE;
require(pending || GetLastError() == ERROR_FILE_NOT_FOUND, "Enumerate runtime cleanup failed", this->path);
}
~Frame() { if (search != INVALID_HANDLE_VALUE) FindClose(search); }
};
std::vector<std::unique_ptr<Frame>> frames;
HANDLE duplicate;
require(DuplicateHandle(GetCurrentProcess(), root.directory.get(), GetCurrentProcess(), &duplicate, 0, FALSE,
DUPLICATE_SAME_ACCESS), "Duplicate runtime cleanup handle failed", root.path);
frames.push_back(std::make_unique<Frame>(Handle(duplicate), root.path));
while (!frames.empty()) {
auto& frame = *frames.back();
if (!frame.pending) {
if (frame.search != INVALID_HANDLE_VALUE) { FindClose(frame.search); frame.search = INVALID_HANDLE_VALUE; }
dispose(frame.directory.get(), frame.path);
frames.pop_back();
continue;
}
const auto name = std::wstring(frame.next.cFileName);
frame.pending = FindNextFileW(frame.search, &frame.next) != FALSE;
require(frame.pending || GetLastError() == ERROR_NO_MORE_FILES, "Continue runtime cleanup enumeration failed", frame.path);
if (name == L"." || name == L"..") continue;
const auto path = frame.path + L"\\" + name;
Handle child(CreateFileW((L"\\\\?\\" + path).c_str(), DELETE | GENERIC_READ | FILE_WRITE_ATTRIBUTES,
FILE_SHARE_READ, nullptr, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr));
require(child.valid(), "Open owned cleanup object failed", path);
FILE_ATTRIBUTE_TAG_INFO info{};
require(GetFileInformationByHandleEx(child.get(), FileAttributeTagInfo, &info, sizeof(info)), "Read cleanup object tag failed", path);
if ((info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 && (info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0)
frames.push_back(std::make_unique<Frame>(std::move(child), path));
else dispose(child.get(), path);
}
}
struct SnapshotWork {
napi_env env;
napi_async_work work = nullptr;
napi_deferred deferred = nullptr;
napi_ref rootReference = nullptr;
Runtime* root;
std::unique_ptr<File> source;
Handle destination;
std::wstring path;
std::wstring digest;
std::unique_ptr<Failure> failure;
SnapshotWork(napi_env env, Runtime& root) : env(env), root(&root) {}
};
static void copySnapshot(napi_env, void* pointer) {
auto& work = *static_cast<SnapshotWork*>(pointer);
try {
BCRYPT_ALG_HANDLE algorithm = nullptr;
require(BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM, nullptr, 0) == 0,
"Initialize snapshot SHA-256 failed", work.source->path, ERROR_GEN_FAILURE);
struct Algorithm { BCRYPT_ALG_HANDLE handle; ~Algorithm() { BCryptCloseAlgorithmProvider(handle, 0); } } owner{algorithm};
BCRYPT_HASH_HANDLE hash = nullptr;
require(BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0) == 0,
"Create snapshot SHA-256 state failed", work.source->path, ERROR_GEN_FAILURE);
struct Hash { BCRYPT_HASH_HANDLE handle; ~Hash() { BCryptDestroyHash(handle); } } hashOwner{hash};
std::array<unsigned char, 65536> buffer;
while (true) {
require(!work.root->snapshotCancelled.load(), "Admitted snapshot was cancelled", work.path, ERROR_OPERATION_ABORTED);
DWORD count;
require(ReadFile(work.source->get(), buffer.data(), buffer.size(), &count, nullptr),
"Read admitted source handle failed", work.source->path);
if (count == 0) break;
writeBytes(work.destination.get(), buffer.data(), count, work.path);
require(BCryptHashData(hash, buffer.data(), count, 0) == 0,
"Hash admitted snapshot bytes failed", work.source->path, ERROR_GEN_FAILURE);
}
std::array<unsigned char, 32> digest;
require(BCryptFinishHash(hash, digest.data(), digest.size(), 0) == 0,
"Finish snapshot SHA-256 failed", work.source->path, ERROR_GEN_FAILURE);
require(FlushFileBuffers(work.destination.get()), "Flush private snapshot failed", work.path);
work.digest = hex(digest.data(), digest.size());
} catch (const Failure& failure) {
work.failure = std::make_unique<Failure>(failure);
} catch (const std::exception& failure) {
work.failure = std::make_unique<Failure>(failure.what(), work.path, ERROR_GEN_FAILURE);
}
}
static void completeSnapshot(napi_env env, napi_status status, void* pointer) {
std::unique_ptr<SnapshotWork> work(static_cast<SnapshotWork*>(pointer));
work->root->snapshotPending = false;
try {
if (work->failure) throw *work->failure;
require(status == napi_ok && !work->root->snapshotCancelled.load(), "Native snapshot worker was cancelled", work->path, ERROR_OPERATION_ABORTED);
auto result = object(env);
set(env, result, "sha256", string(env, work->digest));
set(env, result, "source", identity(env, work->source->get(), work->source->requestedPath));
set(env, result, "snapshot", identity(env, work->destination.get(), work->path));
work->root->immutableFiles.push_back(std::move(work->destination));
check(napi_resolve_deferred(env, work->deferred, result));
} catch (const Failure& failure) {
napi_reject_deferred(env, work->deferred, failureValue(env, failure));
} catch (const std::exception& failure) {
napi_reject_deferred(env, work->deferred,
failureValue(env, Failure(failure.what(), work->path, ERROR_GEN_FAILURE)));
}
napi_delete_reference(env, work->rootReference);
napi_delete_async_work(env, work->work);
}
static napi_value snapshot(napi_env env, napi_value rootValue, Runtime& root,
const std::wstring& sourcePath, const std::wstring& relative) {
auto work = std::make_unique<SnapshotWork>(env, root);
work->source = openFile(sourcePath);
work->path = relativePath(root, relative);
work->destination = createPrivateFile(root, relative);
napi_value promise;
check(napi_create_promise(env, &work->deferred, &promise));
check(napi_create_reference(env, rootValue, 1, &work->rootReference));
const auto label = string(env, std::string("REA admitted Windows snapshot"));
root.snapshotCancelled.store(false);
try {
check(napi_create_async_work(env, nullptr, label, copySnapshot, completeSnapshot, work.get(), &work->work));
check(napi_queue_async_work(env, work->work));
} catch (...) {
if (work->work) napi_delete_async_work(env, work->work);
napi_delete_reference(env, work->rootReference);
throw;
}
root.snapshotPending = true;
work.release();
return promise;
}
napi_value filesystemCall(napi_env env, const std::wstring& operation, const std::vector<napi_value>& args) {
const auto expected = operation == L"open" || operation == L"close" || operation == L"runtime_close" || operation == L"runtime_snapshot_cancel" ? 1
: operation == L"runtime_create" || operation == L"runtime_mkdir" || operation == L"runtime_open" ? 2 : 3;
require(args.size() == static_cast<size_t>(expected), "Wrong native filesystem argument count", L"", ERROR_INVALID_PARAMETER);
if (operation == L"open") {
auto file = openFile(wide(env, args[0]));
auto result = identity(env, file->get(), file->requestedPath);
set(env, result, "handle", wrap(env, std::move(file)));
return result;
}
if (operation == L"close") {
auto& file = static_cast<File&>(resource(env, args[0], Kind::File));
file.handles.clear(); file.closed = true;
return null(env);
}
if (operation == L"read") {
auto& file = static_cast<File&>(resource(env, args[0], Kind::File));
const auto position = numeric(env, args[1]);
const auto length = numeric(env, args[2]);
require(position >= 0 && position <= 9007199254740991.0 && position == static_cast<int64_t>(position) &&
length >= 0 && length <= 65536 && length == static_cast<DWORD>(length),
"Read coordinates exceed the native chunk boundary", file.path, ERROR_INVALID_PARAMETER);
LARGE_INTEGER offset; offset.QuadPart = static_cast<int64_t>(position);
require(SetFilePointerEx(file.get(), offset, nullptr, FILE_BEGIN), "Seek admitted file handle failed", file.path);
std::array<unsigned char, 65536> buffer;
DWORD count;
require(ReadFile(file.get(), buffer.data(), static_cast<DWORD>(length), &count, nullptr), "Read admitted file handle failed", file.path);
napi_value result;
check(napi_create_buffer_copy(env, count, buffer.data(), nullptr, &result));
return result;
}
if (operation == L"runtime_create") {
auto root = createRuntime(wide(env, args[0]), wide(env, args[1]));
auto result = identity(env, root->directory.get(), root->path);
set(env, result, "path", string(env, root->path));
set(env, result, "privateDacl", boolean(env, true));
set(env, result, "handle", wrap(env, std::move(root)));
return result;
}
auto& root = static_cast<Runtime&>(resource(env, args[0], Kind::Runtime));
if (operation == L"runtime_snapshot_cancel") { root.snapshotCancelled.store(true); return null(env); }
require(!root.snapshotPending, "Runtime snapshot is still pending", root.path, ERROR_BUSY);
// Reject overlapping operations before snapshot() resets cancellation. The
// one accepted worker and WindowsPrivateRuntime retain the same root lease.
if (operation == L"runtime_close") { closeRuntime(root); return null(env); }
if (operation == L"runtime_mkdir") { directory(root, wide(env, args[1])); return null(env); }
if (operation == L"runtime_open") {
const auto path = relativePath(root, wide(env, args[1]));
auto file = runtimeDirectory(root, path.substr(0, path.find_last_of(L'\\')));
file->path = path;
file->requestedPath = path;
file->handles.push_back(openComponent(path, GENERIC_READ, false));
verifyPrivate(file->get(), path, false);
auto result = identity(env, file->get(), path);
set(env, result, "handle", wrap(env, std::move(file)));
return result;
}
if (operation == L"runtime_write") {
bool isBuffer; check(napi_is_buffer(env, args[2], &isBuffer));
require(isBuffer, "Private write requires a byte buffer", root.path, ERROR_INVALID_PARAMETER);
void* bytes; size_t length; check(napi_get_buffer_info(env, args[2], &bytes, &length));
require(length <= MAXDWORD, "Private write exceeds Win32 byte representation", root.path, ERROR_INVALID_PARAMETER);
auto file = createPrivateFile(root, wide(env, args[1]));
writeBytes(file.get(), bytes, static_cast<DWORD>(length), root.path);
require(FlushFileBuffers(file.get()), "Flush private runtime file failed", root.path);
root.immutableFiles.push_back(std::move(file));
return null(env);
}
if (operation == L"runtime_snapshot") {
return snapshot(env, args[0], root, wide(env, args[1]), wide(env, args[2]));
}
throw Failure("Unknown native filesystem operation", L"", ERROR_INVALID_PARAMETER);
}
} // namespace rea
+272
View File
@@ -0,0 +1,272 @@
#include "authority.hpp"
namespace rea {
class Attributes {
std::vector<unsigned char> storage_;
public:
explicit Attributes(DWORD count) {
SIZE_T size = 0;
InitializeProcThreadAttributeList(nullptr, count, 0, &size);
storage_.resize(size);
require(InitializeProcThreadAttributeList(get(), count, 0, &size), "Initialize process attributes failed");
}
~Attributes() { DeleteProcThreadAttributeList(get()); }
LPPROC_THREAD_ATTRIBUTE_LIST get() { return reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(storage_.data()); }
void add(DWORD_PTR key, void* value, SIZE_T size) {
require(UpdateProcThreadAttribute(get(), 0, key, value, size, nullptr, nullptr),
"Windows atomic process attribute is unavailable");
}
};
static Handle createJob() {
Handle job(CreateJobObjectW(nullptr, nullptr));
require(job.valid(), "Create owned Job Object failed");
JOBOBJECT_EXTENDED_LIMIT_INFORMATION policy{};
policy.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
require(SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation, &policy, sizeof(policy)),
"Apply kill-on-owner-close Job Object policy failed");
JOBOBJECT_EXTENDED_LIMIT_INFORMATION actual{};
require(QueryInformationJobObject(job.get(), JobObjectExtendedLimitInformation, &actual, sizeof(actual), nullptr),
"Read Job Object policy failed");
require(actual.BasicLimitInformation.LimitFlags == JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
"Job Object policy permits unexpected breakaway", L"", ERROR_ACCESS_DENIED);
return job;
}
static void pipe(Handle& read, Handle& write) {
SECURITY_ATTRIBUTES attributes{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
HANDLE rawRead, rawWrite;
require(CreatePipe(&rawRead, &rawWrite, &attributes, 0), "Create owned process output pipe failed");
read.reset(rawRead); write.reset(rawWrite);
require(SetHandleInformation(read.get(), HANDLE_FLAG_INHERIT, 0), "Protect process output handle inheritance failed");
}
std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::wstring& commandLine,
const std::wstring& cwd, const std::vector<std::wstring>& environment) {
const auto executable = ordinaryDriveSeparators(command);
require(executable.size() >= 3 && executable[1] == L':' && executable[2] == L'\\',
"Owned Windows process requires an absolute executable path", command, ERROR_INVALID_PARAMETER);
auto result = std::make_unique<Process>();
result->job = createJob();
Handle stdoutWrite, stderrWrite;
pipe(result->stdoutRead, stdoutWrite); pipe(result->stderrRead, stderrWrite);
SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
Handle input(CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
&security, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
require(input.valid(), "Open owned process null input failed");
Attributes attributes(2);
HANDLE job = result->job.get();
attributes.add(PROC_THREAD_ATTRIBUTE_JOB_LIST, &job, sizeof(job));
std::array<HANDLE, 3> inherited{input.get(), stdoutWrite.get(), stderrWrite.get()};
attributes.add(PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherited.data(), sizeof(inherited));
STARTUPINFOEXW startup{};
startup.StartupInfo.cb = sizeof(startup);
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
startup.StartupInfo.hStdInput = input.get();
startup.StartupInfo.hStdOutput = stdoutWrite.get();
startup.StartupInfo.hStdError = stderrWrite.get();
startup.lpAttributeList = attributes.get();
// Windows environment names use ordinal case-insensitive comparison, not
// locale collation or JavaScript's expanding Unicode uppercase conversion.
auto orderedEnvironment = environment;
auto keyLength = [&](const std::wstring& entry) {
const auto end = entry.find(L'=', entry.front() == L'=' ? 1 : 0);
require(end != std::wstring::npos && end > 0, "Invalid child environment name", command,
ERROR_INVALID_PARAMETER);
return static_cast<int>(end);
};
for (const auto& entry : orderedEnvironment) {
require(!entry.empty(), "Empty child environment entry", command, ERROR_INVALID_PARAMETER);
keyLength(entry);
}
auto compareNames = [&](const std::wstring& left, const std::wstring& right) {
const auto result = CompareStringOrdinal(left.data(), keyLength(left), right.data(), keyLength(right), TRUE);
require(result != 0, "Compare Windows environment names failed", command);
return result;
};
std::stable_sort(orderedEnvironment.begin(), orderedEnvironment.end(),
[&](const std::wstring& left, const std::wstring& right) {
return compareNames(left, right) == CSTR_LESS_THAN;
});
std::vector<std::wstring> uniqueEnvironment;
for (auto& entry : orderedEnvironment) {
if (!uniqueEnvironment.empty() && compareNames(uniqueEnvironment.back(), entry) == CSTR_EQUAL)
uniqueEnvironment.back() = std::move(entry);
else uniqueEnvironment.push_back(std::move(entry));
}
std::vector<wchar_t> variables;
for (const auto& entry : uniqueEnvironment) {
variables.insert(variables.end(), entry.begin(), entry.end()); variables.push_back(L'\0');
}
variables.push_back(L'\0');
if (uniqueEnvironment.empty()) variables.push_back(L'\0');
std::vector<wchar_t> line(commandLine.begin(), commandLine.end()); line.push_back(L'\0');
PROCESS_INFORMATION process{};
require(CreateProcessW(executable.c_str(), line.data(), nullptr, nullptr, TRUE,
CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT | CREATE_SUSPENDED | CREATE_NO_WINDOW,
variables.data(), cwd.empty() ? nullptr : cwd.c_str(), &startup.StartupInfo, &process),
"Create process atomically inside owned Job Object failed", command);
result->process.reset(process.hProcess);
Handle thread(process.hThread);
result->pid = process.dwProcessId;
// An elevated token may default file ownership to Administrators. Normalize
// only the suspended child's default owner so its runtime files belong to the
// current user; the caller's token and privileges are not modified.
HANDLE rawToken;
require(OpenProcessToken(result->process.get(), TOKEN_QUERY | TOKEN_ADJUST_DEFAULT, &rawToken),
"Open suspended process default-owner token failed", command);
Handle token(rawToken);
DWORD tokenSize = 0;
GetTokenInformation(token.get(), TokenUser, nullptr, 0, &tokenSize);
std::vector<unsigned char> tokenUser(tokenSize);
require(GetTokenInformation(token.get(), TokenUser, tokenUser.data(), tokenSize, &tokenSize),
"Read suspended process user identity failed", command);
TOKEN_OWNER owner{reinterpret_cast<TOKEN_USER*>(tokenUser.data())->User.Sid};
require(SetTokenInformation(token.get(), TokenOwner, &owner, sizeof(owner)),
"Set suspended process file owner failed", command);
DWORD_PTR affinity = 0, systemAffinity = 0;
require(GetProcessAffinityMask(GetCurrentProcess(), &affinity, &systemAffinity),
"Read caller process affinity failed", command);
if (affinity != 0)
require(SetProcessAffinityMask(result->process.get(), affinity), "Preserve caller process affinity failed", command);
const DWORD priority = GetPriorityClass(GetCurrentProcess());
require(priority != 0 && SetPriorityClass(result->process.get(), priority),
"Preserve caller process priority failed", command);
BOOL assigned = FALSE;
if (!IsProcessInJob(result->process.get(), result->job.get(), &assigned) || !assigned) {
TerminateJobObject(result->job.get(), 1);
throw Failure("Suspended process has no owned Job Object membership", command, ERROR_ACCESS_DENIED);
}
require(ResumeThread(thread.get()) != MAXDWORD, "Resume job-owned process failed", command);
return result;
}
static napi_value readOutput(napi_env env, Handle& read, bool& ended) {
std::array<unsigned char, 65536> bytes;
DWORD available = 0, count = 0;
if (!read.valid()) ended = true;
else if (!PeekNamedPipe(read.get(), nullptr, 0, nullptr, &available, nullptr)) {
require(GetLastError() == ERROR_BROKEN_PIPE, "Observe owned process output failed");
read.reset(); ended = true;
} else if (available > 0) {
require(ReadFile(read.get(), bytes.data(), std::min<DWORD>(available, bytes.size()), &count, nullptr),
"Read owned process output failed");
}
napi_value result;
check(napi_create_buffer_copy(env, count, bytes.data(), nullptr, &result));
return result;
}
static DWORD activeProcesses(Process& process) {
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION state{};
require(QueryInformationJobObject(process.job.get(), JobObjectBasicAccountingInformation, &state, sizeof(state), nullptr),
"Observe owned Job Object settlement failed");
return state.ActiveProcesses;
}
static napi_value callerToken(napi_env env) {
HANDLE raw;
require(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, &raw), "Read caller token failed");
Handle token(raw);
DWORD size;
TOKEN_ELEVATION elevation{};
require(GetTokenInformation(token.get(), TokenElevation, &elevation, sizeof(elevation), &size), "Read caller elevation failed");
std::vector<unsigned char> user(4096), owner(4096), integrity(4096);
require(GetTokenInformation(token.get(), TokenUser, user.data(), user.size(), &size) &&
GetTokenInformation(token.get(), TokenOwner, owner.data(), owner.size(), &size) &&
GetTokenInformation(token.get(), TokenIntegrityLevel, integrity.data(), integrity.size(), &size),
"Read caller token observations failed");
HANDLE rawImpersonation;
require(DuplicateToken(token.get(), SecurityIdentification, &rawImpersonation), "Observe caller group membership failed");
Handle impersonation(rawImpersonation);
std::array<unsigned char, SECURITY_MAX_SID_SIZE> administrators;
DWORD administratorSize = administrators.size();
require(CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, administrators.data(), &administratorSize),
"Create administrator group identity failed");
BOOL administrator = FALSE, assigned = FALSE;
require(CheckTokenMembership(impersonation.get(), administrators.data(), &administrator), "Read caller administrator membership failed");
require(IsProcessInJob(GetCurrentProcess(), nullptr, &assigned), "Read caller job membership failed");
const auto sid = reinterpret_cast<TOKEN_MANDATORY_LABEL*>(integrity.data())->Label.Sid;
auto result = object(env);
set(env, result, "elevated", boolean(env, elevation.TokenIsElevated != 0));
set(env, result, "administrator", boolean(env, administrator != FALSE));
set(env, result, "integrityLevel", number(env, *GetSidSubAuthority(sid, *GetSidSubAuthorityCount(sid) - 1)));
set(env, result, "defaultOwnerIsUser", boolean(env, EqualSid(reinterpret_cast<TOKEN_USER*>(user.data())->User.Sid,
reinterpret_cast<TOKEN_OWNER*>(owner.data())->Owner)));
set(env, result, "inJob", boolean(env, assigned != FALSE));
return result;
}
napi_value processCall(napi_env env, const std::wstring& operation, const std::vector<napi_value>& args) {
if (operation == L"process_caller_token") {
require(args.empty(), "Caller token observation takes no arguments", L"", ERROR_INVALID_PARAMETER);
return callerToken(env);
}
if (operation == L"process_spawn") {
require(args.size() == 4, "Wrong native process launch argument count", L"", ERROR_INVALID_PARAMETER);
std::vector<std::wstring> environment;
for (const auto& entry : array(env, args[3])) environment.push_back(wide(env, entry));
auto process = spawnProcess(wide(env, args[0]), wide(env, args[1]), wide(env, args[2]), environment);
auto result = object(env);
set(env, result, "pid", number(env, process->pid));
set(env, result, "jobAssigned", boolean(env, true));
set(env, result, "killOnOwnerClose", boolean(env, true));
set(env, result, "handle", wrap(env, std::move(process)));
return result;
}
require(args.size() == 1, "Wrong native process handle argument count", L"", ERROR_INVALID_PARAMETER);
auto& process = static_cast<Process&>(resource(env, args[0], Kind::Process));
if (operation == L"process_poll") {
auto result = object(env);
bool stdoutEnded = false, stderrEnded = false;
set(env, result, "stdout", readOutput(env, process.stdoutRead, stdoutEnded));
set(env, result, "stderr", readOutput(env, process.stderrRead, stderrEnded));
set(env, result, "stdoutEnded", boolean(env, stdoutEnded));
set(env, result, "stderrEnded", boolean(env, stderrEnded));
const auto exited = WaitForSingleObject(process.process.get(), 0);
require(exited == WAIT_OBJECT_0 || exited == WAIT_TIMEOUT, "Observe owned process exit failed");
DWORD code = 0;
if (exited == WAIT_OBJECT_0) require(GetExitCodeProcess(process.process.get(), &code), "Read owned process exit code failed");
set(env, result, "exitCode", exited == WAIT_OBJECT_0 ? number(env, code) : null(env));
set(env, result, "activeProcesses", number(env, activeProcesses(process)));
return result;
}
if (operation == L"process_alive") return number(env, activeProcesses(process));
if (operation == L"process_terminate") {
const auto active = activeProcesses(process);
if (active > 0) require(TerminateJobObject(process.job.get(), 1), "Terminate owned Job Object failed");
return boolean(env, active > 0);
}
if (operation == L"process_close") {
// Closing the job is the crash-safe authority; no PID enumeration is used.
process.job.reset(); process.process.reset(); process.stdoutRead.reset(); process.stderrRead.reset();
process.closed = true;
return null(env);
}
throw Failure("Unknown native process operation", L"", ERROR_INVALID_PARAMETER);
}
napi_value inspect(napi_env env) {
wchar_t temp[32768];
const auto length = GetTempPathW(32768, temp);
require(length > 0 && length < 32768, "Read temporary directory coordinate failed");
auto root = createRuntime(std::wstring(temp, length), L"rea-native-probe-");
auto filesystem = identity(env, root->directory.get(), root->path);
closeRuntime(*root);
auto job = createJob();
Attributes attributes(1);
HANDLE raw = job.get();
attributes.add(PROC_THREAD_ATTRIBUTE_JOB_LIST, &raw, sizeof(raw));
auto result = object(env);
set(env, result, "abiVersion", number(env, 1));
set(env, result, "nodeApiVersion", number(env, 8));
set(env, result, "architecture", string(env, std::string("x64")));
set(env, result, "filesystem", filesystem);
set(env, result, "privateDacl", boolean(env, true));
set(env, result, "atomicJobAssignment", boolean(env, true));
set(env, result, "killOnOwnerClose", boolean(env, true));
return result;
}
} // namespace rea
+1667 -1225
View File
File diff suppressed because it is too large Load Diff
+72 -44
View File
@@ -1,7 +1,7 @@
{
"name": "rea-agents",
"mcpName": "io.github.morluto/rea",
"version": "3.2.1",
"version": "4.1.0",
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
"license": "MIT",
"repository": {
@@ -35,14 +35,17 @@
"rea-agents": "scripts/rea.mjs"
},
"engines": {
"node": "^22.19.0 || >=24.11.0"
"node": "^22.19.0 || ^24.11.0 || >=26.0.0"
},
"packageManager": "npm@11.16.0",
"files": [
"dist",
"install.sh",
"bridge/hopper_bridge.py",
"bridge/native/ReaNativeUI.swift",
"bridge/ghidra/ReaGhidraBridge.java",
"bridge/ghidra/ReaGhidraExtensions.java",
"bridge/ghidra/ReaGhidraPrepareCom.java",
"scripts/rea.mjs",
"scripts/electron-active-hook.cjs",
"scripts/electron-active-hook-boundaries.cjs",
@@ -51,7 +54,10 @@
"scripts/prepare.mjs",
"skills",
"README.md",
"LICENSE"
"LICENSE",
"native/windows/build/manifest.json",
"native/windows/build/rea-windows-x64.node",
"scripts/verify-windows-native-artifact.mjs"
],
"scripts": {
"deps:check": "node scripts/check-dependency-install.mjs",
@@ -60,44 +66,42 @@
"mcp-registry:check": "node scripts/verify-mcp-registry-metadata.mjs",
"mcp-catalog:generate": "npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs",
"mcp-catalog:check": "node scripts/generate-mcp-tool-catalog.mjs --check",
"prebuild": "npm run deps:check && npm run metadata:check && node scripts/clean-build-output.mjs",
"build": "tsc -p tsconfig.build.json",
"prebuild": "npm run deps:check && npm run metadata:check",
"build": "node scripts/run-exclusive.mjs build npm run build:unlocked",
"build:unlocked": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json",
"precheck": "npm run deps:check",
"check": "turbo run typecheck lint format:check knip metadata:check",
"precheck:fast": "npm run deps:check",
"check:fast": "turbo run typecheck lint",
"check:fast": "turbo run typecheck lint --output-logs=errors-only",
"precheck:changed": "npm run deps:check",
"check:changed": "turbo run typecheck lint && npm run test:changed",
"check:changed": "npm run check:fast && npm run test:changed",
"precheck:test": "npm run deps:check",
"check:test": "turbo run typecheck lint format:check knip metadata:check test:run",
"precheck:pr": "npm run deps:check",
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check",
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check && npm run jscpd && npm run scan:todos",
"precheck:ci": "npm run deps:check",
"check:ci": "turbo run typecheck lint format:check knip metadata:check jscpd scan:todos",
"build:cached": "turbo run build",
"format": "prettier --write .",
"format:check": "prettier --check .",
"format": "oxfmt .",
"format:check": "oxfmt --check .",
"lint": "oxlint",
"lint:fix": "oxlint --fix",
"knip": "knip",
"lint:dead": "knip",
"jscpd": "jscpd src/ tests/",
"scan:todos": "node scripts/scan-todos.mjs",
"start": "node scripts/rea.mjs mcp",
"test": "node scripts/run-exclusive.mjs test npm run test:with-build",
"test": "npm run test:with-build",
"test:with-build": "npm run build:cached && vitest run",
"test:run": "node scripts/run-exclusive.mjs test vitest run",
"test:local": "node scripts/run-exclusive.mjs test vitest run --changed --passWithNoTests --maxWorkers=1",
"pretest:fast": "npm run build:cached",
"test:fast": "vitest run --project domain --project services --project adapters --project composition --project boundary --project process-boundary --project mcp-boundary --project conformance",
"test:run": "vitest run",
"test:local": "node scripts/run-development-tests.mjs local",
"test:fast": "vitest run --project domain --project services --project adapters --project composition --project conformance --project evaluation",
"pretest:boundary": "npm run build:cached",
"test:boundary": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global",
"pretest:mcp": "npm run build:cached",
"test:mcp": "vitest run --project mcp-boundary",
"pretest:acceptance": "npm run build:cached",
"test:acceptance": "vitest run --project acceptance",
"pretest:changed": "npm run build:cached",
"test:changed": "vitest run --project domain --project services --project adapters --project composition --project boundary --project process-boundary --project mcp-boundary --project conformance --project evaluation --changed",
"test:changed": "node scripts/run-development-tests.mjs changed",
"pretest:integration": "npm run build:cached",
"test:integration": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global --project acceptance",
"pretest:coverage": "npm run build:cached",
@@ -109,25 +113,37 @@
"test:ci:merge": "vitest --merge-reports --coverage.enabled --coverage.provider=v8",
"pretest:verbose": "npm run build:cached",
"test:verbose": "vitest run --reporter=verbose",
"pretest:watch": "npm run build:cached",
"test:watch": "vitest --project domain --project services --project adapters --project composition --project boundary --project process-boundary --project mcp-boundary --project conformance --project evaluation --changed",
"test:watch": "vitest --project domain --project services --project adapters --project composition --project conformance --project evaluation --changed",
"pretest:watch:all": "npm run build:cached",
"test:watch:all": "vitest --changed",
"typecheck": "tsc --noEmit",
"verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs",
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
"verify: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: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",
"verify:ghidra:dos": "npm run build:cached && node scripts/verify-real-ghidra-dos.mjs",
"verify:ghidra:com": "npm run build:cached && node scripts/verify-real-ghidra-com.mjs",
"verify:ghidra:aarch64-jump-table": "npm run build:cached && node scripts/verify-real-ghidra.mjs --aarch64-jump-table",
"verify:interface-builder": "npm run build:cached && node scripts/verify-interface-builder.mjs",
"verify:ghidra:cross-format": "npm run build:cached && node scripts/verify-real-ghidra.mjs --cross-format",
"verify:ghidra:windows": "npm run build:cached && npm run build:ghidra-windows-fixture && node scripts/verify-real-ghidra-windows.mjs",
"verify:browser": "npm run build:cached && node scripts/verify-real-browser.mjs",
"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:replay": "npm run build:cached && node scripts/verify-real-javascript-replay.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: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",
@@ -142,22 +158,33 @@
"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:api": "typedoc",
"docs:api:cached": "turbo run docs:api",
"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 && npm run docs:api:cached && node scripts/generate-error-schema.mjs",
"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",
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-mcp-tool-catalog.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check",
"docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs",
"prepack": "node scripts/prepack.mjs",
"prepare": "node scripts/prepare.mjs"
"prepare": "node scripts/prepare.mjs",
"verify:asset-catalog": "npm run build:cached && node scripts/verify-asset-catalog.mjs",
"verify:native-ui:permissions": "npm run build:cached && node scripts/verify-native-ui.mjs --allow-permission-denial",
"test:focused": "node scripts/run-development-tests.mjs focused",
"build:windows-native": "node scripts/build-windows-native.mjs",
"verify:windows-native:artifact": "node scripts/verify-windows-native-artifact.mjs",
"prepublishOnly": "npm run verify:windows-native:artifact",
"verify:windows-native": "node scripts/verify-windows-native.mjs",
"verify:ghidra:windows:package": "node scripts/verify-packaged-ghidra-windows.mjs",
"build:ghidra:nativeaot": "node scripts/build-ghidra-nativeaot.mjs",
"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"
},
"lint-staged": {
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
"prettier --write",
"oxfmt",
"oxlint --fix"
],
"*.{json,md,yml,yaml}": "prettier --write"
"*.{json,md,yml,yaml}": "oxfmt"
},
"dependencies": {
"@babel/parser": "8.0.4",
@@ -166,44 +193,45 @@
"@electron/asar": "4.2.0",
"@jridgewell/trace-mapping": "0.3.31",
"@lydell/node-pty": "1.1.0",
"@modelcontextprotocol/client": "2.0.0",
"@modelcontextprotocol/server": "2.0.0",
"@modelcontextprotocol/client": "2.3.1",
"@modelcontextprotocol/server": "2.3.1",
"@xterm/addon-serialize": "0.14.0",
"@xterm/headless": "6.0.0",
"@zip.js/zip.js": "2.8.26",
"canonicalize": "3.0.0",
"cross-spawn": "7.0.6",
"diff": "9.0.0",
"graphology": "0.26.0",
"graphology-shortest-path": "2.1.0",
"ignore": "7.0.6",
"incur": "0.4.13",
"incur": "0.6.1",
"isomorphic-git": "1.38.7",
"pino": "9.14.0",
"jsonc-parser": "3.3.1",
"pino": "10.4.0",
"playwright-core": "1.58.2",
"plist": "5.0.0",
"semver": "7.8.5",
"smol-toml": "^1.7.0",
"smol-toml": "1.7.0",
"write-file-atomic": "7.0.0",
"ws": "8.21.0",
"zod": "4.4.3"
},
"devDependencies": {
"@fast-check/vitest": "0.4.1",
"@types/node": "24.10.1",
"@fast-check/vitest": "0.5.0",
"@types/cross-spawn": "6.0.6",
"@types/node": "26.6.4",
"@types/semver": "7.7.1",
"@types/write-file-atomic": "4.0.3",
"@types/ws": "8.18.1",
"@vitest/coverage-v8": "4.1.10",
"@vitest/coverage-v8": "5.0.3",
"ajv": "8.20.0",
"husky": "9.1.7",
"jscpd": "5.0.12",
"knip": "6.26.0",
"knip": "6.39.0",
"lint-staged": "16.4.0",
"oxlint": "1.73.0",
"prettier": "3.6.2",
"turbo": "2.10.5",
"typedoc": "0.28.20",
"typescript": "5.9.3",
"vitest": "4.1.10",
"oxfmt": "0.72.0",
"oxlint": "1.87.0",
"turbo": "2.11.7",
"typescript": "7.0.2",
"vitest": "5.0.3",
"yaml": "2.9.0"
}
}
+1
View File
@@ -21,6 +21,7 @@
{ "type": "generic", "path": "README_ja.md" },
{ "type": "generic", "path": "README_ko.md" },
{ "type": "generic", "path": "README_ar.md" },
{ "type": "generic", "path": "docs/installation.md" },
{
"type": "generic",
"path": "src/generatedPackageMetadata.ts"
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import {
mkdir,
mkdtemp,
readdir,
readFile,
rm,
writeFile,
} from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const source = resolve(
process.env.REA_NATIVEAOT_SOURCE ??
join(root, "third_party/ghidra-nativeaot"),
);
const install = process.env.GHIDRA_INSTALL_DIR;
if (!install)
throw new Error(
"NativeAOT build lane requires existing GHIDRA_INSTALL_DIR (Ghidra 12.1.4).",
);
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 output = resolve(
process.env.REA_NATIVEAOT_BUILD_ROOT ??
join(root, "_reference/nativeaot-integration/extension"),
);
const java = process.env.JAVA_HOME ? join(process.env.JAVA_HOME, "bin") : null;
const run = (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.trim();
};
const javac = java ? join(java, "javac") : "javac";
if (!/^javac 21(?:\.|$)/u.test(run(javac, ["-version"])))
throw new Error(
"NativeAOT build lane requires an existing JDK 21 javac; REA does not install or upgrade Java.",
);
const revision = run("git", ["-C", source, "rev-parse", "HEAD"]);
if (
revision !== "effeb734fc570c32650f88b159608979dc7b423e" ||
run("git", ["-C", source, "status", "--porcelain"])
)
throw new Error(
"NativeAOT build requires the clean pinned upstream source; initialize third_party/ghidra-nativeaot.",
);
const files = async (path, extension) => {
const result = [];
for (const entry of await readdir(path, { withFileTypes: true })) {
const child = join(path, entry.name);
if (entry.isDirectory()) result.push(...(await files(child, extension)));
else if (entry.isFile() && child.endsWith(extension)) result.push(child);
}
return result.sort();
};
await mkdir(output, { recursive: true });
const classes = await mkdtemp(join(output, "classes-"));
try {
const sources = [
...(await files(join(source, "src/main/java"), ".java")),
...(await files(join(root, "bridge/ghidra/extensions/nativeaot"), ".java")),
];
const jars = await files(join(install, "Ghidra"), ".jar");
const args = [
"-proc:none",
"-cp",
jars.join(process.platform === "win32" ? ";" : ":"),
"-d",
classes,
...sources,
];
await writeFile(
join(output, "javac.args"),
args
.map((s) => `"${s.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`)
.join("\n"),
);
run(javac, [
"-J-Xmx512m",
"-J-XX:ActiveProcessorCount=1",
`@${join(output, "javac.args")}`,
]);
await writeFile(
join(classes, "LICENSE-ghidra-nativeaot.md"),
await readFile(join(source, "LICENSE.md")),
);
const jar = join(output, "rea-ghidra-nativeaot.jar");
run(java ? join(java, "jar") : "jar", [
"--create",
"--file",
jar,
"--date=2025-01-01T00:00:00Z",
"-C",
classes,
".",
]);
const sha256 = createHash("sha256")
.update(await readFile(jar))
.digest("hex");
await writeFile(
join(output, "build.json"),
JSON.stringify(
{
jar,
sha256,
upstream_revision: revision,
ghidra_version: "12.1.4",
integration_api: 1,
},
null,
2,
) + "\n",
);
console.log(
JSON.stringify({
jar,
sha256,
upstream_revision: revision,
ghidra_version: "12.1.4",
integration_api: 1,
}),
);
} finally {
await rm(classes, { recursive: true, force: true });
}
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const nodeVersion = "22.19.0";
const compiler = "x86_64-w64-mingw32-g++";
const cache = join(homedir(), ".cache", "rea-windows-native");
const output = join(root, "native", "windows", "build");
const run = (command, args) => {
const result = spawnSync(command, args, {
cwd: root,
encoding: "utf8",
maxBuffer: 4 * 1_024 * 1_024,
timeout: 120_000,
});
if (result.error !== undefined)
throw new Error(
`Windows native build lane requires ${command}: ${result.error.message}`,
);
if (result.status !== 0)
throw new Error(
`Windows native build failed: ${result.stdout}${result.stderr}`,
);
return result.stdout;
};
if (process.platform !== "linux")
throw new Error(
"The Windows x64 artifact build lane currently requires Linux and MinGW-w64.",
);
run(compiler, ["--version"]);
await mkdir(cache, { recursive: true });
await mkdir(output, { recursive: true });
const exists = async (path) =>
access(path).then(
() => true,
() => false,
);
const fetchBytes = async (name) => {
const response = await fetch(
`https://nodejs.org/dist/v${nodeVersion}/${name}`,
{
signal: AbortSignal.timeout(60_000),
},
);
if (!response.ok)
throw new Error(
`Node build prerequisite download failed: ${name}: HTTP ${response.status}`,
);
return Buffer.from(await response.arrayBuffer());
};
const checksums = (await fetchBytes("SHASUMS256.txt")).toString("utf8");
const verifiedDownload = async (name, path) => {
if (!(await exists(path))) await writeFile(path, await fetchBytes(name));
const digest = createHash("sha256")
.update(await readFile(path))
.digest("hex");
const expected = checksums
.split("\n")
.find((line) => line.slice(66) === name)
?.slice(0, 64);
if (expected === undefined || digest !== expected)
throw new Error(`Node build prerequisite SHA-256 mismatch: ${name}`);
};
const cachedHeaders = join(
homedir(),
".cache",
"node-gyp",
nodeVersion,
"include",
"node",
);
let headers = cachedHeaders;
if (!(await exists(join(headers, "node_api.h")))) {
const archive = join(cache, `node-v${nodeVersion}-headers.tar.gz`);
await verifiedDownload(`node-v${nodeVersion}-headers.tar.gz`, archive);
run("tar", ["xzf", archive, "-C", cache]);
headers = join(cache, `node-v${nodeVersion}`, "include", "node");
}
const artifact = "rea-windows-x64.node";
// MSVC short-import libraries do not produce a correct Node import table with
// GNU ld. Generate a GNU import library for the exact stable Node-API symbols.
const sources = ["addon.cc", "filesystem.cc", "process.cc"];
const sourceText = (
await Promise.all(
sources.map((path) =>
readFile(join(root, "native", "windows", "src", path), "utf8"),
),
)
).join("\n");
const symbols = [
...new Set(
[...sourceText.matchAll(/\b(napi_[a-z0-9_]+)\s*\(/gu)].map(
(match) => match[1],
),
),
].sort();
const definition = join(cache, "node-api.def");
const library = join(cache, "libnode-api.a");
await writeFile(
definition,
`LIBRARY node.exe\nEXPORTS\n${symbols.join("\n")}\n`,
);
run("x86_64-w64-mingw32-dlltool", ["-d", definition, "-l", library]);
run(compiler, [
"-std=c++17",
"-O2",
"-DNAPI_VERSION=8",
"-D_WIN32_WINNT=0x0A00",
"-shared",
"-static",
"-static-libgcc",
"-static-libstdc++",
"-Wl,--no-insert-timestamp",
`-I${headers}`,
"native/windows/src/addon.cc",
"native/windows/src/filesystem.cc",
"native/windows/src/process.cc",
library,
"-ladvapi32",
"-lbcrypt",
"-lntdll",
"-o",
join(output, artifact),
]);
const packageJson = JSON.parse(
await readFile(join(root, "package.json"), "utf8"),
);
const bytes = await readFile(join(output, artifact));
const metadata = {
packageVersion: packageJson.version,
platform: "win32",
architecture: "x64",
abiVersion: 1,
nodeApiVersion: 8,
artifact,
artifactSha256: createHash("sha256").update(bytes).digest("hex"),
};
await writeFile(
join(output, "manifest.json"),
`${JSON.stringify(metadata, null, 2)}\n`,
);
process.stdout.write(
`${JSON.stringify({ ...metadata, artifactBytes: bytes.length })}\n`,
);
+70
View File
@@ -0,0 +1,70 @@
import { createHash, randomUUID } from "node:crypto";
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" };
const args = process.argv.slice(2);
if (args.length > 1)
throw new Error("Usage: node scripts/fetch-android-fixtures.mjs [DIRECTORY]");
const directory = resolve(
args[0] ??
fileURLToPath(new URL("../_reference/apk-integration/", import.meta.url)),
);
await mkdir(directory, { recursive: true });
const hashFile = async (path) => {
const hash = createHash("sha256");
for await (const chunk of createReadStream(path)) hash.update(chunk);
return hash.digest("hex");
};
for (const item of [
{
url: JADX_RELEASE.url,
sha256: JADX_RELEASE.sha256,
filename: "jadx-headless-mcp-0.7.1-all.jar",
},
fixture,
]) {
const path = join(directory, item.filename);
try {
const digest = await hashFile(path);
if (digest !== item.sha256)
throw new Error(
`Existing ${path} has a different SHA-256; it was not overwritten`,
);
console.log(`Verified existing ${path}: ${digest}`);
continue;
} catch (cause) {
if (!(cause instanceof Error && "code" in cause && cause.code === "ENOENT"))
throw cause;
}
const temporary = `${path}.${randomUUID()}.partial`;
try {
const response = await fetch(item.url, {
signal: AbortSignal.timeout(120_000),
});
if (!response.ok || response.body === null)
throw new Error(`Download failed: ${item.url}: HTTP ${response.status}`);
const handle = await open(temporary, "wx", 0o600);
const hash = createHash("sha256");
try {
for await (const chunk of response.body) {
hash.update(chunk);
await handle.writeFile(chunk);
}
} finally {
await handle.close();
}
const digest = hash.digest("hex");
if (digest !== item.sha256)
throw new Error(
`SHA-256 mismatch for ${item.filename}: expected ${item.sha256}, received ${digest}`,
);
await link(temporary, path);
console.log(`Downloaded and verified ${path}: ${digest}`);
} finally {
await rm(temporary, { force: true });
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"name": "Appium ApiDemos",
"version": "6.0.18",
"source": "https://github.com/appium/android-apidemos",
"source_revision": "3e716ee04ef3390284bfab761e28da8c15c08143",
"license": "Apache-2.0",
"license_url": "https://github.com/appium/android-apidemos/blob/3e716ee04ef3390284bfab761e28da8c15c08143/LICENSE",
"url": "https://github.com/appium/android-apidemos/releases/download/v6.0.18/ApiDemos-debug.apk",
"filename": "ApiDemos-debug.apk",
"sha256": "a9eecf37b26cd084855c530db81c2bb1b91f4c1b095a04f47aa7c20e2791f686",
"package": "io.appium.android.apis",
"class_name": "io.appium.android.apis.ApiDemos",
"method_name": "onCreate"
}
+109
View File
@@ -0,0 +1,109 @@
import { once } from "node:events";
import { createServer } from "node:http";
import { gzipSync } from "node:zlib";
export const networkFixtureSecret = "私密-network-fixture";
export const networkFixtureBinary = Buffer.from([0xff, 0, 0xfe, 1]);
export const networkFixtureDecoded = Buffer.from("decoded-response-marker");
const html = `<!doctype html><meta charset="utf-8"><body><button id="start">Run</button><main id="done" hidden>Done</main>
<script>document.querySelector('#start').onclick = async () => {
const send = marker => fetch('/same', {method:'POST', headers:{
'Content-Type':'application/json', 'Authorization':'Bearer transport-only', 'X-Marker': marker
}, body:JSON.stringify({marker, password:'ordinary-selected-value', declared:'${networkFixtureSecret}'})}).then(r=>r.text());
await Promise.all([send('slow'),send('fast'),fetch('/redirect').then(r=>r.text()),
fetch('/binary').then(r=>r.arrayBuffer()),fetch('/compressed').then(r=>r.text()),
fetch('/empty').then(r=>r.text()),
fetch('/form', {method:'POST', body:new URLSearchParams({declared:'${networkFixtureSecret}'})}).then(r=>r.text())]);
await fetch('/stream');
document.querySelector('#done').hidden=false;
};</script>`;
/** Start an isolated fixture with concurrent, redirected, binary, and streaming responses. */
export async function startBrowserNetworkSite() {
const counts = new Map();
const timers = new Set();
const server = createServer(async (request, response) => {
const path = new URL(request.url, "http://fixture.test").pathname;
counts.set(path, (counts.get(path) ?? 0) + 1);
if (path === "/same") {
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
const body = JSON.parse(Buffer.concat(chunks).toString());
const timer = setTimeout(
() => {
timers.delete(timer);
response.writeHead(200, [
"Content-Type",
"application/json",
"X-Repeat",
"first",
"X-Repeat",
"second",
"Set-Cookie",
"fixture_session=transport-cookie; HttpOnly",
"X-Declared",
encodeURIComponent(networkFixtureSecret),
]);
response.end(
JSON.stringify({
marker: body.marker,
password: body.password,
declared: body.declared,
}),
);
},
body.marker === "slow" ? 120 : 5,
);
timers.add(timer);
} else if (path === "/redirect") {
response.writeHead(302, { Location: "/landing" });
response.end();
} else if (path === "/landing") {
response.end("redirect-target");
} else if (path === "/form") {
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
response.end(
new URLSearchParams(Buffer.concat(chunks).toString()).get("declared"),
);
} else if (path === "/binary") {
response.writeHead(200, { "Content-Type": "application/octet-stream" });
response.end(networkFixtureBinary);
} else if (path === "/compressed") {
response.writeHead(200, {
"Content-Type": "text/plain",
"Content-Encoding": "gzip",
});
response.end(gzipSync(networkFixtureDecoded));
} else if (path === "/empty") {
response.writeHead(204);
response.end();
} else if (path === "/stream") {
response.writeHead(200, { "Content-Type": "text/event-stream" });
response.write("data: still-open\n\n");
} else if (path === "/favicon.ico") {
response.writeHead(204);
response.end();
} else {
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
response.end(html);
}
});
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
if (typeof address !== "object" || address === null)
throw new Error("Network fixture has no TCP address");
return {
origin: `http://127.0.0.1:${address.port}`,
counts,
async close() {
for (const timer of timers) clearTimeout(timer);
server.closeAllConnections();
await new Promise((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
);
},
};
}
+49
View File
@@ -0,0 +1,49 @@
import { createServer } from "node:http";
/** Independently authored modules and competing query variants for export verification. */
export async function startBrowserScriptSite() {
const assets = new Map([
[
"/app/main.js",
"import { sourceMarker } from './lib/dep.js'; export const result = sourceMarker; document.body.dataset.ready = 'true';\n",
],
[
"/app/lib/dep.js",
"export const sourceMarker = 'independent-source-marker';\n",
],
["/variant.js?v=1", "globalThis.variantOne = 'query-variant-one';\n"],
["/variant.js?v=2", "globalThis.variantTwo = 'query-variant-two';\n"],
]);
const counts = new Map();
const server = createServer((request, response) => {
counts.set(request.url, (counts.get(request.url) ?? 0) + 1);
const source = assets.get(request.url);
if (source !== undefined) {
response.setHeader("content-type", "text/javascript; charset=utf-8");
response.end(source);
return;
}
response.setHeader("content-type", "text/html; charset=utf-8");
response.end(
'<!doctype html><html><body><script src="/variant.js?v=1"></script><script src="/variant.js?v=2"></script><script type="module" src="/app/main.js"></script></body></html>',
);
});
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", resolve);
});
const address = server.address();
if (address === null || typeof address === "string")
throw new Error("Script fixture did not bind a port");
return {
origin: `http://127.0.0.1:${address.port}`,
assets,
counts,
close: () =>
new Promise((resolve, reject) =>
server.close((cause) =>
cause === undefined ? resolve() : reject(cause),
),
),
};
}
+23 -2
View File
@@ -7,6 +7,21 @@ export async function startBrowserVerifierSite() {
let port = 0;
let sessionGeneration = 0;
const server = createServer((request, response) => {
if (request.url === "/screenshot-noise") {
response.setHeader("content-type", "text/html");
response.end(`<!doctype html><html><body style="margin:0"><canvas width="2048" height="1536"></canvas><script>
const context = document.querySelector('canvas').getContext('2d');
const image = context.createImageData(2048, 1536);
let state = 0x6d2b79f5;
for (let index = 0; index < image.data.length; index++) {
state ^= state << 13; state ^= state >>> 17; state ^= state << 5;
image.data[index] = index % 4 === 3 ? 255 : state & 255;
}
context.putImageData(image, 0, 0);
document.body.dataset.ready = 'true';
</script></body></html>`);
return;
}
if (request.url === "/app.js.map") {
response.setHeader("content-type", "application/source-map+json");
response.end(sourceMap());
@@ -37,8 +52,12 @@ export async function startBrowserVerifierSite() {
return;
}
response.setHeader("content-type", "text/html");
response.setHeader("set-cookie", [
"rea-root-cookie=root-fixture; Path=/; SameSite=Lax",
"rea-app-cookie=app-fixture; Path=/app; SameSite=Lax",
]);
response.end(
'<!doctype html><html><head><title>REA browser verifier</title></head><body><main><h1>Browser evidence</h1><button aria-label="ax-private-label-value">Verify</button></main><script type="module" src="/app.js?token=script-query-secret"></script></body></html>',
'<!doctype html><html><head><title>REA browser verifier</title><base href="assets/"></head><body><main><h1>Browser evidence</h1><a href="guide">Guide</a><button aria-label="ax-private-label-value">Verify</button><form action=""></form><form action=" "></form><button formaction=""></button><input formaction=""></main><script type="module" src="/app.js?token=script-query-secret"></script></body></html>',
);
});
const webSockets = new WebSocketServer({ noServer: true });
@@ -102,7 +121,9 @@ const browserScript = (port) => `
const { generation } = await response.json();
if (generation <= observedGeneration) return;
observedGeneration = generation;
history.pushState({}, "", "/app/session-" + String(generation));
const sessionUrl = new URL(window.location.href);
sessionUrl.pathname = "/app/session-" + String(generation);
history.pushState({}, "", sessionUrl);
}, 100);
//# sourceMappingURL=/app.js.map
`;
+5 -4
View File
@@ -2,7 +2,7 @@ import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { analysisErrorJsonSchema } from "../dist/contracts/errorSchemas.js";
import { format } from "prettier";
import { formatGeneratedFile } from "./lib/format-generated-file.mjs";
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
const arguments_ = new Set(process.argv.slice(2));
@@ -13,9 +13,10 @@ for (const argument of arguments_)
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
await ensureGeneratedFile({
path: join(root, "docs/error-contract.schema.json"),
source: await format(JSON.stringify(analysisErrorJsonSchema), {
parser: "json",
}),
source: await formatGeneratedFile(
"docs/error-contract.schema.json",
JSON.stringify(analysisErrorJsonSchema),
),
check: arguments_.has("--check"),
generateCommand: "npm run docs:generate",
});
+42
View File
@@ -0,0 +1,42 @@
import { execFile } from "node:child_process";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
const exec = promisify(execFile);
const root = resolve(
process.env.REA_FIRMWARE_FIXTURE_ROOT ??
"_reference/firmware-integration/generated",
);
for (const [command, args, purpose] of [
[
process.env.REA_FIRMWARE_PYTHON ?? "python3",
["--version"],
"Python 3 standard-library fixture generator",
],
[
process.env.REA_FIRMWARE_CC ?? "cc",
["--version"],
"host ELF fixture compiler",
],
]) {
try {
await exec(command, args);
} catch (cause) {
throw new Error(`fixtures:firmware requires ${purpose}: ${command}`, {
cause,
});
}
}
const ext4 =
process.env.REA_FIRMWARE_VERIFY_EXT4 === "1"
? [process.env.REA_FIRMWARE_MKE2FS_COMMAND ?? "mke2fs"]
: [];
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)),
root,
process.env.REA_FIRMWARE_CC ?? "cc",
...ext4,
]);
console.log(generated.stdout.trim());
+17 -4
View File
@@ -1,7 +1,8 @@
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { format } from "prettier";
import { formatGeneratedFile } from "./lib/format-generated-file.mjs";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { Ajv2020 } from "ajv/dist/2020.js";
import { McpServer } from "@modelcontextprotocol/server";
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
@@ -28,6 +29,16 @@ const sessionToolNames = new Set([
...MANAGED_WORKFLOW_TOOL_CONTRACTS.map(({ name }) => name),
]);
const advertisedTools = await sdkToolCatalog();
const ajv = new Ajv2020({ strict: false, validateFormats: false });
for (const tool of advertisedTools.values()) {
for (const kind of ["inputSchema", "outputSchema"]) {
const schema = tool[kind];
if (schema !== undefined && !ajv.validateSchema(schema))
throw new Error(
`Invalid JSON Schema for ${tool.name}.${kind}: ${ajv.errorsText(ajv.errors)}`,
);
}
}
const catalog = TOOL_CONTRACTS.map((contract) => {
const advertised = advertisedTools.get(contract.name);
if (advertised === undefined)
@@ -78,7 +89,7 @@ async function sdkToolCatalog() {
}
}
const auxiliaryProviders = [
new ArtifactProvider(false, false),
new ArtifactProvider(CATALOG_PLATFORM),
new NativeMacOSProvider(undefined, CATALOG_PLATFORM),
new ManagedStaticProvider(),
].map((provider) => ({
@@ -98,8 +109,11 @@ const canonicalizeJson = (value) => {
const payloadJson = JSON.stringify(
canonicalizeJson({ catalog, auxiliaryProviders }),
null,
2,
);
const source = await format(
const source = await formatGeneratedFile(
"src/generatedMcpToolCatalog.ts",
`import type { ToolAnnotations } from "@modelcontextprotocol/server";
import type {
CapabilityDescriptor,
@@ -183,7 +197,6 @@ function isRecord(
return typeof value === "object" && value !== null && !Array.isArray(value);
}
`,
{ parser: "typescript" },
);
await ensureGeneratedFile({
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
if (process.arch !== "x64" || !["linux", "win32"].includes(process.platform))
throw new Error(
"NativeAOT fixture lane requires Linux or Windows x64; it does not cross-compile NativeAOT.",
);
const runtime = process.platform === "win32" ? "win-x64" : "linux-x64";
const directory = resolve(
process.env.REA_NATIVEAOT_FIXTURE_ROOT ??
join(root, "_reference/nativeaot-integration/generated"),
);
const output = join(directory, runtime);
await mkdir(output, { recursive: true });
const env = {
...process.env,
DOTNET_CLI_HOME: join(output, "cli-home"),
DOTNET_CLI_TELEMETRY_OPTOUT: "1",
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1",
DOTNET_PROCESSOR_COUNT: "1",
COMPlus_GCHeapHardLimit: "30000000",
NUGET_PACKAGES: process.env.NUGET_PACKAGES ?? join(directory, "nuget"),
};
await writeFile(
join(output, "global.json"),
JSON.stringify({ sdk: { version: "8.0.416", rollForward: "disable" } }) +
"\n",
);
const run = (command, args) => {
const result = spawnSync(command, args, {
cwd: output,
env,
encoding: "utf8",
timeout: 300_000,
maxBuffer: 8 * 1024 * 1024,
});
if (result.error || result.status !== 0)
throw new Error(
`NativeAOT fixture lane requires/action failed ${command}: ${result.error?.message ?? ""}\n${result.stdout ?? ""}${result.stderr ?? ""}`,
);
return result.stdout;
};
const dotnet = process.env.REA_NATIVEAOT_DOTNET ?? "dotnet";
if (run(dotnet, ["--version"]).trim() !== "8.0.416")
throw new Error(
"NativeAOT fixtures require existing .NET SDK 8.0.416; REA does not install SDKs.",
);
if (process.platform === "linux") {
run(process.env.REA_NATIVEAOT_STRIP ?? "strip", ["--version"]);
run(process.env.REA_NATIVEAOT_NM ?? "nm", ["--version"]);
run(process.env.REA_NATIVEAOT_CC ?? "cc", ["--version"]);
run("clang", ["--version"]);
}
const map = join(output, "NativeAotFixture.map");
run(dotnet, [
"publish",
join(root, "tests/fixtures/nativeaot/NativeAotFixture.csproj"),
"-r",
runtime,
"-c",
"Release",
"-o",
join(output, "symbols"),
"--maxcpucount:1",
`-p:BaseIntermediateOutputPath=${join(output, "obj")}/`,
`-p:BaseOutputPath=${join(output, "bin")}/`,
`-p:NativeAotMapPath=${map}`,
]);
const name =
process.platform === "win32" ? "NativeAotFixture.exe" : "NativeAotFixture";
const binary = join(output, "symbols", name);
let symbols = "";
if (process.platform === "linux") {
// Preserve the symbol table as an independent oracle while avoiding a full
// runtime DWARF import in this metadata-specific lane.
run(process.env.REA_NATIVEAOT_STRIP ?? "strip", ["--strip-debug", binary]);
symbols = run(process.env.REA_NATIVEAOT_NM ?? "nm", ["-n", binary]);
await mkdir(join(output, "stripped"), { recursive: true });
const stripped = join(output, "stripped", name);
await copyFile(binary, stripped);
run(process.env.REA_NATIVEAOT_STRIP ?? "strip", ["--strip-all", stripped]);
} else {
symbols = await readFile(map, "utf8");
}
const bytes = await readFile(binary);
const candidates = [];
for (let offset = 0; offset <= bytes.length - 16; offset++) {
if (
bytes.readUInt32LE(offset) !== 0x00525452 ||
bytes[offset + 14] !== 24 ||
bytes[offset + 15] !== 1
)
continue;
const count = bytes.readUInt16LE(offset + 12);
if (count === 0 || count > 80 || offset + 16 + count * 24 > bytes.length)
continue;
candidates.push({
offset,
major: bytes.readUInt16LE(offset + 4),
minor: bytes.readUInt16LE(offset + 6),
sections: Array.from({ length: count }, (_, index) => {
const row = offset + 16 + index * 24;
return {
type: bytes.readUInt32LE(row),
flags: bytes.readUInt32LE(row + 4),
start: `0x${bytes.readBigUInt64LE(row + 8).toString(16)}`,
end: `0x${bytes.readBigUInt64LE(row + 16).toString(16)}`,
};
}),
});
}
if (
candidates.length !== 1 ||
candidates[0].major !== 9 ||
candidates[0].minor !== 1
)
throw new Error(
"NativeAOT fixture did not produce exactly one independent RTR 9.1 directory.",
);
const sha256 = createHash("sha256").update(bytes).digest("hex");
await writeFile(join(output, "symbols.txt"), symbols);
await writeFile(
join(output, "oracle.json"),
JSON.stringify(
{
sdk: "8.0.416",
runtime_version: "8.0.22",
runtime,
binary,
sha256,
headers: candidates,
expected_strings: [
"REA_NATIVEAOT_FROZEN",
"REA_NATIVEAOT_BASE",
"REA_NATIVEAOT_DERIVED",
],
},
null,
2,
) + "\n",
);
// A source-built negative input, not a downloaded third-party target.
if (process.platform === "linux") {
const source = join(output, "ordinary.c");
await writeFile(
source,
"int ordinary_probe(int x) { return x * 3 + 1; }\nint main(void) { return ordinary_probe(1); }\n",
);
run(process.env.REA_NATIVEAOT_CC ?? "cc", [
"-g",
"-O0",
source,
"-o",
join(output, "ordinary"),
]);
// Small source-built producer regressions run in seconds: no executable
// patching, large runtime import, or downloaded opaque negative inputs.
const row = "struct Row { unsigned type, flags; const char *start, *end; };";
const header =
"struct Header { unsigned signature; unsigned short major, minor; unsigned flags; unsigned short count; unsigned char size, type; struct Row row; };";
for (const kind of ["unsupported", "malformed", "ambiguous"]) {
const major = kind === "unsupported" ? 99 : 9;
const declaration = `const struct Header __attribute__((aligned(8))) probe = {0x00525452, ${major}, 1, 0, 1, 24, 1, {207, 1, data + 8, data}};`;
const extra =
kind === "ambiguous" ? declaration.replace("probe", "second_probe") : "";
const negative = join(output, `${kind}.c`);
await writeFile(
negative,
`${row}\n${header}\nconst char data[16] = {0};\n${declaration}\n${extra}\nint main(void) { return 0; }\n`,
);
run(process.env.REA_NATIVEAOT_CC ?? "cc", [
"-O0",
"-fno-pie",
"-no-pie",
negative,
"-o",
join(output, kind),
]);
}
}
console.log(
`PASS source-built ${runtime} NativeAOT fixture, independent RTR 9.1 oracle and SHA-256 ${sha256}; no target execution`,
);
+120
View File
@@ -0,0 +1,120 @@
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import {
cleanupOwnedProcessGroup,
readProcessRunId,
} from "../../dist/process/ProcessOwnership.js";
/** Exercise a real CLI signal while the owned Java engine is still running. */
export const verifyAndroidCliCancellation = async ({
entrypoint,
environment,
path,
repository,
execute,
}) => {
const child = spawn(
process.execPath,
[entrypoint, "inspect-android-package", path, "--format", "json"],
{ cwd: repository, env: environment, stdio: ["ignore", "pipe", "pipe"] },
);
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
const exited = new Promise((resolve, reject) => {
child.once("error", reject);
child.once("exit", (code, signal) => resolve({ code, signal }));
});
let group;
let ownership;
try {
const deadline = Date.now() + 30_000;
while (Date.now() < deadline) {
const table = (
await execute("ps", ["-eo", "pid=,ppid=,pgid=,args="], {
timeout: 5_000,
maxBuffer: 2 * 1024 * 1024,
})
).stdout;
for (const row of table.split("\n")) {
const entry = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/u.exec(row);
if (
entry !== null &&
Number(entry[2]) === child.pid &&
entry[4].includes("engine.jar") &&
entry[4].includes("-Xmx512m")
) {
group = Number(entry[3]);
const runId = await readProcessRunId(Number(entry[1]));
assert.ok(
runId !== undefined,
"Owned Java process did not carry an identity token",
);
ownership = {
runId,
leaderPid: Number(entry[1]),
processGroupId: group,
};
}
}
if (group !== undefined) break;
if (child.exitCode !== null || child.signalCode !== null)
throw new Error(
`CLI exited before acquiring JADX: ${stdout}\n${stderr}`,
);
await new Promise((resolve) => setTimeout(resolve, 100));
}
assert.ok(
group !== undefined,
"CLI did not acquire the expected owned Java group",
);
child.kill("SIGTERM");
let timer;
const result = await Promise.race([
exited,
new Promise((_, reject) => {
timer = setTimeout(
() => reject(new Error("CLI cancellation did not finish cleanup")),
10_000,
);
}),
]).finally(() => clearTimeout(timer));
assert.equal(result.code, 143, `${stdout}\n${stderr}`);
assert.equal(result.signal, null);
assert.equal(JSON.parse(stdout).code, "cancelled");
assert.throws(() => process.kill(-group, 0), { code: "ESRCH" });
console.log(
"PASS real CLI SIGTERM cancellation and owned Java group cleanup",
);
} finally {
await cleanupCancellation(child, exited, ownership);
}
};
const cleanupCancellation = async (child, exited, ownership) => {
if (child.exitCode === null && child.signalCode === null) {
child.kill("SIGTERM");
let timer;
await Promise.race([
exited,
new Promise((resolve) => {
timer = setTimeout(resolve, 2_000);
}),
]).finally(() => clearTimeout(timer));
if (child.exitCode === null && child.signalCode === null) {
child.kill("SIGKILL");
await exited;
}
}
if (ownership !== undefined)
assert.equal(
(await cleanupOwnedProcessGroup(ownership)).cleaned,
true,
"Verifier could not confirm its owned Java group cleanup",
);
};
+64
View File
@@ -0,0 +1,64 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
const environment = () => ({
PATH: process.env.PATH ?? "/usr/bin:/bin",
REA_LOG_LEVEL: "silent",
REA_ANALYSIS_PROVIDER: "auto",
// Artifact workflows have no deep-provider prerequisite or GUI authority.
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
});
/** Execute the compiled CLI with production providers and no test adapters. */
export async function artifactCli(command, target, arguments_ = []) {
const { stdout } = await promisify(execFile)(
process.execPath,
[
fileURLToPath(new URL("../rea.mjs", import.meta.url)),
command,
target,
...arguments_,
"--json",
],
{ env: environment(), timeout: 60000, maxBuffer: 16 * 1024 * 1024 },
);
const evidence = JSON.parse(stdout);
assert.equal(evidence.error, undefined, stdout);
assert.ok(evidence.normalized_result, "CLI omitted normalized result");
return evidence.normalized_result;
}
/** Run a real stdio MCP subprocess, owning and closing its transport. */
export async function withArtifactMcp(target, verify) {
const transport = new StdioClientTransport({
command: process.execPath,
args: [fileURLToPath(new URL("../rea.mjs", import.meta.url)), "mcp"],
env: environment(),
stderr: "pipe",
});
const client = new Client({ name: "artifact-real-e2e", version: "1" });
try {
await client.connect(transport);
const opened = await client.callTool({
name: "open_binary",
arguments: { path: target },
});
assert.notEqual(opened.isError, true, JSON.stringify(opened));
await verify(client);
} finally {
await client.close();
await transport.close();
}
}
/** Require a successful structured MCP tool result rather than its text rendering. */
export async function artifactMcpResult(client, name, arguments_ = {}) {
const result = await client.callTool({ name, arguments: arguments_ });
assert.notEqual(result.isError, true, JSON.stringify(result));
assert.ok(result.structuredContent?.result, "MCP omitted structured result");
return result.structuredContent.result;
}
@@ -0,0 +1,60 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
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 { assertFormDestinations } from "./browser-verifier-assertions.mjs";
import { mcpTextValue } from "./mcp-verifier-results.mjs";
/** Verify the real fixture's form destinations through CLI and stdio MCP. */
export async function verifyBrowserDomDestinations(
attachment,
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
documentUrl,
) {
const env = { ...process.env, REA_LOG_LEVEL: "silent" };
const { stdout } = await promisify(execFile)(
process.execPath,
[
entrypoint,
"inspect-web-page",
attachment.cdp_endpoint,
attachment.target_id,
"--observation-ms",
"100",
"--json",
],
{ env, timeout: 60000, maxBuffer: 32 * 1024 * 1024 },
);
assertFormDestinations(
parseEvidence(JSON.parse(stdout)).normalized_result,
documentUrl,
);
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({ name: "browser-dom-destinations", version: "1" });
try {
await client.connect(transport);
const result = await client.callTool({
name: "inspect_web_page",
arguments: { ...attachment, observation_ms: 100 },
});
assert.notEqual(result.isError, true, mcpTextValue(result));
assertFormDestinations(
parseEvidence(JSON.parse(mcpTextValue(result)).evidence)
.normalized_result,
documentUrl,
);
return { cli: true, stdio_mcp: true, current_document_destinations: 4 };
} finally {
await client.close();
await transport.close();
}
}
+156
View File
@@ -0,0 +1,156 @@
import assert from "node:assert/strict";
import {
networkFixtureBinary,
networkFixtureDecoded,
networkFixtureSecret,
} from "../fixtures/browser-network-site.mjs";
const retained = (event) => {
assert.equal(event?.kind, "network-content");
assert.equal(event.body.state, "captured", JSON.stringify(event.body));
return Buffer.from(event.body.content, "base64");
};
/** Verify association against server-authored markers rather than provider-derived expectations. */
export function assertBrowserNetworkEvidence(
capture,
origin,
expectedCleanup = "terminated-owned-process",
) {
assert.ok(capture.steps.every((step) => step.status === "completed"));
assert.equal(capture.browser.cleanup, expectedCleanup);
assert.deepEqual(capture.scenario.network_content, {
request_body: true,
response_body: true,
header_values: true,
});
assert.equal(capture.completeness.equality_eligible, false);
assert.ok(!JSON.stringify(capture).includes(networkFixtureSecret));
const events = capture.events.items;
const content = events.filter((event) => event.kind === "network-content");
const requestEvents = events.filter((event) => event.kind === "request");
const sameUrlRequests = requestEvents.filter(
(event) => event.url.url === `${origin}/same`,
);
assert.equal(sameUrlRequests.length, 2);
assert.equal(
new Set(sameUrlRequests.map((event) => event.transaction_id)).size,
2,
);
const markers = [];
for (const request of sameUrlRequests) {
const requestContent = content.find(
(event) =>
event.phase === "request" &&
event.transaction_id === request.transaction_id,
);
const responseContent = content.find(
(event) =>
event.phase === "response" &&
event.transaction_id === request.transaction_id,
);
const input = JSON.parse(retained(requestContent).toString());
const output = JSON.parse(retained(responseContent).toString());
assert.equal(
output.marker,
input.marker,
"Response was associated with a different same-URL request",
);
assert.equal(output.password, "ordinary-selected-value");
assert.equal(input.declared, "[REDACTED:network_secret]");
assert.equal(output.declared, input.declared);
assert.equal(requestContent.body.redacted, true);
assert.equal(responseContent.headers.state, "captured");
assert.ok(
responseContent.headers.items.some(
(item) =>
item.name.toLowerCase() === "x-declared" &&
item.value === "[REDACTED:network_secret]" &&
item.redacted,
),
);
assert.deepEqual(
responseContent.headers.items
.filter((item) => item.name.toLowerCase() === "x-repeat")
.map((item) => item.value),
["first", "second"],
);
assert.ok(
responseContent.headers.items.some(
(item) =>
item.name.toLowerCase() === "set-cookie" &&
item.value === null &&
item.redacted,
),
);
assert.ok(
requestContent.headers.items.some(
(item) =>
item.name.toLowerCase() === "authorization" &&
item.value === null &&
item.redacted,
),
);
assert.ok(
events.some(
(event) =>
event.kind === "request-finished" &&
event.transaction_id === request.transaction_id,
),
);
markers.push(input.marker);
}
assert.deepEqual(markers.sort(), ["fast", "slow"]);
const responseFor = (path) => {
const response = events.find(
(event) =>
event.kind === "response" && event.url.url === `${origin}${path}`,
);
assert.ok(response, `Missing response ${path}`);
return content.find(
(event) =>
event.phase === "response" &&
event.source_event_sequence === response.sequence,
);
};
assert.deepEqual(retained(responseFor("/binary")), networkFixtureBinary);
assert.deepEqual(retained(responseFor("/compressed")), networkFixtureDecoded);
assert.equal(
responseFor("/compressed").body.representation,
"browser-decoded-response-bytes",
);
const formRequest = requestEvents.find(
(event) => event.url.url === `${origin}/form`,
);
assert.ok(formRequest);
const formContent = content.find(
(event) =>
event.phase === "request" &&
event.transaction_id === formRequest.transaction_id,
);
assert.equal(
retained(formContent).toString(),
"declared=[REDACTED:network_secret]",
);
assert.equal(
retained(responseFor("/form")).toString(),
"[REDACTED:network_secret]",
);
assert.equal(responseFor("/stream").body.state, "unavailable");
assert.equal(responseFor("/stream").body.reason, "response-unfinished");
const first = requestEvents.find(
(event) => event.url.url === `${origin}/redirect`,
);
const next = requestEvents.find(
(event) => event.url.url === `${origin}/landing`,
);
assert.ok(first && next);
assert.equal(next.redirected_from_transaction_id, first.transaction_id);
return {
same_url_markers: markers,
binary_bytes: networkFixtureBinary.length,
compressed_representation: "browser-decoded-response-bytes",
unfinished_stream: "response-unfinished",
empty_response_state: responseFor("/empty").body.state,
};
}
+192
View File
@@ -0,0 +1,192 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
import { browserScenarioCaptureSchema } from "../../dist/domain/browserScenarioCapture.js";
import { parseEvidence } from "../../dist/domain/evidence.js";
import {
startBrowserNetworkSite,
networkFixtureSecret,
} from "../fixtures/browser-network-site.mjs";
import { assertBrowserNetworkEvidence } from "./browser-network-assertions.mjs";
import { scenarioProfiles } from "./browser-scenario-verifier.mjs";
/** Exercise selected network content through real CLI and stdio MCP, one browser at a time. */
export async function verifyBrowserNetworkEvidence(
executable,
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
attachment,
) {
const site = await startBrowserNetworkSite();
const profilesBefore = await scenarioProfiles();
try {
const scenario = browserScenarioSchema.parse({
browser: { mode: "launch", executable_path: executable },
start_url: { url: site.origin },
actions: [
{
step_id: "run",
action: "click",
locator: { kind: "css", selector: "#start" },
timeout_ms: 10_000,
},
{
step_id: "ready",
action: "wait_for",
locator: { kind: "css", selector: "#done" },
state: "visible",
timeout_ms: 10_000,
},
],
secrets: [
{
secret_id: "network_secret",
environment_variable: "REA_NETWORK_FIXTURE_SECRET",
},
],
capture: {
at_end: ["url"],
network: {
request_body: true,
response_body: true,
header_values: true,
},
},
});
const env = {
...process.env,
REA_LOG_LEVEL: "silent",
REA_NETWORK_FIXTURE_SECRET: networkFixtureSecret,
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
};
const { stdout } = await promisify(execFile)(
process.execPath,
[
entrypoint,
"capture-browser-scenario",
JSON.stringify(scenario),
"--json",
],
{ env, timeout: 60_000, maxBuffer: 16 * 1024 * 1024 },
);
const cli = browserScenarioCaptureSchema.parse(
parseEvidence(JSON.parse(stdout)).normalized_result,
);
const cliProof = assertBrowserNetworkEvidence(cli, site.origin);
// The oracle must reject a deliberately incorrect same-URL association.
const swapped = structuredClone(cli);
const responses = swapped.events.items.filter(
(event) =>
event.kind === "network-content" &&
event.phase === "response" &&
event.body.state === "captured" &&
Buffer.from(event.body.content, "base64")
.toString()
.includes('"marker":'),
);
assert.equal(responses.length, 2);
[responses[0].transaction_id, responses[1].transaction_id] = [
responses[1].transaction_id,
responses[0].transaction_id,
];
assert.throws(
() => assertBrowserNetworkEvidence(swapped, site.origin),
/associated with a different same-URL request/,
);
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({ name: "network-real-e2e", version: "1" });
let mcpProof;
try {
await client.connect(transport);
const result = await client.callTool(
{ name: "capture_browser_scenario", arguments: scenario },
{ timeout: 60_000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
const evidence = parseEvidence({
...result.structuredContent?.evidence,
normalized_result: result.structuredContent?.result,
});
mcpProof = assertBrowserNetworkEvidence(
browserScenarioCaptureSchema.parse(evidence.normalized_result),
site.origin,
);
if (attachment !== undefined) {
const connectedScenario = browserScenarioSchema.parse({
...scenario,
browser: { mode: "connect", ...attachment },
});
const connected = await client.callTool(
{ name: "capture_browser_scenario", arguments: connectedScenario },
{ timeout: 60_000 },
);
assert.notEqual(connected.isError, true, JSON.stringify(connected));
const connectedEvidence = parseEvidence({
...connected.structuredContent?.evidence,
normalized_result: connected.structuredContent?.result,
});
assert.deepEqual(
assertBrowserNetworkEvidence(
browserScenarioCaptureSchema.parse(
connectedEvidence.normalized_result,
),
site.origin,
"disconnected-external",
),
cliProof,
);
assert.ok(
(await fetch(`${attachment.cdp_endpoint}/json/version`)).ok,
"Network capture closed the external browser",
);
}
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
assert.deepEqual(mcpProof, cliProof);
for (const path of [
"/redirect",
"/landing",
"/binary",
"/compressed",
"/stream",
"/empty",
"/form",
])
assert.equal(
site.counts.get(path),
attachment === undefined ? 2 : 3,
`Unexpected implicit refetch of ${path}`,
);
assert.equal(site.counts.get("/same"), attachment === undefined ? 4 : 6);
const profilesAfter = await scenarioProfiles();
assert.ok(
[...profilesAfter].every((profile) => profilesBefore.has(profile)),
"A scenario profile survived cleanup",
);
return {
...cliProof,
cli: true,
mcp: true,
association_fault_rejected: true,
implicit_refetches: 0,
profile_cleanup: true,
connected_network_verified: attachment !== undefined,
};
} finally {
await site.close();
}
}
+252
View File
@@ -0,0 +1,252 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { once } from "node:events";
import { createServer } from "node:http";
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 { PlaywrightBrowserScenarioProvider } from "../../dist/browser/PlaywrightBrowserScenarioProvider.js";
import { PlaywrightScenarioEvents } from "../../dist/browser/PlaywrightScenarioEvents.js";
import { BrowserScenarioSecrets } from "../../dist/browser/BrowserScenarioSecrets.js";
import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
import { compareBrowserScenarios } from "../../dist/domain/browserScenarioDiff.js";
import { browserScenarioCaptureSchema } from "../../dist/domain/browserScenarioCapture.js";
import { parseEvidence } from "../../dist/domain/evidence.js";
const document = (path) => {
if (path.startsWith("/nested"))
return `<script>throw new Error("nested popup failure")</script>
<script>opener.postMessage("nested-ready", "*")</script>`;
if (path.startsWith("/popup"))
return `<script>throw new Error("popup failure")</script>
<script>onmessage = e => {
if (e.data === "nested-ready") opener.postMessage("popup-ready", "*");
}; window.open("/nested");</script><iframe src="/child"></iframe>`;
if (path.startsWith("/child")) return "child frame";
return `<script>onmessage = e => {
if (e.data === "popup-ready") document.body.dataset.ready = "true";
}</script><button onclick="window.open('/redirect')">Open popup</button>`;
};
const scenarioFor = (executable, origin, events) =>
browserScenarioSchema.parse({
browser: { mode: "launch", executable_path: executable },
start_url: { url: origin },
actions: [
{
step_id: "open",
action: "click",
locator: { kind: "role", role: "button", name: "Open popup" },
},
{
step_id: "ready",
action: "wait_for",
locator: { kind: "css", selector: "body[data-ready=true]" },
state: "attached",
timeout_ms: 10_000,
},
],
capture: { events, after_each_step: [], at_end: [] },
});
const capture = async (scenario) => {
const result = await new PlaywrightBrowserScenarioProvider().captureScenario(
scenario,
);
if (!result.ok) throw result.error;
assert.ok(
result.value.steps.every((step) => step.status === "completed"),
JSON.stringify(result.value.steps),
);
return result.value;
};
const assertNetwork = (result, origin) => {
for (const path of ["/redirect", "/popup", "/nested"])
for (const kind of ["request", "response"])
assert.equal(
result.events.items.filter(
(event) => event.kind === kind && event.url?.url === origin + path,
).length,
1,
`${kind} ${path} must be captured exactly once`,
);
assert.equal(result.completeness.equality_eligible, true);
assert.equal(compareWithSelf(result).overall_status, "unchanged");
};
const compareWithSelf = (capture) =>
compareBrowserScenarios({
before_scenario: capture,
after_scenario: capture,
normalization: { rules: [] },
});
const verifySharedContext = async (executable, origin) => {
const browser = await chromium.launch({
executablePath: executable,
headless: true,
});
try {
const context = await browser.newContext();
const page = await context.newPage();
const foreign = await context.newPage();
const secrets = BrowserScenarioSecrets.resolve(
scenarioFor(executable, origin, ["network"]),
{},
);
assert.ok(secrets);
const events = new PlaywrightScenarioEvents({
page,
context,
ownsContext: false,
enabled: new Set(["network"]),
secrets,
});
await Promise.all([
page.goto(origin),
foreign.goto(origin + "/popup?unrelated-tab"),
]);
await page.getByRole("button", { name: "Open popup", exact: true }).click();
await page.locator("body[data-ready=true]").waitFor({ timeout: 10_000 });
const retained = events.result().items;
assert.ok(
events
.limitations()
.some((value) => value.includes("Shared-context network")),
);
assert.ok(retained.some((event) => event.url?.url === origin + "/"));
assert.ok(!JSON.stringify(retained).includes("unrelated-tab"));
} finally {
await browser.close();
}
};
const verifyTransports = async (executable, origin) => {
const scenario = scenarioFor(executable, origin, ["network"]);
const entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
const env = {
PATH: process.env.PATH ?? "/usr/bin:/bin",
REA_LOG_LEVEL: "silent",
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
};
const { stdout } = await promisify(execFile)(
process.execPath,
[
entrypoint,
"capture-browser-scenario",
JSON.stringify(scenario),
"--json",
],
{ env, timeout: 60_000, maxBuffer: 16 * 1024 * 1024 },
);
assertNetwork(
browserScenarioCaptureSchema.parse(
parseEvidence(JSON.parse(stdout)).normalized_result,
),
origin,
);
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({ name: "popup-real-e2e", version: "1" });
try {
await client.connect(transport);
const captured = await client.callTool(
{ name: "capture_browser_scenario", arguments: scenario },
{ timeout: 60_000 },
);
assert.notEqual(captured.isError, true, JSON.stringify(captured));
const evidence = parseEvidence({
...captured.structuredContent?.evidence,
normalized_result: captured.structuredContent?.result,
});
assertNetwork(
browserScenarioCaptureSchema.parse(evidence.normalized_result),
origin,
);
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
};
/** Verify popup timing, selection, coverage, and shared-context isolation on Chromium. */
export async function verifyPopupEventCoverage(executable) {
const server = createServer((request, response) => {
if (request.url === "/redirect") {
response.writeHead(302, { Location: "/popup" });
response.end();
} else {
response.setHeader("Content-Type", "text/html");
response.end(document(request.url ?? "/"));
}
});
server.listen(0, "127.0.0.1");
await once(server, "listening");
try {
const address = server.address();
assert.ok(address !== null && typeof address === "object");
const origin = `http://127.0.0.1:${address.port}`;
assertNetwork(
await capture(scenarioFor(executable, origin, ["network"])),
origin,
);
const errors = await capture(
scenarioFor(executable, origin, ["page-errors"]),
);
assert.deepEqual(
errors.events.items.map((event) => event.kind),
["page-error", "page-error"],
);
assert.ok(
errors.events.items.some((event) => event.message === "popup failure"),
);
assert.ok(
errors.events.items.some(
(event) => event.message === "nested popup failure",
),
);
for (const family of ["frames", "workers", "websockets", "downloads"]) {
const result = await capture(scenarioFor(executable, origin, [family]));
assert.equal(result.completeness.equality_eligible, false);
assert.ok(result.completeness.missing_sections.includes("events"));
assert.equal(compareWithSelf(result).overall_status, "unknown");
assert.ok(
result.limitations.some((limitation) =>
limitation.includes(`Popup ${family}`),
),
);
}
await verifySharedContext(executable, origin);
await verifyTransports(executable, origin);
return {
mocked: false,
initial_network: true,
redirects: true,
nested_popups: true,
unselected_lifecycle_absent: true,
popup_event_gaps_explicit: true,
incomplete_comparison_unknown: true,
shared_context_isolation: true,
cli: true,
stdio_mcp: true,
};
} finally {
server.closeAllConnections();
await new Promise((resolve, reject) =>
server.close((error) =>
error === undefined ? resolve() : reject(error),
),
);
}
}

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