Compare commits

...
Author SHA1 Message Date
Iwakurakaoru0822andClaude Opus 5.5 b9d4d8301f fix(javascript): keep computed-method XHR request URLs
A computed XHR method, as in `xhr.open(method, "/api")`, no longer
discarded an endpoint that main reports. Keep the URL when it is an
HTTP(S)/WebSocket URL or a path; fs flags and window targets such as
`r` and `_blank` still produce no endpoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:09:18 -07:00
Iwakurakaoru0822andClaude Opus 5.5 1be370543f fix(javascript): read open() endpoints only from XHR method/URL pairs
Static endpoint extraction treated any `.open(_, "literal")` call as
`XMLHttpRequest.open(method, url)`. In application and web bundle
analysis, `fs.open("/tmp/data.txt", "r")` became the network endpoint
`r` and `window.open(url, "_blank")` became the endpoint `_blank`.

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

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

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

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

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

---------

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* refactor(javascript): consume the investigation evidence writer

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

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

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

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

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

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

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

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

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

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

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

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

Fixes #821

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

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

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

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

---------

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

Fixes #726

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

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

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

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

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

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

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

Fixes #783

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

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

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

Refs #783

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:35:02 -07:00
morlutoandGitHub de9593ad48 refactor(browser): align inspector exclusions with canonical vocabulary (#834)
Report canonical BrowserExclusionReasons from runtime location
authorization (unsupported_url, not_approved) and collapse them
onto the fixed V8 wire bucket via inspectorExclusionKey, keeping
describeInspectorTargetLimitations text stable. Document the
provider authority split (attach-only CDP/V8 vs owned-runtime
Playwright) so the shared vocabulary is not re-split.
2026-10-07 11:34:57 +08:00
Kaoru0822 1260cacc67 fix(javascript): preserve computed Electron member uncertainty 2026-10-06 20:33:55 -07:00
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
450 changed files with 21593 additions and 4718 deletions
+8
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
@@ -274,8 +276,12 @@ jobs:
- 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 -- tests/boundary/filesystem/nibNativeHierarchy.test.ts tests/boundary/filesystem/interfaceBuilderBundle.test.ts
- name: Real binary metadata and Interface Builder integration
run: npm run verify:apple-dispatch && npm run verify:interface-builder
- name: Native plist CLI selection regressions
run: npm run test:focused -- tests/boundary/cli/nativePlistCli.test.ts
reconstruction-readiness:
name: Reconstruction readiness
@@ -368,6 +374,8 @@ jobs:
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
+11
View File
@@ -5,10 +5,21 @@ on:
paths:
- ".github/workflows/real-browser.yml"
- "scripts/verify-real-browser.mjs"
- "scripts/verify-browser-network.mjs"
- "scripts/verify-browser-scripts.mjs"
- "scripts/verify-browser-dom-destinations.mjs"
- "scripts/lib/browser-*.mjs"
- "scripts/fixtures/browser-network-site.mjs"
- "scripts/fixtures/browser-script-site.mjs"
- "scripts/fixtures/browser-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:
@@ -0,0 +1,69 @@
name: Real JavaScript recovery
on:
pull_request:
paths:
- ".github/workflows/real-javascript-recovery.yml"
- "src/javascript/recovery/**"
- "src/domain/javascript*.ts"
- "src/application/JavaScriptRecovery*.ts"
- "src/composition/javascriptRecovery.ts"
- "src/contracts/javascriptRecoveryToolContracts.ts"
- "src/cli/javascriptRecoveryCommands.ts"
- "src/cli/javascriptApplicationAnalysis.ts"
- "src/server/registerJavaScriptRecoveryTool.ts"
- "src/server/createServer.ts"
- "src/server/sessionAvailabilityPolicy.ts"
- "src/application/CapabilityInventory.ts"
- "src/cli.ts"
- "src/cliCommandNames.ts"
- "src/contracts/toolContracts.ts"
- "src/contracts/toolEffects.ts"
- "scripts/rea.mjs"
- "src/process/**"
- "src/artifacts/SafeOutputTree.ts"
- "scripts/verify-javascript-recovery.mjs"
- "scripts/lib/javascript-recovery-*.mjs"
- "scripts/fixtures/javascript-recovery/**"
- "third_party/wakaru"
- "package.json"
- "package-lock.json"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: javascript-recovery-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
HUSKY: "0"
NODE_OPTIONS: "--max-old-space-size=768"
RAYON_NUM_THREADS: "1"
jobs:
real-javascript-recovery:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- name: Fetch pinned Wakaru release into the job workspace
shell: bash
run: |
mkdir -p "$RUNNER_TEMP/rea-wakaru"
curl --fail --location --retry 3 https://github.com/pionxzh/wakaru/releases/download/v1.13.0/wakaru-linux-x64.tar.gz -o "$RUNNER_TEMP/rea-wakaru/release.tar.gz"
tar -xzf "$RUNNER_TEMP/rea-wakaru/release.tar.gz" -C "$RUNNER_TEMP/rea-wakaru" wakaru
printf '%s %s\n' e8c7ca052974604197389f6ace7464e4dd24dce64e2730971521bd6606ec9c9e "$RUNNER_TEMP/rea-wakaru/wakaru" | sha256sum --check
- name: Install isolated fixture compilers
run: npm install --prefix "$RUNNER_TEMP/rea-fixture-compilers" --ignore-scripts --no-audit --no-fund esbuild@0.25.10 webpack@5.101.3
- run: npm run build:cached
- run: npm run verify:javascript:recovery
env:
REA_WAKARU_COMMAND: ${{ runner.temp }}/rea-wakaru/wakaru
REA_JAVASCRIPT_FIXTURE_TOOLS: ${{ runner.temp }}/rea-fixture-compilers
+3
View File
@@ -10,3 +10,6 @@
[submodule "third_party/ghidra-nativeaot"]
path = third_party/ghidra-nativeaot
url = https://github.com/Washi1337/ghidra-nativeaot.git
[submodule "third_party/wakaru"]
path = third_party/wakaru
url = https://github.com/pionxzh/wakaru.git
+13 -48
View File
@@ -6,11 +6,19 @@
"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",
@@ -26,59 +34,16 @@
"no-var-requires": "off"
}
},
{
"files": [
"src/{contracts,domain,server}/**/*.ts",
"src/application/**/*.ts"
],
"excludeFiles": ["src/application/runtime.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": ["**/ghidra/**", "**/hopper/**", "**/ida/**"],
"message": "Deep-provider imports belong behind adapters or in the application composition root."
}
]
}
]
}
},
{
"files": ["tests/**/*.ts", "src/**/*.test.ts", "src/**/*.fixture.ts"],
"rules": {
"max-lines": "off",
"max-lines-per-function": [
"error",
{ "max": 150, "skipBlankLines": true, "skipComments": true }
]
}
},
{
"files": ["src/domain/**/*.{fixture,test}.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": [
"**/application/**",
"**/server/**",
"**/process/**",
"**/browser/**",
"**/ghidra/**",
"**/hopper/**",
"**/ida/**",
"**/replay/**",
"**/cli.*",
"**/main.*"
],
"message": "Domain tests may import only their owning domain and inward dependencies."
}
]
"max": 150,
"skipBlankLines": true,
"skipComments": true
}
]
}
+6
View File
@@ -91,6 +91,12 @@ Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rath
Keep tool catalogs complete and self-describing; prefer capability- and session-scoped availability over schema truncation. Serialized bytes alone do not measure agent usability or model context cost.
## Cursor Cloud specific instructions
Development requires Node.js 24.18.0 and npm 11.16.0 (`.nvmrc` and `packageManager`). The Cloud Agent image places an older `node` on `PATH` ahead of a normal install. Environment setup installs the pinned toolchain under `/usr/local` and prepends `/usr/local/bin` for login shells. Confirm `node -v` is `v24.18.0` before installing dependencies.
`npm ci` installs locked dependencies. `npm run build:cached` produces the CLI and MCP server. `npm run check:fast` is the pre-push typecheck and lint. Hopper, Ghidra, and IDA are optional bring-your-own providers. JavaScript analysis and the deterministic Vitest suites do not need them. `rea doctor` reports those engines as missing until they are configured.
## Commit & Pull Request Guidelines
Use Conventional Commit subjects because Release Please derives versions and changelogs from them. Examples: `feat: add historical source import`, `fix(process): stop timers after exit`, and `docs: update architecture`. Use `!` or a `BREAKING CHANGE:` footer for breaking changes. Pull request titles must follow the same format because squash merges use the title as the release commit. Pull requests should describe contract or behavior changes, list verification commands, link issues, and include sanitized MCP examples when schemas change. State whether real Hopper/Ghidra verification was performed. Never commit binaries, Hopper or Ghidra project documents, credentials, `dist/`, `node_modules/`, or local planning artifacts (e.g. `.codex/`).
+5 -1
View File
@@ -2,11 +2,15 @@
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
+144 -80
View File
@@ -51,49 +51,6 @@ REA connects your agent to tools for inspecting native binaries, JavaScript and
Setup registers REA with your agent and installs matching workflow instructions. Native analysis can use an existing Hopper or Ghidra installation; setup can optionally install Hopper with approval. Static JavaScript analysis needs neither engine.
## Just ask your agent
After [setup](#quick-start), restart your agent and ask:
```text
Understand how search works in the Notes app, show me the evidence, and build a
similar feature for my project.
```
Replace Notes with the app you want to understand, or ask for an overview first.
## The investigation model
<table>
<tr>
<td width="33%" valign="top">
<strong>Decompile</strong><br /><br />
Open an app and recover readable code, strings, names, and other clues about how it works.
</td>
<td width="33%" valign="top">
<strong>Understand</strong><br /><br />
Follow the code from one part of the app to another until the agent can explain how a feature actually works.
</td>
<td width="33%" valign="top">
<strong>Recreate</strong><br /><br />
Turn what the agent learned into a feature for your own product, adapted to your stack, interface, and requirements.
</td>
</tr>
</table>
REA shows how it reached its conclusions. It does not claim to recover original source code or automatically clone an application.
## Why REA
| | |
| ------------------------ | ----------------------------------------------------------------------------------------------------- |
| **Built for agents** | Ask what an app does and let your agent inspect it instead of guessing. |
| **CLI and MCP** | Run the same reverse-engineering capabilities from your terminal or agent. |
| **Guided setup** | Configure your agent, connect an existing analysis tool, or install Hopper with your approval. |
| **From insight to code** | Understand a feature, then build your own version in the same coding session. |
| **Local by design** | Analysis runs on your supported local host. REA does not upload the app to a hosted analysis service. |
| **Keeps context** | Investigate several apps without starting over for every question. |
## Quick start
### Run setup (recommended)
@@ -108,25 +65,21 @@ Choose which supported agents should use REA, then review the exact paths and ch
Setup shows its changes before applying them and backs up existing configuration. See [Installation and setup](docs/installation.md) for requirements and setup options.
### With an agent (recommended)
### AI Coding Assistants (optional)
After setup, restart your agent and [describe the app or feature](#just-ask-your-agent) you want to understand. Hopper can run in demo mode; if it shows a first-run prompt, choose the demo or enter an existing license.
REA supports Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, Command Code, and VS Code. Existing REA registrations are selected by default during setup; other detected agents remain unselected until chosen. Other agents can use the [manual MCP configuration](#manual-mcp-configuration).
### Install the skill separately
Add the skill to your AI coding assistant for richer context:
```bash
npx skills add morluto/rea --skill reverse-engineer-anything
```
This installs agent instructions, not MCP registration or analysis engines.
Follow the skill's [conditional connection guide](skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed):
prepare a scoped `npx rea-agents setup` plan if registration is missing, review
and approve its actual changes, then restart/reconnect and verify the tools.
Guided setup above installs a version-matched skill by default. The skills.sh
route uses repository instructions, which can be ahead of the released server;
see [released package and main](docs/installation.md#released-package-and-main).
The skill provides REA's investigation workflow. Run setup above to connect REA to your agent and configure analysis tools. Setup already installs a version-matched skill by default; this command installs the repository version.
### With an agent (recommended)
After setup, restart your agent and [describe the app or feature](#just-ask-your-agent) you want to understand. Hopper can run in demo mode; if it shows a first-run prompt, choose the demo or enter an existing license.
REA supports Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, Command Code, and VS Code. Existing REA registrations are selected by default during setup; other detected agents remain unselected until chosen. Other agents can use the [manual MCP configuration](#manual-mcp-configuration).
### First result from the terminal
@@ -142,6 +95,40 @@ MCP setup, Hopper, Ghidra, or executing the application. For a native app, confi
its engine first, then use `analyze` with that app's path. Run `doctor` when you
need diagnosis; it is not a prerequisite for each analysis.
### Check readiness for the task at hand
`rea doctor` without options is an audit of the whole integration. It checks
every detected agent registration, the installed skill, and every optional
analysis engine, so it can report `healthy: false` while your current task works.
Choose a readiness scope for the work you are doing:
| Task | Readiness check |
| ----------------------------------- | -------------------------------------------------------------------------------------------- |
| Static JavaScript/Electron analysis | None. Run `analyze-javascript-application` directly. |
| Troubleshoot one analysis engine | `rea doctor --provider ghidra --json` (or `hopper`, `ida`) |
| Check one agent's MCP registration | `rea doctor --client codex --json` (see [client IDs](docs/installation.md#supported-agents)) |
| Check the installed skill | `rea doctor --skill --json` |
A scoped report has `scope.mode: "explicit"`. Only `scope_checks` determine
`healthy` and the exit status. Everything else is listed in
`informational_checks`; you don't need to fix it for this task. For example, a
missing engine you are not using needs no repair. `environment_healthy` still
summarizes the full audit. `--target` adds a target check, but without a scope
option the report remains audit-wide.
When more than one installed engine supports a native target, REA does not pick
one: opening the target fails with `code: "capability_unavailable"`,
`details.selection_reason: "ambiguous"`, and the providers in
`details.candidate_ids`. Choose one once: pass `--provider` on
the CLI or `provider_id` on `open_binary`, or set `REA_ANALYSIS_PROVIDER` as a
standing preference. An explicit selector overrides the environment variable.
The session keeps that choice and never falls back to another engine.
Recovery depends on `details.selection_reason`. For `ambiguous`, choose one of
the candidates. For `provider_unavailable`, the engine you selected needs repair,
so run `rea doctor --provider <id> --json` and follow its remediation. Neither
reason means you have to install every engine. See
[Choosing a deep-analysis provider](#choosing-a-deep-analysis-provider).
### Install the rea command
Install the command-line interface:
@@ -176,9 +163,9 @@ Deep native binary analysis requires [Hopper](https://www.hopperapp.com/), [Ghid
Firmware region inspection and explicit extraction use caller-supplied Binwalk and Unblob on Linux. See [Firmware analysis](docs/firmware-analysis.md) for setup, provenance, resource limits and native handoff.
Static APK analysis uses a separately supplied headless JADX JAR and Java, with no emulator or APK execution. See [Android analysis](docs/android-analysis.md) for setup, CLI/MCP operations, coverage and public test fixtures.
Static APK analysis uses a separately supplied headless JADX JAR and Java, with no emulator or APK execution. See [Android analysis](docs/android-analysis.md) for setup, CLI/MCP operations, coverage and public test fixtures. Authenticated IPA inventory Evidence can be projected with [Apple application analysis](docs/apple-application-analysis.md).
Repository main includes experimental Windows x64 Ghidra support for native x86-64 PE applications on local NTFS, with bundled Job Object, private-DACL, and path-admission controls. Check the [release boundary](docs/installation.md#released-package-and-main) before expecting this from an npm package. See [Windows Ghidra P0](docs/windows-ghidra-p0.md) for prerequisites and verified scope.
Repository main and npm 4.1.0 include experimental Windows x64 Ghidra support for native x86-64 PE applications on local NTFS, with bundled Job Object, private-DACL, and path-admission controls. Check the [release boundary](docs/installation.md#released-package-and-main) before expecting this from an older npm package. See [Windows Ghidra P0](docs/windows-ghidra-p0.md) for prerequisites and verified scope.
If something is not working, run:
@@ -213,7 +200,7 @@ REA defaults `HOPPER_LAUNCHER_PATH` to `/Applications/Hopper Disassembler.app/Co
### Ghidra analysis provider
Already use Ghidra? REA can connect it to your agent on Linux x64 or macOS x64/arm64. It requires **Ghidra 12.1.4** and a **64-bit JDK 21**. On macOS, your Ghidra installation must also include the native decompiler for your architecture.
Already use Ghidra? REA can connect it to your agent on Linux x64 or macOS x64/arm64. It accepts **Ghidra 12.1.x** and the **64-bit full JDK** that installation declares (`application.java.min` through `application.java.max`). Current 12.1 releases require JDK 21 or newer and set no maximum. The bridge is verified with Ghidra 12.1.4 and JDK 21. On macOS, your Ghidra installation must also include the native decompiler for your architecture.
Set the installation paths, then run setup:
@@ -297,6 +284,11 @@ Import an older source tree as a reference. REA keeps it separate from observati
rea import-reference-source /absolute/path/to/source
```
Historical-source import requires safe no-follow file opens and currently runs
on Linux and macOS. Native Windows reports `unsupported_host`; run the import
with Linux REA in WSL or on another supported host. Changing permissions or
reinstalling REA does not enable this Windows workflow.
Imports read the path supplied to the command. File names do not cause automatic omissions; files are represented by hashes and metadata. To exclude selected paths, set `REA_REFERENCE_SECRET_PATTERNS_JSON` to a JSON string array of ignore patterns. Exports never replace an existing file unless `--overwrite` is explicit.
Use a snapshot to save successful analysis results and reuse them on later runs. REA reuses a result only when the target bytes, operation, parameters, analysis tool, and settings match. It does not cache changes or cursor-dependent calls. Snapshot files stay local and use owner-only permissions.
@@ -314,6 +306,49 @@ result is returned. Pass `snapshot_path` and, when required, `overwrite: true` t
`close_binary` to save atomically before Hopper resources are released. If the
save fails, REA deliberately leaves the session open.
## Just ask your agent
After [setup](#quick-start), restart your agent and ask:
```text
Understand how search works in the Notes app, show me the evidence, and build a
similar feature for my project.
```
Replace Notes with the app you want to understand, or ask for an overview first.
## The investigation model
<table>
<tr>
<td width="33%" valign="top">
<strong>Decompile</strong><br /><br />
Open an app and recover readable code, strings, names, and other clues about how it works.
</td>
<td width="33%" valign="top">
<strong>Understand</strong><br /><br />
Follow the code from one part of the app to another until the agent can explain how a feature actually works.
</td>
<td width="33%" valign="top">
<strong>Recreate</strong><br /><br />
Turn what the agent learned into a feature for your own product, adapted to your stack, interface, and requirements.
</td>
</tr>
</table>
REA shows how it reached its conclusions. It does not claim to recover original source code or automatically clone an application.
## Why REA
| | |
| ------------------------ | ----------------------------------------------------------------------------------------------------- |
| **Built for agents** | Ask what an app does and let your agent inspect it instead of guessing. |
| **CLI and MCP** | Run the same reverse-engineering capabilities from your terminal or agent. |
| **Guided setup** | Configure your agent, connect an existing analysis tool, or install Hopper with your approval. |
| **From insight to code** | Understand a feature, then build your own version in the same coding session. |
| **Local by design** | Analysis runs on your supported local host. REA does not upload the app to a hosted analysis service. |
| **Keeps context** | Investigate several apps without starting over for every question. |
## One prompt, a full investigation
```text
@@ -349,20 +384,20 @@ See [native investigation](docs/native-investigation.md) for keyed archives, ins
## Tool catalog for investigation
| Tool family | Count | Examples |
| ------------------------- | ----: | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Native inspection | 41 | functions, pseudocode, assembly, strings, symbols, calls, references, annotations, byte reads, and file offsets |
| Investigation workflows | 14 | app overviews, function dossiers, native APIs and dispatch, batch decompilation, feature traces, call paths, call graphs, Swift and Objective-C discovery |
| Native macOS utilities | 7 | Mach-O metadata, code signatures, plists, architectures, and Swift demangling without launching Hopper |
| Artifact graph | 5 | directory and package inventories, compiled Interface Builder files, Apple asset catalogs, and extraction |
| Managed PE/CLI | 7 | .NET identity, metadata, CIL instructions, native dependencies, reconstruction imports, and build comparisons |
| Firmware | 2 | Linux firmware region inspection and explicit extraction |
| Android APK | 5 | package and manifest declarations, class search, member inventories, method decompilation, and incoming static references |
| Browser observation | 9 | page structure, network metadata, scripts, source maps, WebMCP discovery, screenshots, and capture comparisons |
| Electron analysis | 5 | renderer observation, static app mapping, and static/runtime reconciliation |
| JavaScript runtime | 2 | Node/Electron Inspector target discovery, script locations, and execution-context events |
| Application workflows | 7 | cross-layer feature traces, build comparisons, historical source mapping, static return-shape comparison, and reconstruction checks |
| Workspace and observation | 21 | sessions, evidence bundles, navigation context, process/artifact/function comparisons, and open-question tracking |
| Tool family | Count | Examples |
| ------------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Native inspection | 41 | functions, pseudocode, assembly, strings, symbols, calls, references, annotations, byte reads, and file offsets |
| Investigation workflows | 14 | app overviews, function dossiers, native APIs and dispatch, batch decompilation, feature traces, call paths, call graphs, Swift and Objective-C discovery |
| Native macOS utilities | 7 | Mach-O metadata, code signatures, plists, architectures, and Swift demangling without launching Hopper |
| Artifact graph | 5 | directory and package inventories, compiled Interface Builder files, Apple asset catalogs, and extraction |
| Managed PE/CLI | 7 | .NET identity, metadata, CIL instructions, native dependencies, reconstruction imports, and build comparisons |
| Firmware | 2 | Linux firmware region inspection and explicit extraction |
| Android APK | 5 | package and manifest declarations, class search, member inventories, method decompilation, and incoming static references |
| Browser observation | 9 | page structure, network metadata, scripts, source maps, WebMCP discovery, screenshots, and capture comparisons |
| Electron analysis | 5 | renderer observation, static app mapping, and static/runtime reconciliation |
| JavaScript runtime | 2 | Node/Electron Inspector target discovery, script locations, and execution-context events |
| Application workflows | 11 | captured website script export; Android/Apple inventory projections; cross-layer feature traces, build comparisons, historical source mapping, static return-shape comparison, and reconstruction checks |
| Workspace and observation | 21 | sessions, evidence bundles, navigation context, process/artifact/function comparisons, and open-question tracking |
The public interface describes what the agent is trying to learn. Providers decide how to answer. macOS utilities handle common semantic inspection without launching Hopper; Hopper handles deeper native analysis; the process harness records direct behavioral captures.
@@ -376,6 +411,18 @@ Static Android APK inspection is verified on Linux with headless JADX; see [Andr
- **Packages and resources:** Inspect directories, ZIP, APK, IPA, ASAR, plists, compiled Interface Builder files, and Apple asset catalogs. Artifact requests name the input and requested extraction or traversal directly; macOS DMG traversal also requires the host's native mounting support.
- **JavaScript and Electron:** Map modules, imports, source maps, routes, IPC channels, storage, and native add-ons without running the app. Compare builds and trace a feature across the recovered graph. Dynamic and ambiguous relationships remain unresolved. See [JavaScript application workflows](docs/javascript-application-workflows.md).
- **Websites:** Inspect a selected page in an existing Chrome-family browser. Capture page structure, network metadata, script evidence, and screenshots requested by the call. Passive observation does not navigate or execute page JavaScript. See [browser observation](docs/browser-observation.md).
Export retained scripts with `export_web_scripts` / `rea export-web-scripts`
into a verified local directory, then use the existing JavaScript analysis
and tracing tools. Source URLs, frame or transaction references, competing
versions, and missing-source states remain inline. See
[captured website scripts](docs/website-script-export.md).
Recover readable modules from selected local bundles with
`recover_javascript_sources` / `rea recover-javascript-sources`, then pass
the returned `analysis_input` to static analysis. This optional Linux x64
adapter requires caller-supplied Wakaru 1.13.0; see
[JavaScript source recovery](docs/javascript-recovery.md).
- **Electron and Node runtime observation:** Inspect selected Electron pages or attach to a Node/Electron V8 Inspector target. Inspector observation records script locations and execution-context events; it does not infer imports, IPC activity, or which modules executed. See [runtime observation](docs/javascript-runtime-observation.md).
- **.NET assemblies:** Inspect metadata and CIL instructions, compare builds, and check declared native dependencies without loading or running the assembly. Imported decompiler output is labeled as analyst inference. See [managed-code analysis](docs/managed-code-analysis.md).
- **Controlled behavior capture:** Run process, browser, or Electron scenarios with the target, actions, and lifecycle declared in each request, then compare the resulting evidence. Missing observations cannot establish that two runs behaved the same way.
@@ -457,7 +504,7 @@ The [current status](#current-status) section describes shipped capabilities. Th
1. **Expand browser and Electron interaction:** add scenario actions beyond the current click and wait operations.
2. **Observe native apps at runtime:** explore LLDB, Frida, system logs, and native API tracing.
3. **Evaluate more tools and targets:** assess IDA/Hex-Rays, Binary Ninja, Rizin, LIEF, Windows-native tools, mobile apps, and firmware.
3. **Evaluate more tools and targets:** assess Binary Ninja, Rizin, LIEF, Windows-native tools, mobile apps, and firmware. IDA support already ships via the bring-your-own upstream adapter; see the [IDA provider guide](docs/ida-provider.md).
Setup already lets you choose agent integration and Hopper installation. Support for installing additional analysis tools is future work, described in the [installation roadmap](docs/roadmap.md).
@@ -630,9 +677,9 @@ Process Capture records behavior and is not a security sandbox.
Capture a scenario or compare two saved Process Capture Evidence records:
```bash
rea capture-process ./scenario.json > authority.json
rea capture-process ./reconstruction.json > reconstruction.json
rea compare-process-captures authority.json reconstruction.json
rea capture-process ./authority-scenario.json --json > authority-capture.json
rea capture-process ./reconstruction-scenario.json --json > reconstruction-capture.json
rea compare-process-captures authority-capture.json reconstruction-capture.json --json
```
The comparison reports each observed dimension separately and identifies the
@@ -640,9 +687,16 @@ first terminal, interaction, exit, filesystem, or process divergence.
See [Process Capture](docs/process-capture.md) for scenario fields, limits, and
evidence boundaries.
REA installs a prebuilt PTY backend for supported macOS, Linux, and Windows
architectures. If the capability check reports that the backend is unavailable,
reinstall REA for the current platform and architecture.
Process Capture currently runs on Linux and macOS with a working native PTY
backend. Native Windows capture remains unavailable until the PTY adapter can
verify descendant cleanup; installing or reinstalling its Windows PTY binary
does not enable capture. For Linux commands, run Linux REA inside WSL.
Comparing existing capture Evidence remains available on Windows.
On supported capture hosts, a missing or incompatible PTY binary can be fixed
by reinstalling REA for the current platform, architecture, and Node.js version
with optional dependencies enabled. Use separate scenario and output files;
`--json` produces the JSON consumed by the comparison command.
ASAR inventory verifies Electron integrity metadata for both archive entries
and `.asar.unpacked` companion files. Integrity failures identify the logical
@@ -727,3 +781,13 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and contribution ch
## License
[MIT](LICENSE)
## Star history
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date&amp;theme=dark&amp;legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date" />
<img alt="REA GitHub star history" src="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date" />
</picture>
</a>
+42 -55
View File
@@ -2,9 +2,9 @@
[English](README.md) · [简体中文](README_zh.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · **العربية**
# REA: هندسة أي شيء عكسيًا
# REA: اعكس هندسة أي شيء
### استقصِ سلوك التطبيقات والملفات التنفيذية الأصلية باستخدام وكيلك.
### وسيط MCP واحد للهندسة العكسية عبر الملفات التنفيذية والتطبيقات وسلوك وقت التشغيل.
**اعثر على ميزة تعجبك. افهم آلية عملها. ابنها بالطريقة التي تريدها.**
@@ -15,7 +15,7 @@
[![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=trendshift-badge&amp;utm_medium=badge&amp;utm_campaign=badge-trendshift-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/82054/weekly?language=TypeScript" alt="morluto/rea | Trendshift" width="250" height="55" /></a>
<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>
[البدء السريع](#البدء-السريع) · [حالة الدعم](#حالة-الدعم) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [منصة أدوات التحقيق](#منصة-أدوات-التحقيق) · [خطة العمل](#خطة-العمل) · [كيف يعمل؟](#كيف-يعمل)
@@ -47,7 +47,7 @@
يوفر REA أدوات لتحليل الملفات التنفيذية الأصلية وتطبيقات JavaScript وElectron وتجميعات .NET والمواقع. يمكنك استخدامها من وكيلك أو من الطرفية. يعمل التحليل محليًا، وتتضمن النتائج الأدلة والقيود.
يضبط Setup إعدادات الوكيل ويربطه بتثبيت موجود من Hopper أو Ghidra. ويمكنه تثبيت Hopper بعد موافقتك.
يضبط Setup إعدادات الوكيل ويربطه بتثبيت موجود من Hopper أو Ghidra، ويثبّت سير عمل REA الموجّه. ويمكنه تثبيت Hopper بعد موافقتك. ولا يحتاج التحليل الثابت لتطبيقات JavaScript إلى أي محرك.
## اطلب من وكيلك مباشرة
@@ -69,14 +69,14 @@
## لماذا REA؟
| | |
| -------------------- | ------------------------------------------------------------------------------------- |
| **مصمم للوكلاء** | اسأل عما يفعله تطبيق مترجم ودع الوكيل يجمع الأدلة بدلًا من التخمين. |
| **CLI وMCP** | استخدم قدرات الهندسة العكسية نفسها من الطرفية أو وكيل البرمجة. |
| **يتولى التعقيد** | يدير REA إعداد الأدوات وفتح التطبيق واستمرار الاستقصاء والتنظيف بعد الانتهاء. |
| **سير عمل كامل** | انتقل من النظرة الأولى إلى الشيفرة شبه المصدرية وعلاقات الاستدعاء والأنواع وأدلة التنفيذ. |
| **محلي حسب التصميم** | يجري التحليل على جهازك المدعوم ولا يرفع REA الملف التنفيذي إلى خدمة تحليل مستضافة. |
| **يحافظ على السياق** | استقصِ عدة ملفات تنفيذية من دون بدء التحليل من جديد عند كل سؤال. |
| | |
| ------------------------ | ---------------------------------------------------------------------------- |
| **مصمم للوكلاء** | اسأل عما يفعله التطبيق ودع الوكيل يفحصه بدلًا من التخمين. |
| **CLI وMCP** | استخدم قدرات الهندسة العكسية نفسها من الطرفية أو الوكيل. |
| **إعداد موجّه** | اضبط وكيلك، واربط أداة تحليل موجودة، أو ثبّت Hopper بموافقتك. |
| **من الفهم إلى الشيفرة** | افهم الميزة ثم ابنِ نسختك الخاصة في جلسة البرمجة نفسها. |
| **محلي حسب التصميم** | يجري التحليل على مضيفك المحلي المدعوم، ولا يرفع REA التطبيق إلى خدمة مستضافة. |
| **يحافظ على السياق** | استقصِ عدة تطبيقات دون بدء التحليل من جديد عند كل سؤال. |
## البدء السريع
@@ -98,28 +98,23 @@ npx rea-agents setup
يمكن تشغيل Hopper في الوضع التجريبي. إذا ظهرت رسالة عند التشغيل الأول، فاختر التجربة أو أدخل ترخيصًا موجودًا.
### Skill instructions and a first CLI result
### مهارة مساعد البرمجة (اختياري)
أضف المهارة إلى مساعد البرمجة للحصول على سياق أغنى:
```bash
npx skills add morluto/rea --skill reverse-engineer-anything
```
This installs instructions, not MCP registration or analysis engines. Follow the
[conditional connection guide](skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed).
`npx rea-agents setup` installs a matching skill and registers selected clients
only after showing the plan and receiving approval. Restart/reconnect after
registration, then verify REA tools in the active session. Repository skills can
be ahead of the [released package](docs/installation.md#released-package-and-main).
توفر المهارة سير عمل التحقيق في REA. نفّذ الإعداد أعلاه لربط REA بالوكيل وضبط أدوات التحليل. يثبّت الإعداد افتراضيًا مهارة مطابقة للإصدار؛ وهذا الأمر يثبّت نسخة المستودع.
For your extracted JavaScript/Electron tree or ASAR, no MCP setup or native
engine is required:
لشجرة JavaScript/Electron المستخرجة أو ملف ASAR، لا يلزم إعداد MCP ولا محرك أصلي:
```bash
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
Replace the path with your target; Windows paths such as `"D:/apps/example"`
are supported by this static workflow. Native analysis requires its own engine.
استبدل المسار بهدفك (مثل `"D:/apps/example"` على Windows). يدعم سير العمل الثابت هذه المسارات، بينما يتطلب التحليل الأصلي محركه الخاص.
### تثبيت أمر rea
@@ -146,11 +141,11 @@ rea setup
يتطلب تحليل الملفات التنفيذية الأصلية Hopper أو Ghidra. Hopper برنامج منفصل، وللوضع التجريبي قيود يحددها المورّد، لكن الترخيص المدفوع ليس إلزاميًا.
يدعم Ghidra نظامي Linux x64 وmacOS x64/arm64. ثبت Ghidra 12.1.4 وJDK 21 الكامل بنواة 64 بت بصورة منفصلة، ثم اضبط REA لاستخدامهما. يحتاج macOS أيضًا إلى أداة فك الترجمة الأصلية المطابقة لمعمارية الجهاز.
يدعم Ghidra نظامي Linux x64 وmacOS x64/arm64. ثبت Ghidra 12.1.x وJDK الكامل بنواة 64 بت الذي يعلنه ذلك التثبيت (`application.java.min` حتى `application.java.max`) بصورة منفصلة، ثم اضبط REA لاستخدامهما. تتطلب إصدارات 12.1 الحالية JDK 21 أو أحدث ولا تحدد حدًا أعلى. جرى التحقق من الجسر مع Ghidra 12.1.4 وJDK 21. يحتاج macOS أيضًا إلى أداة فك الترجمة الأصلية المطابقة لمعمارية الجهاز.
يستطيع Setup التحقق من التثبيت وحفظ المسارات؛ ولا يثبت أو يحدّث Ghidra أو Java أو Node.js أو npm أو Homebrew.
Repository main includes experimental Windows x64 Ghidra P0 for native, non-managed, non-DLL x86-64 PE applications on fixed local NTFS. The native controls are implemented; npm 4.0.1 does not contain them. See the [release boundary](docs/installation.md#released-package-and-main) and [Windows Ghidra P0](docs/windows-ghidra-p0.md).
يتضمن فرع 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) للمتطلبات والنطاق الموثق.
### تشخيص المشكلات
@@ -166,21 +161,12 @@ Repository main includes experimental Windows x64 Ghidra P0 for native, non-mana
## حالة الدعم
The current repository capabilities and platform requirements are described in
[the English support guide](README.md#current-status). Main can be ahead of the
[npm release](docs/installation.md#released-package-and-main).
تُوصف قدرات المستودع الحالية ومتطلبات الأنظمة في [دليل الدعم الإنجليزي](README.md#current-status). قد يسبق فرع main [إصدار npm](docs/installation.md#released-package-and-main).
- Ghidra provides 25 read-only operations on Linux x64, macOS x64/arm64, and the
experimental Windows x64 P0 boundary. Linux/macOS additionally supports atomic
session function annotations. Windows P0 is read-only; Ghidra has no GUI controls.
- Static Android inspection has separate bring-your-own JADX/Java prerequisites;
real-provider verification covers Linux. See [Android analysis](docs/android-analysis.md).
- Browser, Electron, and process requests specify their target, actions, and
lifecycle directly. They require the host's actual access, without separate
REA permission grants. Setup configuration writes and Hopper installation
still require approval of their exact plan.
- `rea capabilities` describes binary-session operations rather than every
application/runtime tool. Consult the full MCP tool list and relevant guide.
- يوفر 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 الكاملة والدليل ذا الصلة.
## استقصاء كامل بطلب واحد
@@ -225,20 +211,20 @@ The current repository capabilities and platform requirements are described in
## منصة أدوات التحقيق
| عائلة الأدوات | العدد | الاستخدام |
| --------------------- | ----: | -------------------------------------------------------------------------------------- |
| فحص الملفات التنفيذية | 41 | الدوال والشيفرة شبه المصدرية والتعليمات والسلاسل والرموز والمراجع والتعليقات |
| التحليل المركب | 14 | النظرة العامة وتحليل الدوال وفك الترجمة الدفعي ومخطط الاستدعاء وفحص Swift وObjC |
| أدوات macOS الأصلية | 7 | بيانات Mach-O والتوقيعات وplist والمعماريات واستعادة أسماء Swift |
| الملفات والحزم | 5 | فحص الدلائل والحزم وInterface Builder وموارد Apple والاستخراج |
| .NET PE/CLI | 7 | هوية التجميع والبيانات الوصفية وCIL والاستدعاءات الأصلية والمقارنة واستيراد إعادة البناء |
| البرامج الثابتة | 2 | فحص مناطق البرامج الثابتة واستخراجها الصريح على Linux |
| Android APK | 5 | بيانات الحزمة وmanifest والبحث عن الأصناف والأعضاء وفك ترجمة الأساليب والمراجع الثابتة |
| مراقبة المتصفح | 9 | الصفحات والبرامج النصية وخرائط المصدر وWebMCP والصور ومقارنة الالتقاط |
| تحليل Electron | 5 | مراقبة الصفحات وبنية التطبيق وربط النتائج الثابتة بنتائج التشغيل |
| وقت تشغيل JavaScript | 2 | الاتصال بهدف Node/Electron Inspector موجود لمراقبة البرامج وسياقات التنفيذ |
| سير عمل التطبيقات | 7 | تتبع الميزات ومقارنة الإصدارات وبنية قيم الإرجاع والتحقق من إعادة الإنشاء |
| جلسة الملف التنفيذي | 21 | تبديل الأهداف وحفظ الأدلة ومقارنة العمليات والدوال وتسجيل الأسئلة غير المحسومة |
| عائلة الأدوات | العدد | الاستخدام |
| -------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| الفحص الأصلي | 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 ومواضع البرامج وأحداث سياقات التنفيذ |
| سير عمل التطبيقات | 11 | تصدير النصوص البرمجية الملتقطة، تتبع الميزات عبر الطبقات ومقارنة الإصدارات ومواءمة المصدر التاريخي ومقارنة أشكال القيم المعادة والتحقق من إعادة الإنشاء |
| مساحة العمل والملاحظة | 21 | الجلسات وحزم الأدلة وسياق التنقل ومقارنة العمليات والمخرجات والدوال وتسجيل الأسئلة غير المحسومة |
## خطة العمل
@@ -299,8 +285,9 @@ npx -y rea-agents@latest compare /absolute/path/to/left-evidence.json /absolute/
## الأمان والخصوصية
- يبقى تحليل الملف التنفيذي محليًا بين REA ومزوّد التحليل المختار.
- يبقى التحليل محليًا بين REA وموفر التحليل المختار عبر مقابس محلية خاصة، ومع IDA عبر تسجيل MCP المحلي المضبوط.
- لا يرفع REA الأهداف إلى خدمة مستضافة.
- تعمل أدوات التحليل والأهداف المطلقة بصلاحيات المستخدم نفسه؛ وما زال التقاط واجهة النظام على macOS يعتمد على الوصول إلى تسهيلات الاستخدام وتسجيل الشاشة. ولا ينفّذ التحليل الثابت للـ JavaScript الوحدات المستخرجة.
- تستخدم جلسات Ghidra مشروعًا مؤقتًا معزولًا ولا تفتح مشاريع Ghidra التي يملكها المستخدم أو تعدّلها.
- راجع الملفات التنفيذية غير الموثوقة واعزلها كما تفعل مع أي مدخل أصلي قد يكون ضارًا.
- أبلغ عن الثغرات وفق [سياسة الأمان](SECURITY.md)، وليس عبر قضية عامة.
@@ -324,7 +311,7 @@ npx -y rea-agents@latest compare /absolute/path/to/left-evidence.json /absolute/
<details>
<summary><strong>هل يعمل REA على Linux أو Windows؟</strong></summary>
يدعم REA نظام macOS 12+ وUbuntu 24.04+ وFedora 41+ وArch Linux بنواة 64 بت. يعمل تحليل Ghidra للقراءة فقط على Linux x64 وmacOS x64/arm64 مع Ghidra 12.1.4 وJDK 21 الكامل. تحليل Ghidra على Windows غير متاح بسبب الضوابط الأصلية غير المنفذة؛ راجع [Windows Ghidra P0](docs/windows-ghidra-p0.md).
يدعم REA نظام macOS 12+ وUbuntu 24.04+ وFedora 41+ وArch Linux بنواة 64 بت. يعمل تحليل Ghidra للقراءة فقط على Linux x64 وmacOS x64/arm64 مع Ghidra 12.1.x وJDK الكامل الذي يعلنه التثبيت (JDK 21 أو أحدث دون حد أعلى في إصدارات 12.1 الحالية)، إضافة إلى حد Windows x64 P0 التجريبي لتطبيقات PE الأصلية x86-64 على NTFS محلي؛ راجع [دليل Windows Ghidra P0](docs/windows-ghidra-p0.md) للنطاق الموثق.
</details>
+36 -50
View File
@@ -15,7 +15,7 @@
[![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=trendshift-badge&amp;utm_medium=badge&amp;utm_campaign=badge-trendshift-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/82054/weekly?language=TypeScript" alt="morluto/rea | Trendshift" width="250" height="55" /></a>
<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>
[クイックスタート](#クイックスタート) · [現在の対応状況](#現在の対応状況) · [バイナリから動作へ](#バイナリから動作へ) · [調査ツールカタログ](#調査ツールカタログ) · [ロードマップ](#ロードマップ) · [仕組み](#仕組み)
@@ -69,14 +69,14 @@ REA は調査をバイナリ上の根拠に結び付けます。元のソース
## REA を選ぶ理由
| | |
| ---------------------- | ---------------------------------------------------------------------------------------------------- |
| **エージェント向け** | コンパイル済みアプリについて質問し、推測ではなく根拠を集めさせることができます。 |
| **CLI と MCP** | ターミナルとコーディングエージェントから同じリバースエンジニアリング機能を使えます。 |
| **複雑さを処理** | ツール設定、アプリの読み込み、調査の維持、終了後のクリーンアップを REA が担います。 |
| **一連の調査に対応** | 最初の概要から疑似コード、呼び出し関係、型、実装の手掛かりまで掘り下げられます。 |
| **ローカルで解析** | 解析は対応するローカルホストで実行され、REA がバイナリをホスト型解析サービスへ送ることはありません。 |
| **コンテキストを維持** | 質問ごとに解析を最初からやり直さず、複数のバイナリを続けて調査できます。 |
| | |
| -------------------------- | ---------------------------------------------------------------------------------------------------- |
| **エージェント向け** | アプリの動作について質問し、推測ではなくエージェントに調査させることができます。 |
| **CLI と MCP** | ターミナルとエージェントから同じリバースエンジニアリング機能を使えます。 |
| **ガイド付きセットアップ** | エージェントの設定、既存の解析ツールへの接続、承認後の Hopper インストールに対応します。 |
| **洞察からコードへ** | 機能を理解したら、同じコーディングセッションで自分のプロダクト向けに実装できます。 |
| **ローカルで解析** | 解析は対応するローカルホストで実行され、REA がバイナリをホスト型解析サービスへ送ることはありません。 |
| **コンテキストを維持** | 質問ごとに解析を最初からやり直さず、複数のアプリを続けて調査できます。 |
## クイックスタート
@@ -98,28 +98,23 @@ Setup は最初に連携するエージェントを複数選択できるよう
Hopper はデモモードで使えます。初回起動の画面ではデモを選択するか、既存のライセンスを入力してください。
### Skill instructions and a first CLI result
### AI コーディングアシスタント向けスキル(任意)
AI コーディングアシスタントにスキルを追加すると、より豊富なコンテキストが得られます:
```bash
npx skills add morluto/rea --skill reverse-engineer-anything
```
This installs instructions, not MCP registration or analysis engines. Follow the
[conditional connection guide](skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed).
`npx rea-agents setup` installs a matching skill and registers selected clients
only after showing the plan and receiving approval. Restart/reconnect after
registration, then verify REA tools in the active session. Repository skills can
be ahead of the [released package](docs/installation.md#released-package-and-main).
このスキルは REA の調査ワークフローを提供します。上記の setup を実行して REA をエージェントに接続し、解析ツールを設定してください。Setup は既定でバージョンに合ったスキルをインストールします。このコマンドではリポジトリ版のスキルをインストールします。
For your extracted JavaScript/Electron tree or ASAR, no MCP setup or native
engine is required:
展開済みの JavaScript/Electron アプリのツリーや ASAR であれば、MCP の設定もネイティブエンジンも不要です:
```bash
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
Replace the path with your target; Windows paths such as `"D:/apps/example"`
are supported by this static workflow. Native analysis requires its own engine.
パスを調査対象に置き換えてください(Windows では `"D:/apps/example"` のようなパス)。この静的ワークフローではそのようなパスも扱えます。ネイティブ解析には専用のエンジン設定が必要です。
### rea コマンドをインストール
@@ -146,11 +141,11 @@ rea setup
ネイティブバイナリ解析には Hopper または Ghidra が必要です。Hopper は別製品です。デモにはベンダー所定の制限がありますが、有料ライセンスは必須ではありません。
Ghidra は Linux x64 と macOS x64/arm64 に対応します。Ghidra 12.1.4 と完全な 64 ビット JDK 21 を別途インストールし、REA で使うように設定してください。macOS ではホストのアーキテクチャに合うネイティブデコンパイラーも必要です。
Ghidra は Linux x64 と macOS x64/arm64 に対応します。Ghidra 12.1.x と、そのインストールが宣言する完全な 64 ビット JDK(`application.java.min` から `application.java.max` まで)を別途インストールし、REA で使うように設定してください。現行の 12.1 リリースは JDK 21 以降を要求し、上限は設定しません。ブリッジは Ghidra 12.1.4 と JDK 21 で検証しています。macOS ではホストのアーキテクチャに合うネイティブデコンパイラーも必要です。
Setup はインストールを確認し、パスを保存できます。Ghidra、Java、Node.js、npm、Homebrew のインストールや更新は行いません。
Repository main includes experimental Windows x64 Ghidra P0 for native, non-managed, non-DLL x86-64 PE applications on fixed local NTFS. The native controls are implemented; npm 4.0.1 does not contain them. See the [release boundary](docs/installation.md#released-package-and-main) and [Windows Ghidra P0](docs/windows-ghidra-p0.md).
リポジトリの 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) を参照してください。
### トラブルシューティング
@@ -166,21 +161,12 @@ Linux の既定ランチャーは `/opt/hopper/bin/Hopper` です。別の場所
## 現在の対応状況
The current repository capabilities and platform requirements are described in
[the English support guide](README.md#current-status). Main can be ahead of the
[npm release](docs/installation.md#released-package-and-main).
リポジトリの現在の機能とプラットフォーム要件は[英語の対応状況ガイド](README.md#current-status)で説明しています。main は [npm リリース](docs/installation.md#released-package-and-main)より先行する場合があります。
- Ghidra provides 25 read-only operations on Linux x64, macOS x64/arm64, and the
experimental Windows x64 P0 boundary. Linux/macOS additionally supports atomic
session function annotations. Windows P0 is read-only; Ghidra has no GUI controls.
- Static Android inspection has separate bring-your-own JADX/Java prerequisites;
real-provider verification covers Linux. See [Android analysis](docs/android-analysis.md).
- Browser, Electron, and process requests specify their target, actions, and
lifecycle directly. They require the host's actual access, without separate
REA permission grants. Setup configuration writes and Hopper installation
still require approval of their exact plan.
- `rea capabilities` describes binary-session operations rather than every
application/runtime tool. Consult the full MCP tool list and relevant guide.
- 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 ツール一覧と関連ガイドを参照してください。
## ひとつのプロンプトで調査を完結
@@ -214,20 +200,20 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
## 調査ツールカタログ
| ツール分類 | 数 | 用途 |
| -------------------- | --: | ------------------------------------------------------------------------------- |
| バイナリ検査 | 41 | 関数、疑似コード、アセンブリ、文字列、シンボル、参照、注釈 |
| 組み合わせた解析 | 14 | 概要、関数解析、一括逆コンパイル、呼び出しグラフ、Swift と ObjC |
| macOS ネイティブ | 7 | Mach-O メタデータ、署名、plist、アーキテクチャ、Swift 名の復元 |
| ファイルとパッケージ | 5 | ディレクトリとパッケージ、Interface Builder、Apple リソース、抽出 |
| .NET PE/CLI | 7 | アセンブリ識別、メタデータ、CIL、ネイティブ呼び出し、比較、再構築の取り込み |
| ファームウェア | 2 | Linux の領域検査と明示的な展開 |
| Android APK | 5 | パッケージと manifest、クラス検索、メンバー一覧、メソッド逆コンパイル、静的参照 |
| ブラウザー観察 | 9 | ページ、スクリプト、ソースマップ、WebMCP、スクリーンショット、比較 |
| Electron 解析 | 5 | ページ観察、アプリ構造、静的・実行時結果の対応付け |
| JavaScript 実行時 | 2 | 既存の Node/Electron Inspector に接続し、スクリプトと実行コンテキストを観察 |
| アプリのワークフロー | 7 | 機能追跡、バージョン比較、戻り値構造の比較、再実装の検証 |
| バイナリセッション | 21 | 対象切り替え、根拠の保存、プロセス・関数比較、未解決事項の記録 |
| ツール分類 | 数 | 用途 |
| -------------------- | --: | -------------------------------------------------------------------------------------------------------------------------------------------- |
| ネイティブ検査 | 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 対象の検出、スクリプト位置、実行コンテキストイベント |
| アプリワークフロー | 11 | 取得した Web スクリプトのエクスポート、層をまたぐ機能トレース、ビルド比較、履歴ソース対応、静的戻り値形状の比較、再実装検証 |
| ワークスペースと観察 | 21 | セッション、証跡バンドル、ナビゲーション文脈、プロセス・成果物・関数比較、未解決事項の記録 |
## ロードマップ
+36 -50
View File
@@ -15,7 +15,7 @@
[![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=trendshift-badge&amp;utm_medium=badge&amp;utm_campaign=badge-trendshift-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/82054/weekly?language=TypeScript" alt="morluto/rea | Trendshift" width="250" height="55" /></a>
<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>
[빠른 시작](#빠른-시작) · [현재 지원 범위](#현재-지원-범위) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [조사 도구 카탈로그](#조사-도구-카탈로그) · [로드맵](#로드맵) · [작동 방식](#작동-방식)
@@ -70,14 +70,14 @@ REA는 분석을 바이너리 증거에 근거하게 합니다. 원본 소스
## REA를 사용하는 이유
| | |
| ------------------ | --------------------------------------------------------------------------------------------------- |
| **에이전트용** | 컴파일된 앱에 관해 질문하고 추측 대신 증거를 수집하게 합니다. |
| **CLI와 MCP** | 터미널과 코딩 에이전트에서 동일한 리버스 엔지니어링 기능을 사용합니다. |
| **복잡성 처리** | 도구 설정, 앱 열기, 조사 유지, 작업 후 정리를 REA가 처리합니다. |
| **전체 조사 과정** | 첫 개요에서 의사 코드, 호출 관계, 타입, 구현 단서까지 이어서 조사합니다. |
| **로컬 분석** | 분석은 지원되는 로컬 호스트에서 실행되며 REA는 바이너리를 호스팅 분석 서비스에 업로드하지 않습니다. |
| **컨텍스트 유지** | 질문마다 분석을 처음부터 시작하지 않고 여러 바이너리를 연속으로 조사합니다. |
| | |
| ------------------- | --------------------------------------------------------------------------------------------------- |
| **에이전트용** | 앱의 동작에 대해 질문하고 추측 대신 에이전트가 조사하게 합니다. |
| **CLI와 MCP** | 터미널과 에이전트에서 동일한 리버스 엔지니어링 기능을 사용합니다. |
| **안내식 설정** | 에이전트 설정, 기존 분석 도구 연결, 승인 후 Hopper 설치를 지원합니다. |
| **통찰에서 코드로** | 기능을 이해한 뒤 같은 코딩 세션에서 자신의 제품에 맞는 버전을 구현합니다. |
| **로컬 분석** | 분석은 지원되는 로컬 호스트에서 실행되며 REA는 바이너리를 호스팅 분석 서비스에 업로드하지 않습니다. |
| **컨텍스트 유지** | 질문마다 분석을 처음부터 시작하지 않고 여러 앱을 연속으로 조사합니다. |
## 빠른 시작
@@ -99,28 +99,23 @@ Setup은 먼저 연결할 에이전트를 여러 개 선택하도록 안내합
Hopper는 데모 모드로 사용할 수 있습니다. 첫 실행 안내가 나오면 데모를 선택하거나 기존 라이선스를 입력하세요.
### Skill instructions and a first CLI result
### AI 코딩 어시스턴트용 스킬(선택)
AI 코딩 어시스턴트에 스킬을 추가하면 더 풍부한 컨텍스트를 얻을 수 있습니다.
```bash
npx skills add morluto/rea --skill reverse-engineer-anything
```
This installs instructions, not MCP registration or analysis engines. Follow the
[conditional connection guide](skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed).
`npx rea-agents setup` installs a matching skill and registers selected clients
only after showing the plan and receiving approval. Restart/reconnect after
registration, then verify REA tools in the active session. Repository skills can
be ahead of the [released package](docs/installation.md#released-package-and-main).
이 스킬은 REA의 조사 워크플로를 제공합니다. 위 setup을 실행해 REA를 에이전트에 연결하고 분석 도구를 설정하세요. Setup은 기본적으로 버전에 맞는 스킬을 설치합니다. 이 명령은 저장소 버전의 스킬을 설치합니다.
For your extracted JavaScript/Electron tree or ASAR, no MCP setup or native
engine is required:
압축을 해제한 JavaScript/Electron 앱 트리나 ASAR라면 MCP 설정이나 네이티브 엔진 없이 바로 분석할 수 있습니다.
```bash
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
Replace the path with your target; Windows paths such as `"D:/apps/example"`
are supported by this static workflow. Native analysis requires its own engine.
경로를 조사 대상으로 바꾸세요(Windows에서는 `"D:/apps/example"` 같은 경로). 이 정적 워크플로에서는 이런 경로도 지원됩니다. 네이티브 분석에는 전용 엔진 설정이 필요합니다.
### rea 명령 설치하기
@@ -147,11 +142,11 @@ rea setup
네이티브 바이너리 분석에는 Hopper 또는 Ghidra가 필요합니다. Hopper는 별도 소프트웨어입니다. 데모에는 공급업체의 제한이 있지만 유료 라이선스가 필수는 아닙니다.
Ghidra는 Linux x64와 macOS x64/arm64를 지원합니다. Ghidra 12.1.4와 완전한 64비트 JDK 21을 별도로 설치한 뒤 REA가 사용하도록 설정하세요. macOS에서는 호스트 아키텍처에 맞는 네이티브 디컴파일러도 필요합니다.
Ghidra는 Linux x64와 macOS x64/arm64를 지원합니다. Ghidra 12.1.x와 그 설치본이 선언한 완전한 64비트 JDK(`application.java.min`부터 `application.java.max`까지)를 별도로 설치한 뒤 REA가 사용하도록 설정하세요. 현재 12.1 릴리스는 JDK 21 이상을 요구하고 상한은 없습니다. 브리지는 Ghidra 12.1.4와 JDK 21에서 검증됩니다. macOS에서는 호스트 아키텍처에 맞는 네이티브 디컴파일러도 필요합니다.
Setup은 설치를 확인하고 경로를 저장할 수 있습니다. Ghidra, Java, Node.js, npm, Homebrew를 설치하거나 업데이트하지 않습니다.
Repository main includes experimental Windows x64 Ghidra P0 for native, non-managed, non-DLL x86-64 PE applications on fixed local NTFS. The native controls are implemented; npm 4.0.1 does not contain them. See the [release boundary](docs/installation.md#released-package-and-main) and [Windows Ghidra P0](docs/windows-ghidra-p0.md).
저장소 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)를 참고하세요.
### 문제 해결
@@ -167,21 +162,12 @@ Linux의 기본 Hopper 실행 파일은 `/opt/hopper/bin/Hopper`입니다. 다
## 현재 지원 범위
The current repository capabilities and platform requirements are described in
[the English support guide](README.md#current-status). Main can be ahead of the
[npm release](docs/installation.md#released-package-and-main).
저장소의 현재 기능과 플랫폼 요구 사항은 [영문 지원 가이드](README.md#current-status)에 설명되어 있습니다. main은 [npm 릴리스](docs/installation.md#released-package-and-main)보다 앞설 수 있습니다.
- Ghidra provides 25 read-only operations on Linux x64, macOS x64/arm64, and the
experimental Windows x64 P0 boundary. Linux/macOS additionally supports atomic
session function annotations. Windows P0 is read-only; Ghidra has no GUI controls.
- Static Android inspection has separate bring-your-own JADX/Java prerequisites;
real-provider verification covers Linux. See [Android analysis](docs/android-analysis.md).
- Browser, Electron, and process requests specify their target, actions, and
lifecycle directly. They require the host's actual access, without separate
REA permission grants. Setup configuration writes and Hopper installation
still require approval of their exact plan.
- `rea capabilities` describes binary-session operations rather than every
application/runtime tool. Consult the full MCP tool list and relevant guide.
- 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 도구 목록과 관련 가이드를 참고하세요.
## 하나의 프롬프트로 전체 조사
@@ -215,20 +201,20 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
## 조사 도구 카탈로그
| 도구 분류 | 수 | 용도 |
| ----------------- | --: | --------------------------------------------------------------------- |
| 바이너리 검사 | 41 | 함수, 의사 코드, 어셈블리, 문자열, 심볼, 참조, 주석 |
| 결합 분석 | 14 | 개요, 함수 분석, 일괄 디컴파일, 호출 그래프, Swift와 ObjC 검사 |
| macOS 네이티브 | 7 | Mach-O 메타데이터, 서명, plist, 아키텍처, Swift 이름 복원 |
| 파일과 패키지 | 5 | 디렉터리와 패키지 검사, Interface Builder, Apple 리소스, 추출 |
| .NET PE/CLI | 7 | 어셈블리 식별, 메타데이터, CIL, 네이티브 호출, 비교, 재구성 가져오기 |
| 펌웨어 | 2 | Linux 펌웨어 영역 검사 및 명시적 추출 |
| Android APK | 5 | 패키지와 manifest, 클래스 검색, 멤버 목록, 메서드 디컴파일, 정적 참조 |
| 브라우저 관찰 | 9 | 페이지, 스크립트, 소스 맵, WebMCP, 스크린샷, 캡처 비교 |
| Electron 분석 | 5 | 페이지 관찰, 앱 구조, 정적·런타임 결과 연결 |
| JavaScript 런타임 | 2 | 기존 Node/Electron Inspector에 연결해 스크립트와 실행 컨텍스트 관찰 |
| 앱 워크플로 | 7 | 기능 추적, 버전 비교, 반환 구조 비교, 재구현 검증 |
| 바이너리 세션 | 21 | 대상 전환, 근거 저장, 프로세스·함수 비교, 미해결 항목 기록 |
| 도구 분류 | 수 | 용도 |
| ----------------- | --: | ------------------------------------------------------------------------------------------------------------------------- |
| 네이티브 검사 | 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 대상 탐색, 스크립트 위치, 실행 컨텍스트 이벤트 |
| 앱 워크플로 | 11 | 캡처한 웹 스크립트 내보내기, 계층 간 기능 추적, 빌드 비교, 히스토리 소스 매핑, 정적 반환 구조 비교, 재구현 검증 |
| 작업 공간과 관찰 | 21 | 세션, 근거 번들, 탐색 컨텍스트, 프로세스·아티팩트·함수 비교, 미해결 항목 기록 |
## 로드맵
+35 -49
View File
@@ -15,7 +15,7 @@
[![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=trendshift-badge&amp;utm_medium=badge&amp;utm_campaign=badge-trendshift-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/82054/weekly?language=TypeScript" alt="morluto/rea | Trendshift" width="250" height="55" /></a>
<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>
[快速开始](#快速开始) · [当前状态](#当前状态) · [从二进制到行为](#从二进制到行为) · [调查工具目录](#调查工具目录) · [路线图](#路线图) · [工作原理](#工作原理)
@@ -71,12 +71,12 @@ REA 让调查始终以二进制证据为依据。它不会声称能恢复原始
| | |
| ---------------- | -------------------------------------------------------------------- |
| **为智能体设计** | 直接询问编译后应用的行为,让智能体搜集证据,而不是猜测。 |
| **CLI 与 MCP** | 在终端或编程智能体中使用同一套逆向工程能力。 |
| **处理复杂流程** | REA 负责工具设置、打开应用、维持调查过程,并在完成后清理资源。 |
| **完整工作流** | 从初步概览推进到伪代码、调用关系、类型和实现线索。 |
| **为智能体设计** | 直接询问应用的行为,让智能体去检查,而不是猜测。 |
| **CLI 与 MCP** | 在终端或智能体中使用同一套逆向工程能力。 |
| **引导式设置** | 配置智能体、连接已有分析工具,或在你批准后安装 Hopper。 |
| **从洞察到代码** | 理解功能后,在同一次编程会话中构建属于你自己的版本。 |
| **本地运行** | 分析在受支持的本机系统上运行;REA 不会把二进制上传到托管式分析服务。 |
| **保留上下文** | 连续调查多个二进制文件,无需为每个问题重新开始整个分析过程。 |
| **保留上下文** | 连续调查多个应用,无需为每个问题重新开始整个分析过程。 |
## 快速开始
@@ -98,28 +98,23 @@ Setup 会先展示变更并备份已有配置。要求与更多选项见[安装
Hopper 支持演示模式。如果首次启动时出现提示,选择演示模式或输入已有许可证。
### Skill instructions and a first CLI result
### AI 编程助手技能(可选)
为你的 AI 编程助手添加 skill,以获得更丰富的上下文:
```bash
npx skills add morluto/rea --skill reverse-engineer-anything
```
This installs instructions, not MCP registration or analysis engines. Follow the
[conditional connection guide](skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed).
`npx rea-agents setup` installs a matching skill and registers selected clients
only after showing the plan and receiving approval. Restart/reconnect after
registration, then verify REA tools in the active session. Repository skills can
be ahead of the [released package](docs/installation.md#released-package-and-main).
该 skill 提供 REA 的调查工作流。先运行上文的 setup,把 REA 连接到你的智能体并配置分析工具。Setup 默认会安装版本匹配的 skill;这条命令安装的是仓库版本。
For your extracted JavaScript/Electron tree or ASAR, no MCP setup or native
engine is required:
对于已解包的 JavaScript/Electron 应用目录或 ASAR,无需 MCP 配置,也无需原生引擎:
```bash
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
Replace the path with your target; Windows paths such as `"D:/apps/example"`
are supported by this static workflow. Native analysis requires its own engine.
把路径换成你的目标(例如 Windows 上的 `"D:/apps/example"`)。这条静态流程支持此类路径。原生分析仍需要先配置对应的分析引擎。
### 安装 rea 命令
@@ -138,7 +133,7 @@ npm install --global rea-agents
rea setup
```
### 运行要求
### 系统要求
- macOS 12 或更高版本
- Ubuntu 24.04+、Fedora 41+ 或 64 位 Arch Linux
@@ -146,13 +141,13 @@ rea setup
原生二进制分析需要 Hopper 或 Ghidra。Hopper 是独立软件;演示模式有厂商规定的限制,不要求购买许可证。
Ghidra 支持 Linux x64 和 macOS x64/arm64。单独安装 Ghidra 12.1.4 和完整的 64 位 JDK 21,然后配置 REA 使用它们。macOS 还需要对应架构的原生反编译器。
Ghidra 支持 Linux x64 和 macOS x64/arm64。单独安装 Ghidra 12.1.x,以及该安装在 `application.java.min` 到 `application.java.max` 中声明的完整 64 位 JDK,然后配置 REA 使用它们。当前 12.1 发行版要求 JDK 21 或更新版本,并且不设置上限。桥接在 Ghidra 12.1.4 和 JDK 21 上完成验证。macOS 还需要对应架构的原生反编译器。
Setup 可以验证安装并保存路径,不会安装或升级 Ghidra、Java、Node.js、npm 或 Homebrew。
Repository main includes experimental Windows x64 Ghidra P0 for native, non-managed, non-DLL x86-64 PE applications on fixed local NTFS. The native controls are implemented; npm 4.0.1 does not contain them. See the [release boundary](docs/installation.md#released-package-and-main) and [Windows Ghidra P0](docs/windows-ghidra-p0.md).
仓库 main 分支与 npm 4.1.0 已包含实验性的 Windows x64 Ghidra P0 支持,适用于本地 NTFS 上的原生 x86-64 PE 应用(非托管、非 DLL),并附带 Job Object、私有 DACL 与路径准入控制。请先确认[发布边界](docs/installation.md#released-package-and-main),再假定旧版 npm 包具备该能力。详见 [Windows Ghidra P0](docs/windows-ghidra-p0.md) 的前置条件与已验证范围。
### 故障排查
### 故障排除
运行 `npx -y rea-agents@latest doctor`,检查主机、依赖、分析工具和智能体配置。该命令不会修改文件。添加 `--json` 可获取结构化诊断。
@@ -166,21 +161,12 @@ Linux 上的默认 Hopper 启动器为 `/opt/hopper/bin/Hopper`。其他路径
## 当前状态
The current repository capabilities and platform requirements are described in
[the English support guide](README.md#current-status). Main can be ahead of the
[npm release](docs/installation.md#released-package-and-main).
仓库当前能力与平台要求见[英文支持指南](README.md#current-status)。main 分支可能领先于 [npm 发布版](docs/installation.md#released-package-and-main)。
- Ghidra provides 25 read-only operations on Linux x64, macOS x64/arm64, and the
experimental Windows x64 P0 boundary. Linux/macOS additionally supports atomic
session function annotations. Windows P0 is read-only; Ghidra has no GUI controls.
- Static Android inspection has separate bring-your-own JADX/Java prerequisites;
real-provider verification covers Linux. See [Android analysis](docs/android-analysis.md).
- Browser, Electron, and process requests specify their target, actions, and
lifecycle directly. They require the host's actual access, without separate
REA permission grants. Setup configuration writes and Hopper installation
still require approval of their exact plan.
- `rea capabilities` describes binary-session operations rather than every
application/runtime tool. Consult the full MCP tool list and relevant guide.
- 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 工具列表与相关指南为准。
## 一个提示词,完成一次完整调查
@@ -214,20 +200,20 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
## 调查工具目录
| 工具类别 | 数量 | 用途 |
| ----------------- | ---: | -------------------------------------------------------------- |
| 二进制检查 | 41 | 函数、伪代码、汇编、字符串、符号、段、引用与注释 |
| 组合分析 | 14 | 概览、函数分析、批量反编译、调用图、Swift 与 ObjC 检查 |
| macOS 原生工具 | 7 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原 |
| 制品检查 | 5 | 目录与软件包检查、Interface Builder、Apple 资源目录与提取 |
| .NET PE/CLI | 7 | 程序集身份、元数据、CIL、原生调用声明、成员比较与重建导入 |
| 韧体 | 2 | Linux 韧体区域识别与显式解包 |
| Android APK | 5 | 包与 manifest 声明、class 搜索、成员清单、方法反编译与静态引用 |
| 浏览器观察 | 9 | 页面、脚本、来源映射、WebMCP 工具、截图与捕获比较 |
| Electron 分析 | 5 | 页面观察、应用结构与静态/运行时结果关联 |
| JavaScript 运行时 | 2 | 连接现有 Node/Electron Inspector,观察脚本与执行上下文 |
| 应用工作流 | 7 | 功能追踪、版本比较、返回结构比较与重建验证 |
| 二进制会话 | 21 | 目标切换、证据包、进程与函数比较、待解决问题记录 |
| 工具类别 | 数量 | 用途 |
| ----------------- | ---: | ------------------------------------------------------------------------------------------------------ |
| 原生检查 | 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 目标发现、脚本位置与执行上下文事件 |
| 应用工作流 | 11 | 捕获的网站脚本导出、跨层功能追踪、构建比较、历史源码映射、静态返回结构比较与重建验证 |
| 工作区与观察 | 21 | 会话、证据包、导航上下文、进程/产物/函数比较与待解决问题跟踪 |
## 路线图
+19 -3
View File
@@ -5,7 +5,7 @@ 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 absent from npm 4.0.1. Check the
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.
@@ -88,8 +88,24 @@ selected overload. Native/abstract methods with no decompiled body report
These operations do not execute the APK or provide split APK/AAB handling,
signature validation, full resource-table semantics, native-library analysis or
Android runtime capture. Existing artifact inventory/extraction tools can supply
archive evidence; `project_android_application_graph` remains a separate,
execution-free inventory projection.
archive evidence. Project that Evidence with the execution-free inventory tool:
```sh
rea project-android-application-graph '{"inventory_evidence":[<inventory_artifact Evidence>]}'
```
```json
{
"name": "project_android_application_graph",
"arguments": {
"inventory_evidence": ["<inventory_artifact Evidence>"]
}
}
```
The projection reports exact component paths and hashes, runtime-family hints,
and path-based bridge hypotheses. It does not decode DEX or claim observed
runtime calls.
## Resource and lifecycle limits
+13
View File
@@ -6,6 +6,19 @@ execution-free: it reports exact paths, content hashes, detected formats,
runtime-family hints, and path-based bridge hypotheses. It does not parse plist
or CMS semantics or claim observed runtime calls.
```sh
rea project-apple-application-graph '{"inventory_evidence":[<inventory_artifact Evidence>]}'
```
```json
{
"name": "project_apple_application_graph",
"arguments": {
"inventory_evidence": ["<inventory_artifact Evidence>"]
}
}
```
The result includes every component in each inventory category and every
JavaScript-to-native bridge candidate pair. It has no caller-selected component
budget, prefix truncation, or omitted-count fields. Duplicate inventory pages
+13 -6
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,14 +9,16 @@ 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"]
RECORDS["src/application/investigation/<br/>composed InvestigationRecords + narrow ports<br/>EvidenceLedger and Unknown ownership"]
SNAPSHOT["BinarySessionRecords + AnalysisSnapshotCache<br/>binary target/profile snapshots<br/>compatibility facade and post-commit notifications"]
JS_RECON["src/application/ + src/artifacts/<br/>JavaScript artifact reconstruction<br/>directory/ASAR reads<br/>AST-only bundle projection"]
end
subgraph "Provider Layers"
@@ -25,7 +27,7 @@ graph TD
IDA["src/ida/<br/>Upstream MCP contract adaptation<br/>live GUI binding or owned headless database"]
IDA_MCP["External ida-pro-mcp<br/>GUI proxy or database supervisor<br/>upstream installation and engine lifecycle"]
AUX["src/artifacts/ + src/native/<br/>disjoint artifact and native capabilities"]
BROWSER["src/browser/<br/>Loopback CDP transport<br/>exact-origin passive observation"]
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"]
@@ -48,7 +50,7 @@ graph TD
end
subgraph "Configuration"
CONFIG["src/config.ts<br/>Zod-validated env parsing<br/>provider settings and origins"]
CONFIG["src/config.ts + src/config/<br/>Zod-validated env parsing<br/>provider settings and origins"]
end
subgraph "Tests"
@@ -75,6 +77,11 @@ graph TD
CLI --> ERRORS
APP --> SESSION
SESSION --> REGISTRY
SESSION --> SNAPSHOT
SNAPSHOT --> RECORDS
APP -->|record callbacks| RECORDS
SERVER -->|narrow record ports| RECORDS
RECORDS --> DOMAIN
REGISTRY --> HOPPER
REGISTRY --> GHIDRA
REGISTRY --> IDA
+20 -3
View File
@@ -1,5 +1,9 @@
# 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
@@ -8,7 +12,7 @@ REA can attach to a user-owned Chrome-family browser through the Chrome DevTools
- `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 a content-addressed visible-viewport PNG.
- `compare_web_screenshots` / `rea compare-web-screenshots` performs bounded local PNG pixel comparison without OCR or external services.
@@ -18,6 +22,17 @@ REA can attach to a user-owned Chrome-family browser through the Chrome DevTools
Target discovery returns the complete in-scope target array in one result.
Electron `file://` pages use a separate provider and target boundary; see [electron-observation.md](electron-observation.md).
## Provider authorities
CDP page capture, V8 Inspector observation, and Playwright scenarios stay
separate providers because their authorities differ: CDP and V8 attach to an
already-running target and never launch, drive, evaluate, or mutate it, while
Playwright providers own and drive the runtime (launch browsers, run actions,
capture step snapshots). Merging them would mix attach-only and owned-process
lifecycles in one contract. What they share instead is one exclusion
vocabulary (`BrowserExclusionReason`): every denial reason in every stack maps
onto it, even where a wire schema keeps a narrower historical bucket.
Existing static application Evidence and passive web/Electron captures can be
combined later through
[JavaScript static/runtime reconciliation](javascript-runtime-reconciliation.md).
@@ -120,7 +135,7 @@ REA retains local observation data and removes only URL userinfo credentials, ex
- Request/response bodies are not requested or parsed by default. When selected, only allowed-origin JSON media types are read, converted immediately to complete value-free property paths/types, and discarded. `Network.getResponseBody` is never sent unless JSON body shape capture is selected.
- Console observations with a stack source retain call type, argument types, timestamp, and source location. When console text capture is selected, already-delivered primitive values are retained verbatim; objects, getters, and remote properties are never expanded.
- WebSocket observations retain direction, opcode, and payload byte length. When shape capture is selected, text frames are classified as text or complete value-free JSON shape; binary bytes, hashes, prefixes, and raw frames are never retained.
- 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.
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.
@@ -142,10 +157,12 @@ Disallowed-origin entries are filtered before retention.
CDP discovery, WebSocket connection, and commands continue until the peer responds or the caller cancels the operation; REA has no internal byte, message-count, or pending-command quota for these exchanges. Without caller cancellation, a stalled peer can leave an operation waiting. On cancellation, REA closes only its own WebSocket, waiting up to one second before terminating it. Malformed unsolicited protocol events poison the connection so later commands fail closed.
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.
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 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, 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.
+77
View File
@@ -194,6 +194,83 @@ 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
`compare_web_captures` and `compare-web-captures` also accept two complete
+191
View File
@@ -0,0 +1,191 @@
# 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 | InvestigationRecords/EvidenceLedger | Consume narrow read/write/atomic record ports |
| Target/profile snapshots and invalidation | BinarySessionRecords/BinarySession | Keep binary-owned |
| Subprocess ownership and host primitives | process/windows | Reuse; preserve cancellation and cleanup |
| Public translation | CLI/MCP adapters | Delegate to shared workflows and named contracts |
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.
`npm run verify:module-boundaries` parses repository-owned source imports,
reexports, type imports and literal dynamic imports, then checks their resolved
source layer. Nested names such as `domain/android` are domain code; `src/android`
is the provider adapter. The check runs with lint, including cached static gates.
It covers tracked and untracked non-ignored TypeScript source and skips cached
paths removed during a move. Pass exact source paths for a focused check.
Pure source and its tests keep inward dependencies. Application/server provider
construction guards cover the migrated deep, Android, firmware, JavaScript recovery and observation
implementations. The existing binary runtime is an exact temporary composition
exception; shared browser capture/export helpers remain admitted while their
ownership is reviewed. Test lane restrictions still use Oxlint. Producer-dependent
browser and managed comparison tests live in boundary lanes with their original
fixtures and assertions. This is a development guard, not runtime authorization.
Select verification by the claim changed: schema/composition, package, real
engine or host-native workflow. Record host/target prerequisites and source-owned
fixtures. Run heavy lanes serially with bounded workers/heaps and reuse existing
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.
## Independent optional observation adapters
Browser observation, browser scenarios, Electron observation, Electron scenarios
and V8 Inspector observation now have separate factories in `src/composition/`.
CLI commands reuse them; MCP startup dynamically imports each factory independently.
`OptionalObservationProviders` describes these five existing typed ports only. It
is provisional startup wiring, not an extensible plugin manifest or a universal
provider interface.
A failed import or constructor retains the successful peers. The complete tool
catalog remains advertised. Session availability reports the failed adapter and
its actual reason, and affected handlers return the existing capability-unavailable
error. Explicit loading failures take precedence over configured availability.
Passive Electron and active Electron failures are independent; static JavaScript
analysis and runtime reconciliation remain usable without either adapter. CLI
Electron commands also load only their selected runtime factory at execution.
Source identities are available without importing the optional implementations.
Registration acquires no engines, probes no endpoints, changes no permissions and
installs nothing. Core transport failures and shutdown still use their existing
lifecycle. Fault-injection coverage uses the production loading/startup path and
actual MCP SDK calls; recording ports establish composition behavior, while the
real Inspector lane separately establishes runtime behavior.
## Investigation record ownership pilot
`src/application/investigation/InvestigationRecords.ts` owns one existing
EvidenceLedger per runtime. The production session composition constructs that
owner explicitly; the direct BinarySession constructor retains a fresh default
for compatibility. There is no second ledger, global store, persistence or new
retention policy.
The ledger, Unknown Evidence helper and the ledger's two original behavior tests
now live beside the owner in `src/application/investigation/`. Bundle validation
and record-owner consumers follow the new paths; retained-reference access still
uses the narrow record ports. The move preserves implementation bodies and test
assertions. Binary snapshot/cache files remain with their existing owner.
EvidenceReader, EvidenceWriter and EvidenceUnknownWriter describe existing
read/write/atomic callback needs. UnknownRegistryPort preserves optimistic
revisions and consistency verification. Non-binary MCP registrars use these
record types without depending on the binary execution/lifecycle surface.
BinarySessionPort retains its compatible record methods through composition.
BinarySessionRecords remains the snapshot owner and compatibility facade. It
passes the actual active target explicitly for recordUnknown/updateUnknown;
recordEvidenceWithUnknown keeps its target-free mutation subject. Snapshot import
merges records without notifying midway, then emits after the cache commits.
Bundle import retains the full change delta so Unknown-only changes notify even
when recordsAdded is zero. Clearing resets records, cache and invalidation before
notification; observer failures remain best effort after a committed mutation.
Close and failed-cleanup clearing, ordinary target-switch retention, detached
reads, Evidence identities and deterministic bundle transfer keep their existing
behavior. Snapshots still require an active target and concrete analysis profile;
extracting investigation ownership does not make them a target-free record store.
#721 remains responsible for any future lifetime-policy change.
## Auxiliary source declaration pilot
Artifact, macOS native and managed static metadata now belongs to each adapter's
`*ProviderMetadata.ts` module. The implementation and
`src/composition/auxiliaryAnalysisProviders.ts` reuse the same identity and exact
capability descriptors. The declarations reference existing AnalysisProvider
loaders; they cover these three disjoint auxiliary implementations only. Deep
candidate selection and the five typed observation ports remain separate.
The binary runtime reads these source declarations while auxiliary construction
stays lazy. The separate managed-only entrypoint retains direct construction for
its execution-free workflow. The MCP catalog generator reads declarations without
constructing implementations, using its canonical macOS platform; actual runtime
host restrictions remain explicit.
Discovery and binary operation-kind routing now consume canonical source contracts
rather than their generated JSON artifact. No declaration imports its generated
output. Generated catalog facts and public schemas stay unchanged.
All observation registrars resolve contracts by name. An isolated compiled-process
test reverses presentation arrays and performs actual MCP SDK calls to verify
advertised schemas and handler meaning. Reordering metadata must not select a
different operation, schema, provider or deep-provider priority.
+8 -4
View File
@@ -1,7 +1,10 @@
# Firmware analysis
REA integrates **Binwalk 3.1.0** for region inspection and **Unblob 26.6.4** for
explicit extraction. This first firmware milestone is verified on Linux x64.
REA accepts **Binwalk 3.1.x** for region inspection and **Unblob 26.6.x** for
explicit extraction. The report parsers are verified with Binwalk 3.1.0 and
Unblob 26.6.4. Another build on those lines is accepted, the observed version
is reported, and a limitation records that the parser was not verified against
that build. This first firmware milestone is verified on Linux x64.
No device, root privilege, mount, emulation or execution of extracted programs
is needed. Arbitrary firmware bytes do not pass native executable admission;
select a returned ELF separately for supported Ghidra analysis.
@@ -16,7 +19,8 @@ export REA_UNBLOB_COMMAND=/absolute/path/to/unblob
```
REA does not install these tools, their dependencies or system packages. It
checks the actual tool version when executing a request. Inspection and
checks the tool's `--version` banner when executing a request. Binwalk must
print `binwalk <version>` and Unblob must print the version alone. Inspection and
extraction have independent prerequisites; one does not require the other.
Both require util-linux `prlimit`, normally `/usr/bin/prlimit`; override its
absolute path with `REA_FIRMWARE_PRLIMIT_COMMAND` if needed.
@@ -161,7 +165,7 @@ Additional lanes are explicit, so the base lane stays small:
REA_FIRMWARE_VERIFY_EXT4=1 npm run fixtures:firmware
REA_FIRMWARE_VERIFY_EXT4=1 npm run verify:firmware
# Existing Ghidra 12.1.4 and Java required.
# Existing Ghidra 12.1.x and its declared JDK required.
REA_FIRMWARE_VERIFY_GHIDRA=1 npm run verify:firmware
```
+3 -3
View File
@@ -2,9 +2,9 @@
REA can analyze DOS MZ executables and explicitly selected COM images through the bring-your-own Ghidra adapter.
The Linux x64 verification lane exercises actual 16-bit disassembly and
decompilation through the CLI and stdio MCP. It requires the same Ghidra
12.1.4 and 64-bit JDK 21 installation as other Ghidra sessions; no DOS emulator
or cross-compiler is required.
decompilation through the CLI and stdio MCP. Provider admission accepts Ghidra
12.1.x and that installation's declared JDK range. The verification lane uses
Ghidra 12.1.4 and JDK 21; no DOS emulator or cross-compiler is required.
## Open and inspect
+4 -1
View File
@@ -7,7 +7,10 @@ source notices are retained. No upstream source or algorithms are rewritten.
The initial tested layout is .NET runtime **8.0.22**, NativeAOT RTR **9.1**,
x86-64 ELF and native Windows PE **targets analyzed on Linux x64**, using
Ghidra **12.1.4** and JDK **21**. Other layouts/architectures/hosts are unsupported.
Ghidra **12.1.4** and JDK **21**. The build accepts any Ghidra 12.1.x release and
javac 21 or newer, and it compiles with `--release 21`. Real verification of
this layout remains the Ghidra 12.1.4 and JDK 21 pair. Other NativeAOT layouts,
architectures, and hosts are unsupported.
Windows Ghidra P0 has no database mutation authority. A PE/CLI or ReadyToRun
assembly should use `inspect_managed_artifact`; it is not NativeAOT.
+20 -11
View File
@@ -68,13 +68,12 @@ 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.0.1** (116 MCP tools). That artifact
has static JavaScript inspection and full inline Evidence workflows, but does
not include main's Windows native control bundle, Android/JADX or firmware
tools, Ghidra function annotation workflow, or retained application-Evidence
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 those additions; a source build or a subsequent
release containing them is required. Package startup alone does not verify a
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`.
@@ -109,7 +108,9 @@ npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --
```
It returns the complete Evidence record directly and requires no native engine.
Provider failures in doctor do not prevent unrelated target-free tools.
Provider failures in doctor do not prevent unrelated target-free tools. To check
readiness for one task instead of auditing every integration, see
[Check readiness for the task at hand](../README.md#check-readiness-for-the-task-at-hand).
## Supported agents
@@ -130,6 +131,11 @@ Setup can configure these clients for REA's local MCP server:
| Command Code | `commandcode` |
| VS Code | `vscode` |
For OpenCode, setup writes the V1 `mcp.rea` entry, which OpenCode V1 and V2
both load. If the configuration already uses OpenCode V2's native
`mcp.servers` table, setup registers REA there instead and replaces any earlier
`mcp.rea` entry from REA.
## Review setup changes
`rea setup` first offers the supported agents in a multi-select. Existing REA
@@ -265,7 +271,10 @@ copying license secrets into logs, or killing unrelated Hopper processes.
REA connects to an existing Ghidra installation on Linux x64, macOS x64/arm64,
or experimental Windows x64 P0.
It requires Ghidra 12.1.4 and a 64-bit full JDK 21. On macOS, the installation
It accepts Ghidra 12.1.x and the 64-bit full JDK declared by that installation's
`application.java.min` and `application.java.max`. Current 12.1 releases require
JDK 21 or newer and set no maximum. The bridge is verified with Ghidra 12.1.4
and JDK 21. On macOS, the installation
must include the native decompiler for the host architecture; REA does not
build it or change Gatekeeper quarantine settings.
@@ -311,9 +320,9 @@ installation remains unavailable on Windows. Setup never installs Ghidra,
Java, or Python. It preserves valid detected Ghidra/JDK settings in agent
registrations. See [Windows Ghidra P0](windows-ghidra-p0.md) for provider diagnostics.
Doctor validates the platform, architecture, application version,
`support/analyzeHeadless` or `support/analyzeHeadless.bat`, Java
version/bitness, and the presence of `javac`/`javac.exe`.
Doctor validates the platform, architecture, Ghidra 12.1.x application version,
`support/analyzeHeadless` or `support/analyzeHeadless.bat`, the installation's
Java major range, 64-bit JDK bitness, and the presence of `javac`/`javac.exe`.
When Java is found through `PATH`, setup records its observed JDK home so GUI
MCP clients do not depend on an incidental shell path. Setup shows every exact
environment entry in its plan, writes only after approval, and never downloads,
+11 -1
View File
@@ -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
@@ -164,7 +174,7 @@ 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. npm 4.0.1 accepts full inline Evidence only; installing
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).
+90
View File
@@ -0,0 +1,90 @@
# JavaScript source recovery
`recover_javascript_sources` / `recover-javascript-sources` derives readable
modules from one explicitly selected UTF-8 JavaScript file or bundle. Use it on
files returned by `export_web_scripts` or other local scripts. The operation
never executes the selected or recovered application code.
## Caller-supplied engine
The initial adapter is verified on Linux x64 with Wakaru **1.13.0**. Set
`REA_WAKARU_COMMAND` to the absolute path of an executable supplied by the caller.
The adapter uses util-linux `prlimit` (default `/usr/bin/prlimit`; override with
an absolute `REA_JAVASCRIPT_PRLIMIT_COMMAND`). REA does not install either tool.
Engine configuration is checked only when recovery is requested; other REA
capabilities remain available without it.
Unmodified upstream source is retained in `third_party/wakaru`, pinned to
`6070266d24b32951fa2fe1d2dad5b4313bd5c81b` (v1.13.0, Apache-2.0).
The result reports the executed binary's exact path, SHA-256 and observed version.
The audited source revision is distinct from `executed_source_revision: null`:
a version string cannot prove which source produced a caller-supplied binary.
## Recover and analyze
```sh
rea recover-javascript-sources /tmp/web-scripts/app.js /tmp/recovered-app --json
rea analyze-javascript-application /tmp/recovered-app/modules --json
```
MCP input:
```json
{
"path": "/tmp/web-scripts/app.js",
"output_directory": "/tmp/recovered-app"
}
```
The output directory must be absent, absolute, and have an existing parent.
The source is snapshotted in a private workspace, and the engine writes only to
its own staging directory. Reported module paths, input identities, UTF-8 byte
ranges, regular files and emitted map representations are validated before
publication. REA writes verified bytes to an exclusively owned output tree and
rolls it back on failure. It also checks that the original source and engine
bytes remain unchanged before committing. The existing output is never overwritten.
Published contents:
- `inputs/bundle.js`: exact original snapshot; its digest binds the Evidence.
- `modules/`: derived modules and any upstream-emitted source maps.
- `reports/stdout.json` and `reports/provenance.json`: exact producer reports.
- `manifest.json`: identities, modules, transform choices, warnings and limitations.
The inline result includes complete file paths/digests/sizes, extraction ranges,
warnings and `analysis_input`, directly usable as the arguments to
`analyze_javascript_application`. Raw Evidence retains selected command arguments,
reported input paths, exit status, stderr and reports; temporary paths are recorded
as historical execution coordinates after their workspace is removed.
## Transform choices and interpretation
`extraction_mode` defaults to `structural` (Wakaru `--unpack=strict`). `heuristic`
uses `--unpack=auto`; `inspection` uses `--unpack=inspect` and can return finer,
non-executable regions. `rewrite_level` accepts `minimal`, `standard` (default)
and `aggressive`. Maps and provenance are requested in every operation.
Structural extraction can fall back to a single file. In the real fixtures,
webpack 5 yields two modules; the esbuild scope-hoisted fixture remains one
unknown-format script. Returned module counts and detected formats report those
actual outcomes. A valid report with failed transformations returns `partial`
with original warnings and files. An unusable report, inconsistent provenance,
escaping path, symlink or missing module is an error, with owned cleanup.
Evidence confidence is `derived`. Extraction ranges are half-open **UTF-8 byte
offsets in the original snapshot**, rather than rewritten line positions or a
coverage assertion. Source maps are preserved without claiming independently
verified mapping accuracy. Original variable names and arbitrary runtime
equivalence remain unknown. No network fetching occurs.
## Resource policy and verification
Each operation uses one worker, a 1 GiB address-space ceiling and a 120-second
deadline; it accepts up to 64 MiB of input and 128 MiB/10000 entries of staged
output, with 8 MiB per diagnostic/report stream. Exceeding a limit is an explicit
failure with rollback. These limits apply per operation and do not claim aggregate
host containment across concurrent callers.
See [testing.md](testing.md#javascript-source-recovery) for the focused real
Wakaru CLI/MCP lane and its source-owned compiler fixtures. Other host platforms
and Bun/Vue recovery are not claimed by this adapter.
+45
View File
@@ -90,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
+7 -1
View File
@@ -6,6 +6,12 @@ coverage and unknowns. The CLI and MCP use the same application workflows.
## Static inspection
`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.
- `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.
@@ -144,7 +150,7 @@ actions may change application data or trigger network activity.
## Provider and verification boundaries
Install Ghidra 12.1.4 and a full 64-bit JDK 21 separately, then configure REA to
Install a Ghidra 12.1.x release and the 64-bit full JDK it declares, then configure REA to
use them. Ghidra analysis supports Linux x64 and macOS x64/arm64; macOS requires
the matching native decompiler. Experimental Windows x64 P0 admits native
x86-64 PE applications on local NTFS using bundled Job Object ownership,
+20
View File
@@ -12,6 +12,24 @@ 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.
## Host support
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.
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.
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:
@@ -22,6 +40,8 @@ rea capture-process ./scenario.json --json > capture.json
`--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.
For example:
+40 -8
View File
@@ -10,7 +10,7 @@
"skill_version": "26"
},
"tools": {
"total": 125,
"total": 129,
"families": [
{
"id": "direct",
@@ -179,13 +179,17 @@
{
"id": "application",
"surface": "application-workflow",
"count": 7,
"count": 11,
"tools": [
"build_reconstruction_obligation_ledger",
"compare_application_versions",
"compare_javascript_export_shapes",
"compare_source_to_bundle",
"evaluate_reconstruction_coverage",
"export_web_scripts",
"project_android_application_graph",
"project_apple_application_graph",
"recover_javascript_sources",
"trace_application_feature",
"trace_javascript_semantics"
]
@@ -349,6 +353,18 @@
"observe_native_ui"
]
},
{
"id": "rea-android-application",
"name": "REA Android application workflows",
"version": "1",
"capabilities": ["project_android_application_graph"]
},
{
"id": "rea-apple-application",
"name": "REA Apple application workflows",
"version": "1",
"capabilities": ["project_apple_application_graph"]
},
{
"id": "rea-artifact-graph",
"name": "REA safe artifact graph provider",
@@ -450,11 +466,23 @@
"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"]
},
{
"id": "wakaru",
"name": "Wakaru",
"version": "1.13.0",
"capabilities": ["recover_javascript_sources"]
}
],
"setup_clients": [
@@ -532,7 +560,7 @@
}
],
"cli": {
"primary_count": 80,
"primary_count": 84,
"commands": [
"address-to-file-offset",
"analyze",
@@ -562,6 +590,7 @@
"evaluate-reconstruction-coverage",
"evidence-export",
"evidence-import",
"export-web-scripts",
"extract-artifact",
"extract-firmware",
"function",
@@ -596,10 +625,13 @@
"observe-javascript-runtime",
"observe-native-ui",
"observe-web-session",
"project-android-application-graph",
"project-apple-application-graph",
"project-managed-application-graph",
"providers",
"read-bytes",
"reconcile-javascript-runtime",
"recover-javascript-sources",
"resolve-native-call-targets",
"search",
"search-android-classes",
@@ -624,15 +656,15 @@
},
"runtime_catalog": {
"counts": {
"cli_commands": 80,
"mcp_tools": 125,
"cli_commands": 84,
"mcp_tools": 129,
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "eec3abb8fa5b25c6e5d6e361b7ebd157e3599f69b68defda85680dbfe061b3d3",
"tools_sha256": "53ac19960ec7b9907a63951a4e4d8eff02a6c41ad04a4611d61370b8511cc783",
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
"combined_sha256": "ed9c36900e9223561d38ba30a44921f4ff101d5eca989920f9edc9dc91df470b",
"providers_sha256": "044634314b36b966bd918f99f116f4684108556ac1dabc912e76ccd351d97e3e"
"combined_sha256": "856ce0f99cc6e858f3317e40cec27c2cfd479ae7ff6a8d9cf695504d6166b64d",
"providers_sha256": "6e1de7ee30b9b1ff633abfa4fd2b3848bb58e6e76a2ed196aa01836a202e1941"
}
}
}
+9 -5
View File
@@ -3,8 +3,10 @@
This guide describes repository main. Check the [released package boundary](installation.md#released-package-and-main) when using npm.
Ghidra read-only analysis is available on Linux x64 and macOS x64/arm64.
Install Ghidra 12.1.4 and a full 64-bit JDK 21 separately, then configure REA to
use them. macOS installations need the matching native decompiler.
Install a Ghidra 12.1.x release and the 64-bit full JDK that release declares,
then configure REA to use them. Current 12.1 releases require JDK 21 or newer
and set no maximum. The bridge is verified with Ghidra 12.1.4 and JDK 21.
macOS installations need the matching native decompiler.
REA imports one target into a temporary project and exposes 25 read-only
operations after analysis completes. They cover inventories, search,
@@ -53,9 +55,11 @@ follow.
## Shipped foundation boundary
`GHIDRA_INSTALL_DIR` must identify an extracted official 12.1.4 release;
optional `JAVA_HOME` must identify a 64-bit full JDK 21, otherwise doctor probes
`java`/`javac` or `java.exe`/`javac.exe` from `PATH`. Supported Linux and macOS
`GHIDRA_INSTALL_DIR` must identify an extracted Ghidra 12.1.x release. Optional
`JAVA_HOME` must identify a 64-bit full JDK inside that installation's
`application.java.min` and `application.java.max` (JDK 21 or newer, with no
maximum, when the release leaves those at the 12.1 defaults). Otherwise doctor
probes `java`/`javac` or `java.exe`/`javac.exe` from `PATH`. Supported Linux and macOS
hosts accept compatible ELF, PE, and Mach-O executable targets. Host admission
and target compatibility are separate checks. Windows x64 P0 admits only native,
non-managed, non-DLL x86-64 PE applications on fixed local NTFS, with the packaged
+4 -2
View File
@@ -45,8 +45,10 @@ verified paths for an existing Ghidra installation. It configures Claude Code,
Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity,
GitHub Copilot CLI, Command Code, and VS Code using each client's configuration format.
Ghidra analysis supports Linux x64 and macOS x64/arm64 with Ghidra 12.1.4 and a
64-bit full JDK 21. macOS also requires the matching native decompiler. The
Ghidra analysis supports Linux x64 and macOS x64/arm64 with Ghidra 12.1.x and the
64-bit full JDK that installation declares. Current 12.1 releases require JDK 21
or newer and set no maximum; the bridge is verified with Ghidra 12.1.4 and JDK 21.
macOS also requires the matching native decompiler. The
adapter exposes thirteen inventory operations and twelve function-analysis
operations, for 25 read-only operations total. Linux and macOS additionally support
atomic function-name and entry-comment edits with refreshed analysis; metadata
-169
View File
@@ -1,169 +0,0 @@
# Test suite audit
## Scope and priority
The expanded audit on 2026-10-04 reviewed every one of the 355 Vitest files
present at its start: 82 domain/contracts, 56 application/composition, 147
adapter/configuration/process/filesystem/CLI files, and 70 MCP/acceptance/
conformance/evaluation files. Three review passes checked the proposed removals
against existing stronger coverage and checked the final changes for lost
assertions. The remaining suite has 351 files.
Prefer full real-provider E2E, then integration across schemas and transports,
then real-data goldens. Remove lower-level duplication once the replacement
asserts the same behavior. Keep distinct failure regressions that a successful
E2E cannot exercise. Do not lower coverage thresholds to accommodate pruning.
## Changes made
| Previous test or assertion | Decision | Replacement or retained claim |
| ----------------------------------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Asset application tests using fabricated `.car` bytes and injected utility JSON | Remove two plumbing cases | `verify:asset-catalog`: real actool → assetutil → CLI and stdio MCP, including digest, full metadata, pagination and invalid input |
| Keyed archive positive binary/XML objects assembled with the same plist library as the reader | Remove two synthetic success cases | Captured Foundation XML plus reviewed complete object/reference graph golden; real Foundation binary/XML → CLI and stdio MCP |
| Generic contract schema/annotation/example loops and output-map inventory comparisons | Consolidate into one SDK wire test | Assert exported schemas, guidance, effect annotations and valid examples on the actual advertised MCP catalog |
| Ghidra function operation enum membership | Remove | Canonical catalog, provider handshake and actual operation roundtrips establish admission; malformed provider results remain covered |
| Asset page assertion pinning `10_001` | Remove | Invalid-page rejection through real MCP; malformed utility data still covered |
| Suggested prompt tool positions | Remove incidental ordering assertions | Keep authority, evidence, untrusted input and preparation constraints |
| Package “acceptance” assertions pinning CI matrix/shard values and fake launcher command arrays | Remove configuration-shaped cases | Actual package/install verifier remains; retain a compact release verification-before-publish and registry-canary static guard |
| Package failure classifier under acceptance | Move to package evaluation | Preserve all malformed/unrelated-failure rejection cases; do not call them E2E |
| Native value graph checks conditional on success | Strengthen | Require success before pagination/ambiguity assertions; unexpected errors can no longer pass |
| Native UI verifier treating permission denial as a positive pass | Separate outcomes | Default lane requires real capture/action success; opt-in permissions lane explicitly reports no positive E2E proof |
Ten previous test cases were removed or consolidated; three replacement
Vitest cases cover the captured goldens and release guard. The two real artifact
E2E scripts add product-level evidence outside the deterministic Vitest count.
## Exhaustive follow-up
The follow-up removes or consolidates another 31 test declarations, including
three redundant test files. Parameterized declarations and executed cases have
different counts; the figures above count source declarations. It also repairs
assertions that could pass without exercising their stated behavior.
| Area | Cleanup | Stronger retained evidence |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Configuration | Merge duplicate target/loader tests and preserve custom launcher, explicit provider and actionable error diagnostics | Independent malformed inputs; each capability flag is rejected separately |
| Browser scenarios | Remove fake launch success and expression-selected artifact success | Real Chromium scenarios, actual artifacts, CLI capture and owned/external cleanup in `verify:browser` |
| CLI policy | Remove duplicate confirmation helper cases | Compiled CLI proves project files stay unchanged until `--yes`; distinct interactive choices remain |
| Provider capabilities | Remove numerical catalog freezes; consolidate Hopper byte-read discovery | Exact canonical capability identities, admission, mutation effects and detached descriptors |
| Evidence ledger | Merge reordered-key checks into serialized bundle import; remove one-record quota claims | Atomic import, conflicts, residual revisions, actual large collections and byte regressions |
| MCP batching and concurrency | Replace maximum concurrency and empty indistinguishable responses | Every distinct ordered batch result; concurrently gated requests complete in reverse order with matching payloads |
| MCP inventories and graphs | Remove traversal/inventory call order and redundant catalog/prose checks | Exact complete cyclic graph, full inline inventories, canonical SDK catalog and all-handler roundtrips |
| Authority and lifecycle | Attempt the refused Ghidra mutation; require successful setup, call and switch; close twice | Typed refusal with zero mutation dispatch, restored target calls and exact owned-resource cleanup |
| Process and provider output | Remove incidental environment read and command option order/defaults | PID/ownership validation, exact disabled-policy failure and complete decoded response above 10 MiB |
| Conformance and CI guards | Merge identical replay runs; remove duplicate missing-row fault, trivial hash/self equality and incidental worker/step counts | Distinct faulty evidence, digest order, actual test discovery, file existence and release/runner authority |
| Pure schemas | Remove fabricated comparison-dimension injection and impossible long boundary success | Real artifact comparison and SDK coverage evaluation with returned boundary identity |
The removed long boundary selector test only parsed a 1,000-character request;
workspace boundary identities admit at most 200 characters, so that positive
could never represent a successful evaluation. The actual MCP evaluation stays
covered. One unused ledger serialization helper was also retired after removing
its only test consumer; no CLI or MCP contract changed.
No further immediately safe pruning was identified in the final review. Retained
replacement candidates below are coverage gaps, rather than permission to delete
unique tests. Test-only domain scaffolding also stays pending a separate decision
about the corresponding source behavior; deleting those tests alone would leave
unverified code and fail unused-export checks.
## Stronger replacements from the closing pass
Three more synthetic success cases were removed. Native value composition now
runs through the production CLI and a separate stdio MCP subprocess with real
Ghidra, using a source-built native fixture. It checks argument/parameter/return
relationships, the fixture's global operand, authenticated Evidence, upstream
and workflow analysis profiles, identical CLI/MCP graphs, discovery availability
and successful MCP session closure. Ambiguity, cancellation, target changes,
missing flow and bounded-work regressions remain in the focused suite.
That replacement found missing CLI workflow routing for `trace_native_values`
and missing workflow profile classification. The routing and discovery now use
the composed operation's actual provider prerequisites; workflow Evidence commits
to the upstream Ghidra profile.
The lone hand-built client-document equality test is replaced by a real TOML
filesystem transaction: reversed environment keys leave the original bytes
unchanged and create no backup. A real ZIP entry with an application root longer
than 5,000 characters replaces injected schema-only positive data. Inventory
completeness tests partition actual scans into authenticated partial observations
and require exact assembled results and every source citation, including more
than 100 citations. These are consumer merge tests, not provider pagination
claims. Replay aggregation now covers all four statuses and retains actual runner
diagnostics; it does not claim to execute a real sandbox. Retained pixel-metric
fixtures now write correct PNG chunk checksums.
The all-zero screenshot buffer is replaced by a real Chromium PNG of 9,467,940
bytes, transported intact through CLI and stdio MCP and decoded by the production
pixel comparator. The verifier configures the SDK client's receive buffer because
its 10 MiB default is smaller than the inline JSON response. This does not claim
large comparison-request transport coverage. A real SDK initialize request,
captured during a successful production stdio handshake, replaces the JSON-RPC
decoder's field-count-only oracle with exact field/value expectations.
The closing pass also removed assertions tied to internal call order, defaults,
private cache sizes, duplicated polling and fabricated schema extensions. Unique
malformed-input, security-capacity and cleanup regressions remain.
## Real process regression found
The full suite exposed a renderer ordering race. Publishing a terminal output
journal entry could synchronously trigger a reactive resize before the preceding
output was queued for rendering. Queue that output first. The existing real
source-owned interactive PTY test now also requires the first resized frame to
contain the output that triggered the resize, preserving the original event
order rather than sorting or changing timestamps to conceal it.
## Coverage kept
Malformed archives, broken and cyclic references, unsupported metadata pointers,
relative Objective-C method layouts, ambiguous call candidates, digest mismatch,
cancellation, host permission denial, bounded output and owned-resource cleanup
remain covered. These cases provide evidence beyond a successful real-tool run.
The hand-assembled relative method fixture stays until compiler-produced
relative-list fixtures establish equivalent assertions.
## Remaining priorities
1. Extend the real Ghidra CLI/stdio MCP journey from value tracing to dispatch
and UI handlers before pruning their hand-authored composition success cases.
Native value tracing now establishes full transport parity.
2. Real binary dispatch and nib → full product transports, with normalized
semantic goldens. The compiler-backed readers already run in the new Apple
CI lane; synthetic pointer/layout rejection tests remain useful.
3. An approved interactive macOS runner for native UI capture/actions, including
stdio MCP scenarios. Hosted runners without permissions cannot supply this
proof. No arbitrary application's windows should be opened for this lane.
4. Real macOS/Linux Ghidra CI runners with bring-your-own installations. This
change adds Apple artifact CI without installing Java or Ghidra.
5. Classify fake-runner replay/application acceptance accurately and replace
repeated happy paths only after real sandbox journeys assert the same claims.
The replay workflow's allow-unavailable mode must not imply executed replay.
6. Replace repeated-array schema ceiling tests with real producer/transport
regressions before deleting them. Existing integration tests do not yet
establish their large-input guarantees.
Some static guards protect irreversible release authority rather than product
behavior. Keep these few guards explicit and separate from E2E evidence.
## Verification after the expanded audit
- `npm run check:pr`: 351 files passed; 1,794 tests passed and two skipped.
Typecheck, lint, formatting, unused-code and generated-document gates passed.
- `npm run check:fast`: passed; 70 lint warnings and zero errors.
- `npm run verify:browser`: actual Chromium observation and scenario CLI routes,
retained artifacts, owned/external lifecycle checks and large CLI/stdio MCP PNG
capture passed.
- `npm run verify:ghidra`: macOS ARM64 with Ghidra 12.1.4/JDK 21 passed, including
real native value tracing through CLI and stdio MCP: 171 nodes, 298 edges and
six decompilations.
- `npm run verify:asset-catalog`: real actool/assetutil → CLI and stdio MCP,
complete records, pagination and malformed input checks passed.
- `npm run verify:keyed-archive`: real Foundation binary/XML → CLI and stdio MCP,
full graph golden, shared/cyclic identity and malformed scope checks passed.
- `npm run verify:fixtures`: seven source-built host fixtures, source/artifact
digests and exhaustive inventory checks passed.
No Hopper windows were opened for this audit. The closing pass runs real Ghidra
on macOS ARM64; earlier Hopper evidence remains separate. No new hosted CI success
is claimed.
+44 -3
View File
@@ -85,8 +85,26 @@ regressions until a real fixture establishes equivalent coverage.
decoding. Its SDK client explicitly permits the larger inline JSON response;
this lane does not establish large image-comparison request transport coverage.
The [test suite audit](test-suite-audit.md) records the pruning decisions,
replacement evidence and remaining priorities.
`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).
## Real-toolchain verification lanes
@@ -97,6 +115,11 @@ commands. Check prerequisites before starting expensive work and name the
missing command, target, and lane in any failure message. A lane must not imply
that a host or target is covered when it was skipped.
Provider admission accepts Ghidra 12.1.x and the JDK range declared by that
installation (`application.java.min` through `application.java.max`). Current
12.1 releases require JDK 21 or newer and set no maximum. The lanes below still
prove behavior on the verified Ghidra 12.1.4 and JDK 21 build.
| Ghidra lane | Supported runner/target | Additional local tools |
| ------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `npm run verify:ghidra` | Linux x64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
@@ -158,6 +181,8 @@ kept under ignored `_reference/`. No Gradle build, Android SDK, emulator or
application execution is required. The lane compares real CLI/MCP package,
class search, class inventory, method decompilation and incoming references.
See [Android analysis](android-analysis.md) for boundaries and resource budgets.
Authenticated IPA inventory projection is documented in
[Apple application analysis](apple-application-analysis.md).
Synthetic producer regressions run independently:
@@ -378,9 +403,25 @@ for the exact ABI, authority, graph and observation boundaries.
`npm run fixtures:firmware` uses existing Python 3 and a host C compiler to make
an ignored gzip/USTAR firmware fixture and independent offset/hash oracle.
`npm run verify:firmware` requires caller-supplied Binwalk 3.1.0, Unblob 26.6.4
and util-linux prlimit on Linux. It verifies CLI/MCP parity, selected ranges,
and util-linux prlimit on Linux. The provider also accepts other 3.1.x and
26.6.x builds and reports them as unverified; this lane proves the audited
releases. It verifies CLI/MCP parity, selected ranges,
unknown chunks, depth limits and extracted child digests. The optional
`REA_FIRMWARE_VERIFY_EXT4=1` lane requires existing mke2fs/debugfs; the separate
`REA_FIRMWARE_VERIFY_GHIDRA=1` lane checks a selected host ELF through real Ghidra.
Neither optional toolchain is a base-lane prerequisite. See
[firmware analysis](firmware-analysis.md) for limits and unverified formats.
### JavaScript source recovery
Build once with `npm run build:cached`, then run `npm run verify:javascript:recovery`.
This focused lane requires Linux x64, util-linux `prlimit`,
`REA_WAKARU_COMMAND` pointing to the official Wakaru 1.13.0 Linux x64 binary,
and `REA_JAVASCRIPT_FIXTURE_TOOLS` pointing to an isolated npm prefix containing
esbuild 0.25.10 and webpack 5.101.3. No global installation is required.
The lane compiles source-owned fixtures, exercises CLI and stdio MCP, verifies
published bytes and provenance, feeds recovered modules into existing analysis,
and compares a finite set of known fixture results. It does not establish
arbitrary recovered-application equivalence. CI installs these prerequisites only
in `.github/workflows/real-javascript-recovery.yml`; the existing `real-browser`
lane uses real Chrome for browser capture and website workflows.
@@ -1,5 +1,5 @@
{
"manifest_id": "ecm_fbd47cbd3815d7e252ebcb60c4d086dcc1fc6b63f020e83513e28bf4d94c732c",
"manifest_id": "ecm_ae079950d6d61294a0940a34379f6a3c1cb26e249557ca62ba72aa1e755886e1",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "b317babebb82157b46b4dff698a170045490bcb21aaf9af582e20908ba2bb998"
"sha256": "ee03e19730d1dde6ab46b767120b090074f59166868cfd9f547e2b6d217627b7"
}
],
"claims": [
+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.
+8 -4
View File
@@ -1,8 +1,8 @@
# Windows Ghidra P0
This guide describes repository main. npm 4.0.1 does not include the Windows
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 a published package.
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
@@ -13,8 +13,8 @@ additional permission flag or degraded mode is required.
- Windows 10+ x64 with fixed local NTFS target and temporary volumes;
- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+;
- an operator-installed official Ghidra 12.1.4 distribution;
- a 64-bit full JDK 21;
- an operator-installed Ghidra 12.1.x distribution (verified with 12.1.4);
- a 64-bit full JDK inside that installation's declared Java range (JDK 21 or newer, with no maximum, for current 12.1 releases);
- an explicit native, non-managed, non-DLL x86-64 PE application; and
- the 25 read-only Ghidra inventory, memory, and function-analysis operations.
@@ -32,6 +32,10 @@ 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
```powershell
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "./node_modules/knip/schema.json",
"entry": ["src/main.ts", "src/cli.ts", "src/contracts/errorSchemas.ts"],
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
"ignoreDependencies": ["electron"],
"ignoreDependencies": ["electron", "esbuild", "webpack"],
"ignoreExportsUsedInFile": true,
"rules": {
"files": "error",
+5
View File
@@ -66,6 +66,11 @@ 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
+10
View File
@@ -19,6 +19,16 @@
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;
+1
View File
@@ -14,6 +14,7 @@ static std::wstring hex(const unsigned char* bytes, size_t count) {
}
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')),
+3 -2
View File
@@ -44,7 +44,8 @@ static void pipe(Handle& read, Handle& write) {
std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::wstring& commandLine,
const std::wstring& cwd, const std::vector<std::wstring>& environment) {
require(command.size() >= 3 && command[1] == L':' && command[2] == L'\\',
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();
@@ -102,7 +103,7 @@ std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::ws
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(command.c_str(), line.data(), nullptr, nullptr, TRUE,
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);
+11 -24
View File
@@ -31,7 +31,7 @@
"playwright-core": "1.58.2",
"plist": "5.0.0",
"semver": "7.8.5",
"smol-toml": "1.7.0",
"smol-toml": "1.9.0",
"write-file-atomic": "7.0.0",
"ws": "8.21.0",
"zod": "4.4.3"
@@ -2659,9 +2659,9 @@
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.9.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
"integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==",
"version": "0.9.12",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.12.tgz",
"integrity": "sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==",
"license": "MIT",
"engines": {
"node": ">=14.6"
@@ -2846,15 +2846,15 @@
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
"integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/buffer": {
@@ -3892,19 +3892,6 @@
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/knip/node_modules/smol-toml": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.0.tgz",
"integrity": "sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">= 18"
},
"funding": {
"url": "https://github.com/sponsors/cyyynthia"
}
},
"node_modules/knip/node_modules/yaml": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz",
@@ -5196,9 +5183,9 @@
}
},
"node_modules/smol-toml": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz",
"integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.0.tgz",
"integrity": "sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">= 18"
+10 -4
View File
@@ -84,8 +84,8 @@
"build:cached": "turbo run build",
"format": "oxfmt .",
"format:check": "oxfmt --check .",
"lint": "oxlint",
"lint:fix": "oxlint --fix",
"lint": "oxlint && npm run verify:module-boundaries",
"lint:fix": "oxlint --fix && npm run verify:module-boundaries",
"knip": "knip",
"jscpd": "jscpd src/ tests/",
"scan:todos": "node scripts/scan-todos.mjs",
@@ -132,6 +132,8 @@
"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",
@@ -173,7 +175,11 @@
"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:ghidra:nativeaot": "npm run build:cached && node scripts/verify-real-ghidra-nativeaot.mjs",
"verify:browser:scripts": "npm run build:cached && node scripts/verify-browser-scripts.mjs",
"verify:test-discovery": "node scripts/verify-test-discovery.mjs",
"verify:javascript:recovery": "node scripts/verify-javascript-recovery.mjs",
"verify:module-boundaries": "node scripts/verify-module-boundaries.mjs"
},
"lint-staged": {
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
@@ -205,7 +211,7 @@
"playwright-core": "1.58.2",
"plist": "5.0.0",
"semver": "7.8.5",
"smol-toml": "1.7.0",
"smol-toml": "1.9.0",
"write-file-atomic": "7.0.0",
"ws": "8.21.0",
"zod": "4.4.3"
+45 -7
View File
@@ -20,14 +20,17 @@ const source = resolve(
const install = process.env.GHIDRA_INSTALL_DIR;
if (!install)
throw new Error(
"NativeAOT build lane requires existing GHIDRA_INSTALL_DIR (Ghidra 12.1.4).",
"NativeAOT build lane requires existing GHIDRA_INSTALL_DIR (Ghidra 12.1.x).",
);
const properties = await readFile(
join(install, "Ghidra/application.properties"),
"utf8",
);
if (!/^application.version=12\.1\.4\s*$/m.test(properties))
throw new Error("NativeAOT build lane requires Ghidra 12.1.4.");
const ghidraVersion = propertyValue(properties, "application.version");
if (!ghidraReleaseLineAccepted(ghidraVersion))
throw new Error(
`NativeAOT build lane accepts Ghidra 12.1.x (verified with 12.1.4). Observed application.version=${ghidraVersion ?? "missing"}.`,
);
const output = resolve(
process.env.REA_NATIVEAOT_BUILD_ROOT ??
join(root, "_reference/nativeaot-integration/extension"),
@@ -47,9 +50,11 @@ const run = (command, args) => {
return result.stdout.trim();
};
const javac = java ? join(java, "javac") : "javac";
if (!/^javac 21(?:\.|$)/u.test(run(javac, ["-version"])))
const javacBanner = capture(javac, ["-version"]);
const javacMajor = Number(/^javac (\d+)/mu.exec(javacBanner)?.[1]);
if (!Number.isInteger(javacMajor) || javacMajor < 21)
throw new Error(
"NativeAOT build lane requires an existing JDK 21 javac; REA does not install or upgrade Java.",
`NativeAOT build lane requires javac 21 or newer and compiles with --release 21. Observed: ${javacBanner || "no version"}. REA does not install or upgrade Java.`,
);
const revision = run("git", ["-C", source, "rev-parse", "HEAD"]);
if (
@@ -93,6 +98,8 @@ try {
run(javac, [
"-J-Xmx512m",
"-J-XX:ActiveProcessorCount=1",
"--release",
"21",
`@${join(output, "javac.args")}`,
]);
await writeFile(
@@ -119,7 +126,7 @@ try {
jar,
sha256,
upstream_revision: revision,
ghidra_version: "12.1.4",
ghidra_version: ghidraVersion,
integration_api: 1,
},
null,
@@ -131,10 +138,41 @@ try {
jar,
sha256,
upstream_revision: revision,
ghidra_version: "12.1.4",
ghidra_version: ghidraVersion,
integration_api: 1,
}),
);
} finally {
await rm(classes, { recursive: true, force: true });
}
/** Read one application.properties value. Empty and missing values stay unknown. */
function propertyValue(content, name) {
for (const line of content.split(/\r?\n/u)) {
const separator = line.indexOf("=");
if (separator < 0 || line.slice(0, separator).trim() !== name) continue;
const value = line.slice(separator + 1).trim();
return value.length === 0 ? null : value;
}
return null;
}
/** Same 12.1 line as GhidraInstallationPolicy, including a distro suffix. */
function ghidraReleaseLineAccepted(raw) {
const match = /^(\d+)\.(\d+)(?:\.(\d+))?([-+].*)?$/u.exec(raw ?? "");
return match !== null && Number(match[1]) === 12 && Number(match[2]) === 1;
}
function capture(command, args) {
const result = spawnSync(command, args, {
cwd: root,
encoding: "utf8",
timeout: 120_000,
maxBuffer: 4 * 1024 * 1024,
});
if (result.error || result.status !== 0)
throw new Error(
`NativeAOT build prerequisite/action ${command} failed: ${result.error?.message ?? ""}\n${result.stdout ?? ""}${result.stderr ?? ""}`,
);
return `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
}
+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),
),
),
};
}
+5 -1
View File
@@ -52,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 });
@@ -0,0 +1,3 @@
import { greet, total } from "./helpers.mjs";
globalThis.recoveryFixture = { greet, total };
@@ -0,0 +1,6 @@
export function greet(name) {
return `Hello ${name ?? "world"}`;
}
export function total(items) {
return items.reduce((sum, value) => sum + value, 0);
}
+4 -11
View File
@@ -7,9 +7,7 @@ import { McpServer } from "@modelcontextprotocol/server";
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managedWorkflowToolContracts.js";
import { ArtifactProvider } from "../dist/artifacts/ArtifactProvider.js";
import { ManagedStaticProvider } from "../dist/dotnet/ManagedStaticProvider.js";
import { NativeMacOSProvider } from "../dist/native/NativeMacOSProvider.js";
import { auxiliaryAnalysisProviderDeclarations } from "../dist/composition/auxiliaryAnalysisProviders.js";
import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
@@ -88,14 +86,9 @@ async function sdkToolCatalog() {
await Promise.allSettled([client.close(), server.close()]);
}
}
const auxiliaryProviders = [
new ArtifactProvider(CATALOG_PLATFORM),
new NativeMacOSProvider(undefined, CATALOG_PLATFORM),
new ManagedStaticProvider(),
].map((provider) => ({
identity: provider.identity(),
capabilities: provider.capabilities(),
}));
const auxiliaryProviders = auxiliaryAnalysisProviderDeclarations(
CATALOG_PLATFORM,
).map(({ identity, capabilities }) => ({ identity, capabilities }));
const canonicalizeJson = (value) => {
if (Array.isArray(value)) return value.map(canonicalizeJson);
@@ -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();
}
}
+11 -2
View File
@@ -204,11 +204,20 @@ export function assertScenarioCapture(capture) {
throw new Error(
"Scenario event URL did not report declared-secret redaction",
);
const unfinished = capture.events.items.filter(
({ kind }) => kind === "request-unfinished",
);
// This fixture polls continuously. A request crossing the capture cutoff
// makes later activity unknown even when no response body was selected.
const expectedMissing = unfinished.length === 0 ? [] : ["events"];
if (
capture.events.items.length === 0 ||
capture.completeness.equality_eligible !== true
capture.completeness.equality_eligible !== (unfinished.length === 0) ||
JSON.stringify(capture.completeness.missing_sections) !==
JSON.stringify(expectedMissing) ||
capture.completeness.truncated_sections.length !== 0
)
throw new Error(
"Scenario launch did not produce complete runtime evidence",
`Scenario launch completeness disagrees with its capture cutoff: ${JSON.stringify({ completeness: capture.completeness, unfinished })}`,
);
}
@@ -0,0 +1,70 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { webScriptExportResultSchema } from "../../dist/domain/webScriptExport.js";
import { javascriptApplicationAnalysisResultSchema } from "../../dist/domain/javascriptApplicationAnalysis.js";
/** Verify exact exported bytes and the independently authored source manifest. */
export const assertWebScriptExport = async (evidence, capture, assets) => {
const result = webScriptExportResultSchema.parse(evidence.normalized_result);
assert.equal(result.source_evidence_id, capture.evidence_id);
assert.deepEqual(evidence.evidence_links, [capture.evidence_id]);
const manifest = await readFile(result.manifest.path);
assert.equal(
createHash("sha256").update(manifest).digest("hex"),
result.manifest.sha256,
);
const { manifest: descriptor, ...inline } = result;
assert.equal(descriptor.bytes, manifest.length);
assert.deepEqual(JSON.parse(manifest.toString()), inline);
assert.ok(result.analysis_input);
assert.ok(result.scripts.length > 0);
for (const script of result.scripts) {
assert.equal(script.content.state, "exported");
const bytes = await readFile(
join(result.analysis_input.input_path, script.content.relative_path),
);
assert.equal(
createHash("sha256").update(bytes).digest("hex"),
script.content.sha256,
);
if (assets) {
const url = new URL(script.url);
assert.equal(
bytes.toString(),
assets.get(`${url.pathname}${url.search}`),
`Wrong bytes for ${script.url}`,
);
}
}
if (assets) {
assert.equal(result.scripts.length, assets.size);
const variants = result.scripts.filter(({ url }) =>
url.includes("/variant.js?"),
);
assert.equal(variants.length, 2);
assert.ok(variants.every(({ content }) => content.layout === "isolated"));
assert.equal(
new Set(variants.map(({ content }) => content.relative_path)).size,
2,
);
}
return result;
};
/** Verify existing application analysis and an observed relative module layout. */
export const assertExportedScriptAnalysis = (evidence, modules, imports) => {
const result = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
);
assert.equal(result.statistics.parsed_javascript_files, modules);
if (imports)
assert.ok(
result.graph.edges.some(
({ relation, properties }) =>
relation === "imports" &&
properties.specifier === "./lib/dep.js" &&
properties.resolution_status === "resolved",
),
);
};
+192
View File
@@ -0,0 +1,192 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../../dist/domain/evidence.js";
import { startBrowserScriptSite } from "../fixtures/browser-script-site.mjs";
import {
assertWebScriptExport,
assertExportedScriptAnalysis,
} from "./browser-script-export-assertions.mjs";
import { mcpTextValue } from "./mcp-verifier-results.mjs";
/** Verify capture → export → existing analysis through real CLI and stdio MCP. */
export async function verifyBrowserScriptExport(
executable,
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
attachment,
) {
const site = await startBrowserScriptSite();
const root = await mkdtemp(join(tmpdir(), "rea-browser-script-export-"));
const env = {
...process.env,
REA_LOG_LEVEL: "silent",
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
};
const cli = exportCli(entrypoint, env);
const scenario = scriptCaptureScenario(executable, site.origin);
const save = (name, capture) => saveCapture(root, name, capture);
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({ name: "web-script-real-e2e", version: "1" });
try {
const cliCapture = await cli(
"capture-browser-scenario",
JSON.stringify(scenario),
);
const cliInput = await save("cli", cliCapture);
const counts = new Map(site.counts);
const cliExport = await cli(
"export-web-scripts",
cliInput.capture_path,
cliInput.output_directory,
);
const cliResult = await assertWebScriptExport(
cliExport,
cliCapture,
site.assets,
);
await cli(
"analyze-javascript-application",
cliResult.analysis_input.input_path,
).then((value) => assertExportedScriptAnalysis(value, 4, true));
assert.deepEqual(
site.counts,
counts,
"Export/analysis refetched a captured asset",
);
await client.connect(transport);
const call = async (name, args) => {
const result = await client.callTool({ name, arguments: args });
assert.notEqual(result.isError, true, mcpTextValue(result));
return parseEvidence(JSON.parse(mcpTextValue(result)).evidence);
};
const mcpCapture = await call("capture_browser_scenario", scenario);
const mcpInput = await save("mcp", mcpCapture);
const mcpCounts = new Map(site.counts);
const mcpResult = await assertWebScriptExport(
await call("export_web_scripts", mcpInput),
mcpCapture,
site.assets,
);
assertExportedScriptAnalysis(
await call("analyze_javascript_application", mcpResult.analysis_input),
4,
true,
);
assert.deepEqual(
site.counts,
mcpCounts,
"MCP export/analysis refetched a captured asset",
);
const passive = attachment
? await verifyPassiveExport(attachment, { cli, call, save })
: false;
return {
cli: true,
stdio_mcp: true,
active_scripts: 4,
passive,
exact_bytes: true,
manifest_readback: true,
relative_import_resolved: true,
query_variants_isolated: true,
implicit_refetches: 0,
};
} finally {
await client.close();
await transport.close();
await site.close();
await rm(root, { recursive: true, force: true });
}
}
const verifyPassiveExport = async (attachment, { cli, call, save }) => {
const input = {
...attachment,
observation_ms: 100,
include_script_sources: true,
};
const page = await call("inspect_web_page", input);
const pageInput = await save("passive", page);
const exported = await assertWebScriptExport(
await call("export_web_scripts", pageInput),
page,
);
assertExportedScriptAnalysis(
await call("analyze_javascript_application", exported.analysis_input),
exported.scripts.length,
false,
);
const pageCli = await cli(
"inspect-web-page",
attachment.cdp_endpoint,
attachment.target_id,
"--include-script-sources",
"--observation-ms",
"100",
);
const pageCliInput = await save("passive-cli", pageCli);
const pageCliResult = await assertWebScriptExport(
await cli(
"export-web-scripts",
pageCliInput.capture_path,
pageCliInput.output_directory,
),
pageCli,
);
assertExportedScriptAnalysis(
await cli(
"analyze-javascript-application",
pageCliResult.analysis_input.input_path,
),
pageCliResult.scripts.length,
false,
);
return true;
};
const exportCli =
(entrypoint, env) =>
async (...args) => {
const { stdout } = await promisify(execFile)(
process.execPath,
[entrypoint, ...args, "--json"],
{ env, timeout: 60000, maxBuffer: 32 * 1024 * 1024 },
);
return parseEvidence(JSON.parse(stdout));
};
const saveCapture = async (root, name, capture) => {
const capturePath = join(root, `${name}.json`);
await writeFile(capturePath, JSON.stringify(capture));
return {
capture_path: capturePath,
output_directory: join(root, `${name}-export`),
};
};
const scriptCaptureScenario = (executable, origin) => ({
browser: { mode: "launch", executable_path: executable },
start_url: { url: origin },
actions: [
{
step_id: "ready",
action: "wait_for",
locator: { kind: "css", selector: '[data-ready="true"]' },
state: "visible",
timeout_ms: 10000,
},
],
capture: { network: { response_body: true } },
});
+37 -2
View File
@@ -22,7 +22,7 @@ export function assertObservation(result, origin) {
assertObservationInventory(result, origin);
assertObservationEvents(result);
assertObservationPrivacy(result);
assertObservationMetadata(result);
assertObservationMetadata(result, origin);
}
function assertObservationInventory(result, origin) {
@@ -86,9 +86,27 @@ function assertObservationPrivacy(result) {
);
}
function assertObservationMetadata(result) {
function assertObservationMetadata(result, origin) {
assertFormDestinations(result);
if (
!result.metadata.dom_urls.some(
({ attribute, url }) =>
attribute === "href" && url === `${origin}/assets/guide`,
)
)
throw new Error(
"Real Chrome relative DOM URL did not use the document base",
);
if (!result.storage.local_storage_keys.includes("rea-storage-key"))
throw new Error("Real Chrome local-storage key inventory was missing");
if (
result.storage.content_fingerprints.filter(
({ scope }) => scope === "cookie",
).length !== 2
)
throw new Error(
"Real Chrome root and current-page cookie fingerprints were missing",
);
if (!result.storage.indexed_db_names.includes("rea-browser-db"))
throw new Error("Real Chrome IndexedDB name inventory was missing");
if (!result.storage.cache_names.includes("rea-browser-cache"))
@@ -104,6 +122,23 @@ function assertObservationMetadata(result) {
throw new Error("Real Chrome safe response metadata was missing");
}
/** Assert the fixture's empty form destinations retain the current document URL. */
export function assertFormDestinations(
result,
documentUrl = result.target.url,
) {
const destinations = result.metadata.dom_urls.filter(
({ attribute }) => attribute === "action" || attribute === "formaction",
);
if (
destinations.length !== 4 ||
destinations.some(({ url }) => url !== documentUrl)
)
throw new Error(
`Real Chrome form destinations did not use the current document URL: ${JSON.stringify({ expected: documentUrl, target: result.target.url, destinations })}`,
);
}
/** Assert real-browser static bundle and source-map findings. */
export function assertBundleAnalysis(result) {
if (
+32 -2
View File
@@ -64,7 +64,12 @@ export const toolFamilyCatalog = (sources) => {
{
id: "application",
surface: "application-workflow",
contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS,
contracts: [
...sources.applicationContracts.APPLICATION_TOOL_CONTRACTS,
...sources.webScriptContracts.WEB_SCRIPT_TOOL_CONTRACTS,
...sources.javascriptRecoveryContracts
.JAVASCRIPT_RECOVERY_TOOL_CONTRACTS,
],
},
{
id: "session",
@@ -182,7 +187,32 @@ export const providerCatalog = (sources) => {
{
identity:
sources.artifactProviders.JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER,
contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS,
contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS.filter(
({ name }) =>
name !== "project_android_application_graph" &&
name !== "project_apple_application_graph",
),
},
{
identity: sources.artifactProviders.ANDROID_APPLICATION_PROVIDER,
contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS.filter(
({ name }) => name === "project_android_application_graph",
),
},
{
identity: sources.artifactProviders.APPLE_APPLICATION_PROVIDER,
contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS.filter(
({ name }) => name === "project_apple_application_graph",
),
},
{
identity: sources.artifactProviders.WEB_SCRIPT_EXPORT_PROVIDER,
contracts: sources.webScriptContracts.WEB_SCRIPT_TOOL_CONTRACTS,
},
{
identity: sources.javascriptRecoveryProvider.WAKARU_PROVIDER_IDENTITY,
contracts:
sources.javascriptRecoveryContracts.JAVASCRIPT_RECOVERY_TOOL_CONTRACTS,
},
]
.map(({ identity, contracts }) => ({
+5 -1
View File
@@ -54,11 +54,15 @@ const SOURCE_PATHS = {
javascriptRuntimeObservationContracts:
"dist/contracts/javascriptRuntimeObservationToolContracts.js",
applicationContracts: "dist/contracts/applicationToolContracts.js",
webScriptContracts: "dist/contracts/webScriptToolContracts.js",
javascriptRecoveryContracts:
"dist/contracts/javascriptRecoveryToolContracts.js",
javascriptRecoveryProvider: "dist/javascript/recovery/WakaruRelease.js",
supportedClients: "dist/application/SupportedClients.js",
hopperProvider: "dist/hopper/HopperProvider.js",
ghidraProvider: "dist/ghidra/GhidraProvider.js",
idaProvider: "dist/ida/IdaProviderCapabilities.js",
nativeProvider: "dist/native/NativeMacOSProvider.js",
nativeProvider: "dist/native/NativeMacOSProviderMetadata.js",
artifactProviders: "dist/application/InvestigationProviders.js",
browserProvider: "dist/browser/CdpBrowserProvider.js",
browserScenarioProvider: "dist/browser/PlaywrightBrowserScenarioProvider.js",
@@ -0,0 +1,104 @@
import { createRequire } from "node:module";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
/** Compile source-owned fixtures using explicitly supplied, pinned toolchains. */
export async function buildRecoveryFixtures(root, prefix) {
if (!prefix)
throw new Error(
"verify:javascript:recovery requires REA_JAVASCRIPT_FIXTURE_TOOLS pointing to the isolated esbuild/webpack installation",
);
const require = createRequire(join(prefix, "package.json"));
const esbuild = require("esbuild");
const webpack = require("webpack");
const webpackVersion = require("webpack/package.json").version;
if (esbuild.version !== "0.25.10" || webpackVersion !== "5.101.3")
throw new Error(
"Fixture compilers must be esbuild 0.25.10 and webpack 5.101.3",
);
const source = fileURLToPath(
new URL("../fixtures/javascript-recovery/entry.mjs", import.meta.url),
);
const webpackOutput = join(root, "webpack");
await mkdir(webpackOutput);
await new Promise((resolve, reject) => {
const compiler = webpack({
mode: "production",
entry: source,
parallelism: 1,
optimization: { concatenateModules: false },
output: { path: webpackOutput, filename: "bundle.js" },
});
compiler.run((error, stats) => {
compiler.close((closeError) => {
if (error || closeError) return reject(error ?? closeError);
if (!stats || stats.hasErrors())
return reject(new Error(stats?.toString() ?? "No webpack stats"));
resolve();
});
});
});
const esbuildPath = join(root, "esbuild.js");
await esbuild.build({
entryPoints: [source],
outfile: esbuildPath,
bundle: true,
format: "iife",
minify: true,
logLevel: "silent",
});
// One self-contained source fixture avoids unresolved imports in the oracle.
const helperSource = await readFile(
fileURLToPath(
new URL("../fixtures/javascript-recovery/helpers.mjs", import.meta.url),
),
"utf8",
);
const plain = await esbuild.transform(
helperSource.replaceAll("export ", "") +
"\nglobalThis.recoveryFixture = {greet, total};",
{ minify: true },
);
const minifiedPath = join(root, "minified.js");
await writeFile(minifiedPath, plain.code);
if (plain.code.length === 0)
throw new Error("Expected a real minifier output");
return {
files: [
{
name: "minified",
path: minifiedPath,
expectedFormat: "unknown",
expectedModules: 1,
},
{
name: "webpack",
path: join(webpackOutput, "bundle.js"),
expectedFormat: "webpack5",
expectedModules: 2,
},
{
name: "esbuild-scope-hoisted",
path: esbuildPath,
expectedFormat: "unknown",
expectedModules: 1,
},
{
name: "heuristic-aggressive",
path: minifiedPath,
expectedFormat: "unknown",
expectedModules: 1,
options: { extraction_mode: "heuristic", rewrite_level: "aggressive" },
},
{
name: "inspection-minimal",
path: join(webpackOutput, "bundle.js"),
expectedFormat: "webpack5",
expectedModules: 2,
options: { extraction_mode: "inspection", rewrite_level: "minimal" },
},
],
toolchains: { esbuild: esbuild.version, webpack: webpackVersion },
};
}
+18
View File
@@ -0,0 +1,18 @@
/** One dependency crossing a settled source ownership boundary. */
export interface ModuleBoundaryViolation {
readonly file: string;
readonly line: number;
readonly specifier: string;
readonly target: string;
readonly boundary:
| "pure-layer"
| "application-composition"
| "provider-construction";
}
/** Check resolved source ownership, including type imports and reexports. */
export function inspectModuleBoundaries(
file: string,
source: string,
root: string,
): readonly ModuleBoundaryViolation[];
+100
View File
@@ -0,0 +1,100 @@
import { parse } from "@babel/parser";
import { traverseFast } from "@babel/types";
import { isAbsolute, relative, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const PURE_LAYERS = new Set(["domain", "contracts"]);
const MIGRATED_PROVIDER_ROOTS = new Set([
"android",
"firmware",
"ghidra",
"hopper",
"ida",
"javascript",
]);
const repositoryPath = (root, path) =>
relative(root, path).replaceAll("\\", "/");
const literalValue = (node) => {
if (node?.type === "StringLiteral") return node.value;
if (node?.type === "TemplateLiteral" && node.expressions.length === 0)
return node.quasis[0]?.value.cooked ?? undefined;
return undefined;
};
const importSpecifier = (node) => {
switch (node.type) {
case "ImportDeclaration":
case "ExportNamedDeclaration":
case "ExportAllDeclaration":
case "ImportExpression":
case "TSImportType":
return literalValue(node.source);
case "TSImportEqualsDeclaration":
return node.moduleReference.type === "TSExternalModuleReference"
? literalValue(node.moduleReference.expression)
: undefined;
default:
return undefined;
}
};
const sourceTarget = (file, specifier) => {
if (specifier.startsWith("file:")) return fileURLToPath(specifier);
if (isAbsolute(specifier)) return resolve(specifier);
return specifier.startsWith(".")
? fileURLToPath(new URL(specifier, pathToFileURL(file)))
: undefined;
};
const failedBoundary = (file, target) => {
if (!file.startsWith("src/") || !target.startsWith("src/")) return undefined;
const layer = file.split("/")[1];
const targetLayer = target.split("/")[1];
if (PURE_LAYERS.has(layer)) {
if (
(/^src\/[^/]+\//u.test(target) && !PURE_LAYERS.has(targetLayer)) ||
/^src\/(?:cli|main)\./u.test(target)
)
return "pure-layer";
}
if (file === "src/application/runtime.ts") return undefined;
if (layer === "application" && targetLayer === "composition")
return "application-composition";
if (
(layer === "application" || layer === "server") &&
(MIGRATED_PROVIDER_ROOTS.has(targetLayer) ||
/^src\/browser\/[^/]*Provider\.[^/]+$/u.test(target))
)
return "provider-construction";
return undefined;
};
/** Check resolved source ownership, including type imports and reexports. */
export const inspectModuleBoundaries = (file, source, root) => {
const absoluteFile = resolve(root, file);
const importer = repositoryPath(root, absoluteFile);
const tree = parse(source, {
sourceType: "module",
plugins: ["typescript"],
});
const violations = [];
traverseFast(tree, (node) => {
const specifier = importSpecifier(node);
if (specifier === undefined) return;
const resolved = sourceTarget(absoluteFile, specifier);
if (resolved === undefined) return;
const target = repositoryPath(root, resolved);
const boundary = failedBoundary(importer, target);
if (boundary !== undefined)
violations.push({
file: importer,
line: node.loc?.start.line ?? 1,
specifier,
target,
boundary,
});
});
return violations;
};
+14
View File
@@ -0,0 +1,14 @@
/** Compare actual Vitest discovery with repository-owned test paths. */
export function compareTestDiscovery(
expectedPaths: readonly string[],
discovery: unknown,
root: string,
): {
readonly missing: readonly string[];
readonly unexpected: readonly string[];
readonly duplicates: readonly {
readonly file: string;
readonly projects: readonly string[];
}[];
readonly discovered: number;
};
+41
View File
@@ -0,0 +1,41 @@
import { relative, resolve } from "node:path";
/** Compare actual Vitest discovery with repository-owned test paths. */
export const compareTestDiscovery = (expectedPaths, discovery, root) => {
if (!Array.isArray(discovery))
throw new TypeError("Vitest file discovery must be an array");
const projectsByFile = new Map();
for (const entry of discovery) {
if (
entry === null ||
typeof entry !== "object" ||
typeof entry.file !== "string" ||
entry.file.length === 0 ||
typeof entry.projectName !== "string" ||
entry.projectName.length === 0
)
throw new TypeError(
"Vitest discovery requires file and projectName strings",
);
const path = relative(root, resolve(root, entry.file)).replaceAll(
"\\",
"/",
);
const projects = projectsByFile.get(path) ?? [];
projects.push(entry.projectName);
projectsByFile.set(path, projects);
}
const expected = new Set(expectedPaths);
const sorted = (values) => [...values].sort();
return {
missing: sorted(expected).filter((path) => !projectsByFile.has(path)),
unexpected: sorted(projectsByFile.keys()).filter(
(path) => !expected.has(path),
),
duplicates: sorted(projectsByFile.keys()).flatMap((file) => {
const projects = projectsByFile.get(file);
return projects.length > 1 ? [{ file, projects: sorted(projects) }] : [];
}),
discovered: projectsByFile.size,
};
};
@@ -0,0 +1,53 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { chromium } from "playwright-core";
import { startBrowserVerifierSite } from "./fixtures/browser-verifier-site.mjs";
import { verifyBrowserDomDestinations } from "./lib/browser-dom-destinations-e2e.mjs";
const executable = process.env.REA_BROWSER_EXECUTABLE;
if (!executable)
throw new Error("verify:browser:dom requires REA_BROWSER_EXECUTABLE");
const profile = await mkdtemp(join(tmpdir(), "rea-browser-dom-"));
const site = await startBrowserVerifierSite();
let context;
try {
context = await chromium.launchPersistentContext(profile, {
executablePath: executable,
headless: true,
args: ["--remote-debugging-port=0"],
});
const page = context.pages()[0] ?? (await context.newPage());
await page.goto(`${site.origin}/app?selected=fixture#section`);
const native = await page.evaluate(() =>
[
...document.querySelectorAll("form,button[formaction],input[formaction]"),
].map((element) =>
element.tagName === "FORM" ? element.action : element.formAction,
),
);
assert.deepEqual(native, Array(4).fill(page.url()));
const session = await context.newCDPSession(page);
const { targetInfo } = await session.send("Target.getTargetInfo");
await session.detach();
const [port] = (
await readFile(join(profile, "DevToolsActivePort"), "utf8")
).split("\n");
const result = await verifyBrowserDomDestinations(
{
cdp_endpoint: `http://127.0.0.1:${port}`,
target_id: targetInfo.targetId,
},
process.argv[2],
page.url(),
);
process.stdout.write(
`${JSON.stringify({ ...result, browser: context.browser().version(), native_oracle: true, verified: true })}\n`,
);
} finally {
await context?.close();
await site.close();
await rm(profile, { recursive: true, force: true });
}
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env node
import { access } from "node:fs/promises";
import { verifyBrowserNetworkEvidence } from "./lib/browser-network-e2e.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
const executable = process.env.REA_BROWSER_EXECUTABLE;
if (executable === undefined || executable === "")
throw new Error(
"verify:browser:network requires REA_BROWSER_EXECUTABLE pointing to an installed Chrome-family browser",
);
await access(executable);
const run = createVerifierRun();
const proof = await verifyBrowserNetworkEvidence(executable, process.argv[2]);
process.stdout.write(
`${JSON.stringify({ ...proof, verifier_run: await completeVerifierRun(run), verified: true })}\n`,
);
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env node
import { access } from "node:fs/promises";
import { verifyBrowserScriptExport } from "./lib/browser-script-export-e2e.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
const executable = process.env.REA_BROWSER_EXECUTABLE;
if (executable === undefined || executable === "")
throw new Error(
"verify:browser:scripts requires REA_BROWSER_EXECUTABLE pointing to an installed Chrome-family browser",
);
await access(executable);
const run = createVerifierRun();
const proof = await verifyBrowserScriptExport(executable, process.argv[2]);
process.stdout.write(
`${JSON.stringify({ ...proof, verifier_run: await completeVerifierRun(run), verified: true })}\n`,
);
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../dist/domain/evidence.js";
import { javascriptRecoveryResultSchema } from "../dist/domain/javascriptRecovery.js";
import { buildRecoveryFixtures } from "./lib/javascript-recovery-fixtures.mjs";
import { mcpTextValue } from "./lib/mcp-verifier-results.mjs";
const OFFICIAL_BINARY_SHA256 =
"e8c7ca052974604197389f6ace7464e4dd24dce64e2730971521bd6606ec9c9e";
if (!process.env.REA_WAKARU_COMMAND)
throw new Error(
"Provide REA_WAKARU_COMMAND for the official Wakaru 1.13.0 Linux x64 binary",
);
const entrypoint =
process.argv[2] ?? fileURLToPath(new URL("./rea.mjs", import.meta.url));
const root = await mkdtemp(join(tmpdir(), "rea-real-recovery-"));
const environment = { ...process.env, REA_LOG_LEVEL: "silent" };
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env: environment,
stderr: "pipe",
});
const client = new Client({ name: "real-recovery", version: "1" });
const run = promisify(execFile);
const cli = async (...args) => {
const { stdout } = await run(
process.execPath,
[entrypoint, ...args, "--json"],
{ env: environment, timeout: 150000, maxBuffer: 32 * 1024 * 1024 },
);
return parseEvidence(JSON.parse(stdout));
};
const call = async (name, args) => {
const result = await client.callTool({ name, arguments: args }, undefined, {
timeout: 150000,
});
assert.notEqual(result.isError, true, mcpTextValue(result));
return parseEvidence(JSON.parse(mcpTextValue(result)).evidence);
};
const fingerprint = (bytes) => createHash("sha256").update(bytes).digest("hex");
const counts = [];
try {
const fixtures = await buildRecoveryFixtures(
root,
process.env.REA_JAVASCRIPT_FIXTURE_TOOLS,
);
await client.connect(transport);
for (const fixture of fixtures.files) {
for (const surface of ["cli", "stdio_mcp"]) {
const output = join(root, `${fixture.name}-${surface}`);
const flags =
fixture.options === undefined
? []
: [
"--extraction-mode",
fixture.options.extraction_mode,
"--rewrite-level",
fixture.options.rewrite_level,
];
const evidence =
surface === "cli"
? await cli(
"recover-javascript-sources",
fixture.path,
output,
...flags,
)
: await call("recover_javascript_sources", {
path: fixture.path,
output_directory: output,
...fixture.options,
});
const result = javascriptRecoveryResultSchema.parse(
evidence.normalized_result,
);
await assertRecoveredArtifacts(evidence, result, fixture);
const analysis =
surface === "cli"
? await cli(
"analyze-javascript-application",
result.analysis_input.input_path,
)
: await call("analyze_javascript_application", result.analysis_input);
assert.equal(analysis.normalized_result.statistics.parse_failures, 0);
assert.equal(
analysis.normalized_result.statistics.parsed_javascript_files,
result.modules.length,
);
const recoveredEntry =
result.modules.find(
(module) => module.reported_filename === "entry.js",
) ?? result.modules[0];
assert.ok(recoveredEntry);
const oracle =
'await import((await import("node:url")).pathToFileURL(process.argv[1])); const f=globalThis.recoveryFixture; console.log(JSON.stringify([f.greet(null),f.greet("REA"),f.total([1,2,3])]));';
for (const selected of fixture.options?.extraction_mode === "inspection"
? []
: [fixture.path, recoveredEntry.artifact.path]) {
const execution = await run(
process.execPath,
["--input-type=module", "-e", oracle, selected],
{ env: environment, timeout: 10000 },
);
assert.deepEqual(JSON.parse(execution.stdout), [
"Hello world",
"Hello REA",
6,
]);
}
counts.push({
fixture: fixture.name,
surface,
modules: result.modules.length,
maps: result.modules.filter((module) => module.source_map !== null)
.length,
finite_fixture_oracle:
fixture.options?.extraction_mode !== "inspection",
});
}
}
console.log(
JSON.stringify({
engine: "wakaru 1.13.0",
toolchains: fixtures.toolchains,
results: counts,
verified: true,
}),
);
} finally {
await client.close();
await transport.close();
await rm(root, { recursive: true, force: true });
}
async function assertRecoveredArtifacts(evidence, result, fixture) {
assert.deepEqual(result.options, {
extraction_mode: "structural",
rewrite_level: "standard",
...fixture.options,
});
if (fixture.options?.extraction_mode === "inspection")
assert.equal(result.reported_safety, "inspection-only");
assert.equal(result.engine.executable.sha256, OFFICIAL_BINARY_SHA256);
assert.equal(result.engine.executed_source_revision, null);
assert.equal(evidence.confidence, "derived");
assert.equal(result.runtime_equivalence, "unknown");
assert.equal(result.status, "complete");
assert.equal(result.reported_format, fixture.expectedFormat);
assert.equal(result.modules.length, fixture.expectedModules);
assert.equal(result.source.sha256, fingerprint(await readFile(fixture.path)));
for (const file of [
result.source.published_copy,
result.report,
result.provenance,
result.manifest,
...result.modules.flatMap((module) => [
module.artifact,
...(module.source_map ? [module.source_map] : []),
]),
]) {
const bytes = await readFile(file.path);
assert.equal(bytes.length, file.bytes);
assert.equal(fingerprint(bytes), file.sha256);
}
for (const module of result.modules) {
assert.equal(module.provenance.original_sha256, result.source.sha256);
assert.equal(module.provenance.reported_input, result.source.snapshot_path);
for (const range of module.provenance.byte_ranges)
assert.ok(range.start <= range.end && range.end <= result.source.bytes);
if (module.source_map) {
const map = JSON.parse(await readFile(module.source_map.path, "utf8"));
for (const source of map.sources)
assert.equal(
resolve(dirname(module.source_map.path), source),
result.source.published_copy.path,
);
}
}
}
+56
View File
@@ -0,0 +1,56 @@
import { execFile } from "node:child_process";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { promisify } from "node:util";
import { inspectModuleBoundaries } from "./lib/module-boundaries.mjs";
const execute = promisify(execFile);
const root = process.cwd();
const selected = process.argv.slice(2);
for (const file of selected)
if (!file.endsWith(".ts"))
throw new TypeError(`Expected an exact TypeScript source file: ${file}`);
const files =
selected.length > 0
? selected
: (
await execute(
"git",
[
"ls-files",
"-z",
"--cached",
"--others",
"--exclude-standard",
"--",
"src",
],
{ cwd: root, maxBuffer: 8 * 1024 * 1024 },
)
).stdout.split("\0");
let checked = 0;
for (const file of new Set(files.filter((path) => path.endsWith(".ts")))) {
let source;
try {
source = await readFile(resolve(root, file), "utf8");
} catch (cause) {
if (selected.length === 0 && cause.code === "ENOENT") continue;
throw cause;
}
checked += 1;
try {
for (const violation of inspectModuleBoundaries(file, source, root)) {
console.error(
`${violation.file}:${violation.line}: ${violation.boundary}: ${violation.specifier} resolves to ${violation.target}`,
);
process.exitCode = 1;
}
} catch (cause) {
console.error(`Could not inspect ${file}: ${cause.message}`);
process.exitCode = 1;
}
}
if (process.exitCode !== 1)
console.log(`Checked resolved module boundaries in ${checked} source files.`);
+27 -4
View File
@@ -108,14 +108,26 @@ try {
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
const runtimeParent = join(workspace, "runtime with spaces");
await mkdir(runtimeParent);
const callerDirectory = join(workspace, "caller cwd with spaces");
const scriptCollisions = ["ReaGhidraBridge.java", "ReaGhidraPrepareCom.java"];
await mkdir(callerDirectory);
for (const name of scriptCollisions) await mkdir(join(callerDirectory, name));
const environment = {
...process.env,
...Object.fromEntries(
Object.entries(process.env).filter(
([name]) => name.toLowerCase() !== "comspec",
),
),
ComSpec: (
process.env.ComSpec ??
join(process.env.SystemRoot ?? "C:\\Windows", "System32", "cmd.exe")
).replaceAll("\\", "/"),
REA_ANALYSIS_PROVIDER: "ghidra",
REA_LOG_LEVEL: "error",
GHIDRA_HEADLESS_MAXMEM: "512m",
NODE_OPTIONS: "--max-old-space-size=512",
TEMP: runtimeParent,
TMP: runtimeParent,
TEMP: runtimeParent.replaceAll("\\", "/"),
TMP: runtimeParent.replaceAll("\\", "/"),
HOME: workspace,
USERPROFILE: workspace,
APPDATA: join(workspace, "appdata"),
@@ -150,6 +162,7 @@ try {
(
await exec(process.execPath, [entry, "providers", "--json"], {
env: environment,
cwd: callerDirectory,
timeout: 30_000,
})
).stdout,
@@ -159,7 +172,12 @@ try {
const inspected = await exec(
process.execPath,
[entry, "inspect", target, "--provider", "ghidra", "--format", "json"],
{ env: environment, timeout: 360_000, maxBuffer: 8 * 1024 * 1024 },
{
env: environment,
cwd: callerDirectory,
timeout: 360_000,
maxBuffer: 8 * 1024 * 1024,
},
);
const inspection = JSON.parse(inspected.stdout);
assert.ok(inspection.error === undefined);
@@ -169,6 +187,7 @@ try {
command: process.execPath,
args: [entry, "mcp"],
env: environment,
cwd: callerDirectory,
stderr: "pipe",
});
const client = new Client({
@@ -282,6 +301,10 @@ try {
token,
"Analysis changed caller token authority.",
);
assert.deepEqual((await readdir(callerDirectory)).sort(), scriptCollisions);
for (const name of scriptCollisions)
assert.deepEqual(await readdir(join(callerDirectory, name)), []);
report.callerScriptCollisionsPreserved = true;
report.runtimeCleanup = true;
report.ok = true;
if (process.argv[4] !== undefined)
+28 -2
View File
@@ -35,6 +35,9 @@ import {
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
import { verifyLargeScreenshotE2e } from "./lib/browser-screenshot-e2e.mjs";
import { verifyPopupEventCoverage } from "./lib/browser-popup-e2e.mjs";
import { verifyBrowserNetworkEvidence } from "./lib/browser-network-e2e.mjs";
import { verifyBrowserScriptExport } from "./lib/browser-script-export-e2e.mjs";
import { verifyBrowserDomDestinations } from "./lib/browser-dom-destinations-e2e.mjs";
const REAL_BROWSER_STARTUP_TIMEOUT_MS = 60_000;
const SCENARIO_SECRET_VALUE = "rea-browser-verifier-secret";
@@ -96,6 +99,10 @@ try {
);
if (!observed.ok) throw observed.error;
assertObservation(observed.value, site.origin);
const domDestinations = await verifyBrowserDomDestinations({
cdp_endpoint: endpoint,
target_id: target,
});
const serialized = JSON.stringify(observed.value);
if (
!observed.value.target.url.includes("startup=browser-secret-value") ||
@@ -320,6 +327,15 @@ try {
const largeScreenshot = await verifyLargeScreenshotE2e(endpoint, site.origin);
const popupEvents = await verifyPopupEventCoverage(executable);
const networkEvidence = await verifyBrowserNetworkEvidence(
executable,
undefined,
{ cdp_endpoint: endpoint, target_id: target },
);
const scriptExport = await verifyBrowserScriptExport(executable, undefined, {
cdp_endpoint: endpoint,
target_id: target,
});
process.stdout.write(
`${JSON.stringify({
verifier_run: await completeVerifierRun(verifierRun),
@@ -339,6 +355,9 @@ try {
screenshotBytes: screenshot.value.artifact.bytes,
largeScreenshot,
popupEvents,
networkEvidence,
scriptExport,
domDestinations,
browserScenarioCli: true,
browserScenarioAttachCleanup: "disconnected-external",
browserScenarioLaunchCleanup: "terminated-owned-process",
@@ -394,9 +413,16 @@ async function verifyPageScopedTransport(provider, proxy, origin) {
target_id: target,
observation_ms: 5_000,
}),
{
progress: {
report(event) {
if (event.phase === "browser_observation" && event.completed === 1)
proxy.disconnectClients();
return Promise.resolve();
},
},
},
);
await delay(100);
proxy.disconnectClients();
const disconnected = await disconnectedPromise;
if (
!disconnected.ok ||
+65
View File
@@ -0,0 +1,65 @@
import { execFile } from "node:child_process";
import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { promisify } from "node:util";
import { compareTestDiscovery } from "./lib/test-discovery.mjs";
const execute = promisify(execFile);
const root = resolve(import.meta.dirname, "..");
const temporary = await mkdtemp(join(tmpdir(), "rea-test-discovery-"));
try {
const output = join(temporary, "discovery.json");
await execute(
process.execPath,
[
join(root, "node_modules/vitest/vitest.mjs"),
"list",
"--filesOnly",
`--json=${output}`,
],
{ cwd: root, maxBuffer: 8 * 1024 * 1024 },
);
const files = await execute(
"git",
[
"ls-files",
"-z",
"--cached",
"--others",
"--exclude-standard",
"--",
"src",
"tests",
],
{ cwd: root, maxBuffer: 8 * 1024 * 1024 },
);
const expected = [];
for (const path of files.stdout
.split("\0")
.filter((path) => path.endsWith(".test.ts"))) {
try {
if ((await stat(join(root, path))).isFile()) expected.push(path);
} catch (cause) {
if (cause.code !== "ENOENT") throw cause;
}
}
const report = compareTestDiscovery(
expected,
JSON.parse(await readFile(output, "utf8")),
root,
);
if (
report.missing.length ||
report.unexpected.length ||
report.duplicates.length
) {
console.error(JSON.stringify(report, null, 2));
process.exitCode = 1;
} else
console.log(`Discovered all ${report.discovered} test files exactly once.`);
} finally {
await rm(temporary, { recursive: true, force: true });
}
+51 -3
View File
@@ -79,13 +79,43 @@ try {
await mkdir(sourceDirectory);
const source = join(sourceDirectory, "target.bin");
await writeFile(source, Buffer.alloc(1024 * 1024, 0x37));
const sourceIdentity = native.call("open", [source]);
try {
for (const requested of [
source.replaceAll("\\", "/"),
source.replace(/\\/u, "/"),
]) {
const admitted = native.call("open", [requested]);
try {
assert.equal(admitted.requestedPath, requested);
assert.equal(admitted.finalPath, sourceIdentity.finalPath);
assert.equal(admitted.fileId, sourceIdentity.fileId);
assert.equal(native.call("read", [admitted.handle, 0, 1])[0], 0x37);
} finally {
native.call("close", [admitted.handle]);
}
}
} finally {
native.call("close", [sourceIdentity.handle]);
}
for (const rejected of [
source.replace(/^([A-Za-z]):\\/u, "$1:"),
`${sourceDirectory}/../source/target.bin`,
`${sourceDirectory}//target.bin`,
`${source}:stream`,
`\\\\?\\${source.replaceAll("\\", "/")}`,
"//localhost/share/target.bin",
])
assert.throws(() => native.call("open", [rejected]));
report.controls.ordinaryDriveSeparatorsAndRequestedIdentity = true;
const snapshotOwner = WindowsPrivateRuntime.create(
workspace,
workspace.replace(/\\/u, "/"),
"single-flight-",
);
try {
const rejectedController = new AbortController();
const accepted = snapshotOwner.snapshot(source, "accepted.bin");
const requestedSource = source.replaceAll("\\", "/");
const accepted = snapshotOwner.snapshot(requestedSource, "accepted.bin");
const rejected = snapshotOwner.snapshot(
source,
"rejected.bin",
@@ -94,6 +124,7 @@ try {
rejectedController.abort();
await assert.rejects(rejected, /Runtime snapshot is still pending/u);
const completed = await accepted;
assert.equal(completed.source.requestedPath, requestedSource);
assert.equal(
completed.sha256,
createHash("sha256")
@@ -147,7 +178,7 @@ try {
);
assert.throws(() => native.call("open", [source + ":stream"]));
const runtime = native.call("runtime_create", [
workspace,
workspace.replaceAll("\\", "/"),
"private root with spaces-",
]);
const sentinel = join(sourceDirectory, "sentinel");
@@ -316,6 +347,23 @@ setInterval(()=>{},1000);\n`,
assert.match(settled.output, /observed/u);
native.call("process_close", [exited.handle]);
report.processes.normalExitAndOutput = true;
const forwardCommand = process.execPath.replaceAll("\\", "/");
const forwardProcess = native.call("process_spawn", [
forwardCommand,
[forwardCommand, "-e", "console.log('forward executable')"]
.map(quote)
.join(" "),
workspace,
environment,
]);
try {
const completed = await wait(forwardProcess);
assert.equal(completed.exitCode, 0);
assert.match(completed.output, /forward executable/u);
report.processes.ordinaryDriveSeparators = true;
} finally {
native.call("process_close", [forwardProcess.handle]);
}
const boundaryFixture = process.argv[3];
if (boundaryFixture !== undefined) {
const before = JSON.parse(
+2 -2
View File
@@ -3,8 +3,8 @@ name: reverse-engineer-anything
description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
metadata:
version: "26"
tool_count: 125
catalog_digest: "ed9c36900e9223561d38ba30a44921f4ff101d5eca989920f9edc9dc91df470b"
tool_count: 129
catalog_digest: "856ce0f99cc6e858f3317e40cec27c2cfd479ae7ff6a8d9cf695504d6166b64d"
---
# REA
@@ -1,7 +1,7 @@
# Android application artifacts
Use the connected server's advertised Android tools. Repository main includes
this family; npm 4.0.1 does not. A skill installation alone cannot add it to an
Use the connected server's advertised Android tools. Repository main and npm
4.1.0 include this family. A skill installation alone cannot add it to an
older server.
Start with `inspect_android_package` on the caller's APK path for package
@@ -44,7 +44,7 @@ When advertised by the connected server, application trace and compare tools
accept complete inline Evidence or
`{"kind":"retained-evidence","evidence_id":"ev_<64 lowercase hex characters>"}`
for their application input (`application`, or `left`/`right`). This notation is
a template: replace it with the actual returned ID. npm 4.0.1 accepts only full
a template: replace it with the actual returned ID. Versions before 4.1.0 accept only full
inline Evidence. Use the exact ID
returned by the producer on the same MCP connection. Resolution does not run
analysis or select a provider; findings remain inline. Native observation
@@ -37,29 +37,30 @@ assembly. Keep managed/native boundaries and unavailable reconstruction facts
explicit. A bring-your-own reconstruction oracle is separate from the canonical
parser and must not become an implicit setup dependency.
## .NET NativeAOT
### .NET NativeAOT
A NativeAOT PE/ELF contains native code. Absence of PE/CLI metadata does not mean
analysis must stop at assembly. Use native function analysis for recovered
pseudocode; it is not reconstructed original C#.
NativeAOT binaries contain native machine code, so ordinary CIL decompilers
cannot recover the original method bodies. When managed metadata/CIL is absent,
classify that as a native-analysis route rather than claiming source recovery.
Open the native image with the selected deep provider and inspect functions,
strings, calls, and references; describe decompiler output as pseudocode and
inference.
With an explicitly configured NativeAOT Ghidra adapter, call
`inspect_native_load_image` after opening the target. Its optional
`observations.metadata_recovery` returns the format, positive header evidence,
coverage, derived-memory identity and type addresses/category paths inline.
Then inspect a reported type with `inspect_native_data_type`, follow its
`metadata_recovery.related_type`, interfaces and virtual slot addresses, and use
`analyze_function` plus string/xref searches for implementation evidence. Keep
`not_applicable`, partial recovery and unsupported layouts explicit. Generated
`Class_address` names and inferred System.Object/String identities are not
original names; custom field layouts remain unknown.
NativeAOT images can be PE/COFF, ELF, or Mach-O executables and shared
libraries. Use file bytes and loader metadata rather than suffixes to identify
them. For packaged apps, inventory/extract with REA's artifact tools and retain
the exact contained image identity. PDB, ELF debug, or dSYM sidecars are
additional evidence only when the provider accepts and matches them.
Recovery modifies an ephemeral analysis database and may replace loaded metadata
bytes with derived content. Original executable files remain unchanged. Distinguish
original file offsets from derived analysis memory. Windows x64 PE **targets**
can be analyzed on a supported Linux host; Windows **host** P0 does not admit
this database recovery. See the [NativeAOT guide](https://github.com/morluto/rea/blob/main/docs/ghidra-nativeaot.md)
for the tested layout and optional bring-your-own adapter build.
For deeper type recovery, the optional third-party
[Ghidra NativeAOT analyzer](https://github.com/Washi1337/ghidra-nativeaot)
rehydrates ReadyToRun metadata and can recover method tables, type relationships,
frozen objects, and strings. This is a Ghidra extension with its own install and
interactive workflow; it is not currently part of REA's managed parser or
headless Ghidra bridge. Treat its annotations as analysis evidence, retain the
binary identity, and do not present recovered types as original source. Header
discovery may require a symbol or analyst identification of the ReadyToRun
header, depending on the binary and analyzer version.
## Packages and extraction
+3 -2
View File
@@ -16,6 +16,7 @@ import {
jadxXrefsSchema,
normalizeJadxText,
} from "./JadxProtocol.js";
import { JADX_RELEASE } from "./JadxRelease.js";
/** The adapter consumes tool payloads, never provider RPC details outside this layer. */
export interface JadxToolPort {
@@ -246,7 +247,7 @@ const inspectMethod = async (
throw new AnalysisCapabilityUnavailableError(
"jadx",
request.operation,
`JADX 0.7.1 smali fallback joins all same-name overloads for ${summary.full_name}.${request.input.method_name}; it cannot establish source for only overload ${index}.`,
`JADX ${JADX_RELEASE.version} smali fallback joins all same-name overloads for ${summary.full_name}.${request.input.method_name}; it cannot establish source for only overload ${index}.`,
);
return androidResultSchemas.inspect_android_method.parse({
engine,
@@ -306,7 +307,7 @@ const traceReferences = async (
throw new AnalysisCapabilityUnavailableError(
"jadx",
request.operation,
`JADX 0.7.1 references select the first same-name method; ${summary.full_name}.${methodName} has ${candidates.length} overloads. No unambiguous reference result is available.`,
`JADX ${JADX_RELEASE.version} references select the first same-name method; ${summary.full_name}.${methodName} has ${candidates.length} overloads. No unambiguous reference result is available.`,
);
const expectedTarget =
methodName === undefined
+2 -3
View File
@@ -3,6 +3,7 @@ import { constants } from "node:fs";
import { isAbsolute, join } from "node:path";
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
import type { AndroidOperation } from "../domain/androidAnalysis.js";
import { JADX_JAR_CONFIGURATION_REMEDIATION } from "./JadxRelease.js";
/** Caller-supplied tools, resolved only when an Android operation is selected. */
export interface JadxConfiguration {
@@ -27,9 +28,7 @@ export const resolveJadxConfiguration = async (
"JAVA_HOME must select an existing JDK by absolute path; omit it to use java on PATH.",
);
if (jar === undefined || !isAbsolute(jar))
throw unavailable(
"Set REA_JADX_MCP_JAR to the absolute path of a caller-supplied jadx-headless-mcp 0.7.1 JAR. REA does not download or install it.",
);
throw unavailable(JADX_JAR_CONFIGURATION_REMEDIATION);
const java =
environment.JAVA_HOME === undefined
? "java"
+5 -2
View File
@@ -20,6 +20,7 @@ import {
} from "./AndroidTargetSnapshot.js";
import { resolveJadxConfiguration } from "./JadxConfiguration.js";
import type { JadxLauncher } from "./JadxMcpTransport.js";
import { JADX_HEAP_XMX_ARGUMENT } from "./JadxRelease.js";
import { JadxSession } from "./JadxSession.js";
const OPERATION_TIMEOUT_MS = 120_000;
@@ -203,7 +204,7 @@ export class JadxProvider implements AndroidAnalysisPort {
{
command: configuration.java,
arguments: [
"-Xmx512m",
JADX_HEAP_XMX_ARGUMENT,
"-XX:ActiveProcessorCount=1",
"-jar",
engine.path,
@@ -216,7 +217,9 @@ export class JadxProvider implements AndroidAnalysisPort {
],
cwd: root.path,
hostEnvironment: this.environment,
env: { _JAVA_OPTIONS: "-Xmx512m -XX:ActiveProcessorCount=1" },
env: {
_JAVA_OPTIONS: `${JADX_HEAP_XMX_ARGUMENT} -XX:ActiveProcessorCount=1`,
},
},
this.launcher,
);
+9
View File
@@ -6,6 +6,15 @@ export const JADX_RELEASE = Object.freeze({
url: "https://github.com/1013503897/jadx-headless-mcp/releases/download/v0.7.1/jadx-headless-mcp-0.7.1-all.jar",
});
/** JVM heap ceiling applied to every owned JADX worker process. */
export const JADX_HEAP_LIMIT_MIB = 512;
/** JVM `-Xmx` argument matching {@link JADX_HEAP_LIMIT_MIB}. */
export const JADX_HEAP_XMX_ARGUMENT = `-Xmx${JADX_HEAP_LIMIT_MIB}m`;
/** Remediation when `REA_JADX_MCP_JAR` is missing or not an absolute path. */
export const JADX_JAR_CONFIGURATION_REMEDIATION = `Set REA_JADX_MCP_JAR to the absolute path of a caller-supplied jadx-headless-mcp ${JADX_RELEASE.version} JAR. REA does not download or install it.`;
/** Producing engine identity used by Evidence and the generated product catalog. */
export const JADX_PROVIDER_IDENTITY = Object.freeze({
id: "jadx",
+3 -2
View File
@@ -20,6 +20,7 @@ import {
parseJadxJson,
} from "./JadxProtocol.js";
import {
JADX_HEAP_LIMIT_MIB,
JADX_RELEASE,
JADX_PROVIDER_IDENTITY,
JADX_LIMITATIONS,
@@ -79,12 +80,12 @@ export class JadxSession {
);
const engine = {
name: "jadx-headless-mcp",
version: "0.7.1",
version: JADX_RELEASE.version,
artifact_sha256: jarHash,
source_revision:
jarHash === JADX_RELEASE.sha256 ? JADX_RELEASE.revision : null,
worker_count: 1,
heap_limit_mib: 512,
heap_limit_mib: JADX_HEAP_LIMIT_MIB,
} as const;
const result = await analyzeJadxRequest(tools, request, loaded, engine);
return createAnalysisExecution(result, JADX_PROVIDER_IDENTITY, {
+10 -9
View File
@@ -18,12 +18,14 @@ import type {
ProviderIdentity,
} from "./AnalysisProvider.js";
import type { AnalysisOperation } from "./AnalysisProvider.js";
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/officialToolContracts.js";
import { ENHANCED_TOOL_CONTRACTS } from "../contracts/enhancedToolContracts.js";
import type { BinarySessionPort } from "./BinarySessionPort.js";
import {
SessionProviderRouter,
type SessionProviderRoute,
} from "./SessionProviderRouter.js";
import { InvestigationRecords } from "./investigation/InvestigationRecords.js";
import { BinarySessionRecords } from "./BinarySessionRecords.js";
import { binarySessionStatus } from "./BinarySessionStatus.js";
import {
@@ -38,14 +40,10 @@ import {
import { closeAnalysisClient } from "./AnalysisClientCleanup.js";
export type { BinarySessionPort } from "./BinarySessionPort.js";
const OFFICIAL_OPERATIONS: ReadonlySet<string> = new Set(
GENERATED_MCP_TOOL_CATALOG.filter(
({ kind }) => kind === "official-proxy",
).map(({ name }) => name),
OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name),
);
const ENHANCED_OPERATIONS: ReadonlySet<string> = new Set(
GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "enhanced").map(
({ name }) => name,
),
ENHANCED_TOOL_CONTRACTS.map(({ name }) => name),
);
/**
@@ -78,8 +76,11 @@ export class BinarySession
>();
readonly #availabilityListeners = new Set<() => void | Promise<void>>();
constructor(providerRouter: SessionProviderRouter) {
super();
constructor(
providerRouter: SessionProviderRouter,
records: InvestigationRecords = new InvestigationRecords(),
) {
super(records);
this.#providerRouter = providerRouter;
}
@@ -1,3 +1,4 @@
import { InvestigationRecords } from "./investigation/InvestigationRecords.js";
import type { AnalysisProvider } from "./AnalysisProvider.js";
import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js";
import { BinarySession } from "./BinarySession.js";
@@ -15,4 +16,5 @@ export const composeBinarySession = (
registry instanceof SessionProviderRouter
? registry
: SessionProviderRouter.selectable(registry, auxiliaryProviders),
new InvestigationRecords(),
);
+4 -41
View File
@@ -1,21 +1,13 @@
import type { InvestigationRecordPort } from "./investigation/InvestigationRecordPort.js";
import type { ExecutableFormatHint } from "../domain/dosCom.js";
import type { BinaryTarget } from "../domain/binaryTarget.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { Evidence } from "../domain/evidence.js";
import type { EvidenceBundle } from "../domain/evidenceBundle.js";
import type { AnalysisSnapshot } from "../domain/analysisSnapshot.js";
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
import type { AnalysisProviderSelector } from "../contracts/providerSelection.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
import type { UnknownRegistryError } from "../domain/unknownRegistryError.js";
import type { Result } from "../domain/result.js";
import type {
RecordUnknownInput,
ResidualUnknown,
UnknownStatus,
UpdateUnknownInput,
} from "../domain/residualUnknown.js";
import type {
AnalysisOperation,
AnalysisOperationPort,
@@ -24,7 +16,8 @@ import type {
} from "./AnalysisProvider.js";
/** Target lifecycle used by CLI and MCP without exposing a concrete provider. */
export interface BinarySessionPort extends AnalysisOperationPort {
export interface BinarySessionPort
extends AnalysisOperationPort, InvestigationRecordPort {
open(
path: string,
options?: {
@@ -42,40 +35,10 @@ export interface BinarySessionPort extends AnalysisOperationPort {
): Promise<Result<null, AnalysisError>>;
status(): JsonValue;
activeTarget(): BinaryTarget | undefined;
recordEvidence(
evidence: Evidence,
): Result<"added" | "duplicate", EvidenceIntegrityError>;
hasEvidence(evidenceId: string): boolean;
evidenceById(evidenceId: string): Evidence | undefined;
exportEvidenceBundle(): EvidenceBundle;
importEvidenceBundle(bundle: unknown): Result<number, EvidenceIntegrityError>;
exportAnalysisSnapshot(): Result<AnalysisSnapshot, AnalysisError>;
importAnalysisSnapshot(
snapshot: AnalysisSnapshot,
): Result<number, AnalysisError>;
recordUnknown(
input: RecordUnknownInput,
): Result<ResidualUnknown, AnalysisError>;
recordEvidenceWithUnknown(
evidence: Evidence,
input: RecordUnknownInput,
): Result<ResidualUnknown | null, AnalysisError>;
updateUnknown(
input: UpdateUnknownInput,
): Result<ResidualUnknown, AnalysisError>;
listUnknowns(filters?: {
readonly status?: UnknownStatus;
readonly severity?: ResidualUnknown["severity"];
readonly domain?: string;
}): ResidualUnknown[];
verifyUnknownResolution(unknownId: string): Result<
{
readonly valid: boolean;
readonly truthVerified: boolean;
readonly unknown: ResidualUnknown;
},
UnknownRegistryError
>;
providerIdentity(operation?: AnalysisOperation): ProviderIdentity;
analysisProfile(
operation?: AnalysisOperation,
+25 -45
View File
@@ -1,7 +1,7 @@
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
import type { AnalysisSnapshot } from "../domain/analysisSnapshot.js";
import type { BinaryTarget } from "../domain/binaryTarget.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import type { Evidence } from "../domain/evidence.js";
import type { EvidenceBundle } from "../domain/evidenceBundle.js";
import { evidenceBundleForTarget } from "../domain/evidenceBundle.js";
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
@@ -19,25 +19,24 @@ import type {
AnalysisOperation,
} from "./AnalysisProvider.js";
import { AnalysisSnapshotCache } from "./AnalysisSnapshotCache.js";
import { EvidenceLedger } from "./EvidenceLedger.js";
import {
UNKNOWN_REGISTRY_PROVIDER,
unknownEvidenceLinks,
unknownMutationEvidence,
} from "./UnknownEvidence.js";
import { InvestigationRecords } from "./investigation/InvestigationRecords.js";
export interface ActiveAnalysisBinding {
readonly target: BinaryTarget;
readonly profile: AnalysisProfileCommitment | null;
}
/** Owns session evidence, snapshots, and residual unknowns. */
/** Binary snapshot owner and compatibility facade for composed investigation records. */
export abstract class BinarySessionRecords {
readonly #evidence = new EvidenceLedger();
readonly #records: InvestigationRecords;
readonly #snapshot = new AnalysisSnapshotCache();
#snapshotInvalidated = false;
readonly #snapshotListeners = new Set<() => void | Promise<void>>();
constructor(records: InvestigationRecords = new InvestigationRecords()) {
this.#records = records;
}
/** Observe changes to the mutable current analysis snapshot resource. */
onAnalysisSnapshotChanged(listener: () => void | Promise<void>): () => void {
this.#snapshotListeners.add(listener);
@@ -47,27 +46,27 @@ export abstract class BinarySessionRecords {
recordEvidence(
evidence: Evidence,
): Result<"added" | "duplicate", EvidenceIntegrityError> {
const recorded = this.#evidence.record(evidence);
const recorded = this.#records.recordEvidence(evidence);
if (recorded.ok && recorded.value === "added") this.#emitSnapshotChanged();
return recorded;
}
hasEvidence(evidenceId: string): boolean {
return this.#evidence.has(evidenceId);
return this.#records.hasEvidence(evidenceId);
}
evidenceById(evidenceId: string): Evidence | undefined {
return this.#evidence.get(evidenceId);
return this.#records.evidenceById(evidenceId);
}
exportEvidenceBundle(): EvidenceBundle {
return this.#evidence.export();
return this.#records.exportEvidenceBundle();
}
importEvidenceBundle(
bundle: unknown,
): Result<number, EvidenceIntegrityError> {
const imported = this.#evidence.import(bundle);
const imported = this.#records.mergeEvidenceBundle(bundle);
if (!imported.ok) return imported;
if (imported.value.changed) this.#emitSnapshotChanged();
return ok(imported.value.recordsAdded);
@@ -95,8 +94,11 @@ export abstract class BinarySessionRecords {
target,
profile,
target === undefined
? this.#evidence.export()
: evidenceBundleForTarget(this.#evidence.export(), target.sha256),
? this.#records.exportEvidenceBundle()
: evidenceBundleForTarget(
this.#records.exportEvidenceBundle(),
target.sha256,
),
);
}
@@ -116,7 +118,7 @@ export abstract class BinarySessionRecords {
? undefined
: { target: active.target, profile: active.profile },
(bundle) => {
const imported = this.#evidence.import(bundle);
const imported = this.#records.mergeEvidenceBundle(bundle);
return imported.ok ? ok(imported.value.recordsAdded) : imported;
},
);
@@ -175,7 +177,7 @@ export abstract class BinarySessionRecords {
}
protected clearSessionRecords(): void {
this.#evidence.clear();
this.#records.clear();
this.#snapshot.clear();
this.#snapshotInvalidated = false;
this.#emitSnapshotChanged();
@@ -202,10 +204,7 @@ export abstract class BinarySessionRecords {
input: RecordUnknownInput,
): Result<ResidualUnknown, AnalysisError> {
const target = this.activeAnalysisBinding()?.target;
const recorded = this.#evidence.recordUnknown(
input,
unknownMutationEvidence(target, input),
);
const recorded = this.#records.recordUnknown(input, target);
if (recorded.ok) this.#emitSnapshotChanged();
return recorded;
}
@@ -214,11 +213,7 @@ export abstract class BinarySessionRecords {
evidence: Evidence,
input: RecordUnknownInput,
): Result<ResidualUnknown | null, AnalysisError> {
const recorded = this.#evidence.recordWithUnknown(
evidence,
input,
unknownMutationEvidence(undefined, input),
);
const recorded = this.#records.recordEvidenceWithUnknown(evidence, input);
if (recorded.ok) this.#emitSnapshotChanged();
return recorded;
}
@@ -227,22 +222,7 @@ export abstract class BinarySessionRecords {
input: UpdateUnknownInput,
): Result<ResidualUnknown, AnalysisError> {
const target = this.activeAnalysisBinding()?.target;
const evidence = createEvidence(target, UNKNOWN_REGISTRY_PROVIDER, {
predicateType: "rea.residual-unknown-mutation",
operation: "update_unknown",
parameters: {
unknown_id: input.unknown_id,
expected_revision: input.expected_revision,
},
result: { action: "update", status: input.status },
confidence: "derived",
authority: "analyst-inference",
evidenceLinks: unknownEvidenceLinks(input),
limitations: [
"Registry mutation evidence records analyst intent, not proof of the answer.",
],
});
const updated = this.#evidence.updateUnknown(input, evidence);
const updated = this.#records.updateUnknown(input, target);
if (updated.ok) this.#emitSnapshotChanged();
return updated;
}
@@ -254,7 +234,7 @@ export abstract class BinarySessionRecords {
readonly domain?: string;
} = {},
): ResidualUnknown[] {
return this.#evidence.listUnknowns(filters);
return this.#records.listUnknowns(filters);
}
verifyUnknownResolution(unknownId: string): Result<
@@ -265,6 +245,6 @@ export abstract class BinarySessionRecords {
},
UnknownRegistryError
> {
return this.#evidence.verifyUnknownResolution(unknownId);
return this.#records.verifyUnknownResolution(unknownId);
}
}
+18 -15
View File
@@ -12,6 +12,8 @@ import { execFile } from "node:child_process";
import { extname, isAbsolute, join, resolve } from "node:path";
import { promisify } from "node:util";
import { parse as parsePlist } from "plist";
import { isPathWithinRoot } from "../domain/localPath.js";
import { BinaryTargetError } from "../domain/configurationErrors.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
@@ -264,27 +266,36 @@ const resolveAppBundle = async (
}
};
/** Decode the top-level executable name with an XML parser, not a pattern. */
const parseXmlPlistExecutable = (plist: string): string => {
const match =
/<key>\s*CFBundleExecutable\s*<\/key>\s*<string>([^<]+)<\/string>/u.exec(
plist,
);
if (match?.[1] === undefined)
const value = parsePlist(plist);
const executable =
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
!(value instanceof Date) &&
!(value instanceof Uint8Array) &&
Object.hasOwn(value, "CFBundleExecutable")
? value.CFBundleExecutable
: undefined;
if (typeof executable !== "string")
throw new Error("CFBundleExecutable is missing");
return decodeXml(match[1].trim());
return executable;
};
const readBinaryPlistExecutable = async (plistPath: string): Promise<string> =>
// `-n` strips only the newline plutil appends; it requires macOS 12+.
(
await execFileAsync("/usr/bin/plutil", [
"-extract",
"CFBundleExecutable",
"raw",
"-n",
"-o",
"-",
plistPath,
])
).stdout.trim();
).stdout;
const isSafeExecutableName = (name: string): boolean =>
name.length > 0 &&
@@ -293,14 +304,6 @@ const isSafeExecutableName = (name: string): boolean =>
!name.includes("\0") &&
!/[/\\]/u.test(name);
const decodeXml = (value: string): string =>
value
.replaceAll("&amp;", "&")
.replaceAll("&lt;", "<")
.replaceAll("&gt;", ">")
.replaceAll("&quot;", '"')
.replaceAll("&apos;", "'");
const readExecutableMetadata = async (
handle: FileHandle,
hostArchitecture: NodeJS.Architecture,
@@ -333,3 +333,25 @@ describe("capability inventory: caller guidance", () => {
});
});
});
describe("optional JavaScript recovery availability", () => {
it("keeps optional recovery target-free and does not disable other static workflows", () => {
expect(entry("recover_javascript_sources", status())).toMatchObject({
available: false,
reason: "provider_missing",
remediation: expect.stringContaining("REA_WAKARU_COMMAND"),
});
expect(
entry("recover_javascript_sources", status(), {
processCaptureEnabled: true,
javascriptRecoveryEnabled: true,
}),
).toMatchObject({
available: true,
surface: "application",
});
expect(entry("analyze_javascript_application", status())).toMatchObject({
available: true,
});
});
});
+40 -3
View File
@@ -1,4 +1,8 @@
import { z } from "zod";
import type {
OptionalProviderLoadFailures,
OptionalProviderLoadFailure,
} from "./OptionalObservationProviders.js";
import {
providerCapability,
@@ -10,7 +14,7 @@ import {
} from "../contracts/toolOutputSchemaPrimitives.js";
import type { ToolKind } from "../contracts/toolContractTypes.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
import { TOOL_CONTRACTS } from "../contracts/toolContracts.js";
import {
clientRequirementsFor,
NO_CLIENT_FEATURES,
@@ -28,9 +32,11 @@ type ToolAvailabilityReason = "available" | ToolUnavailabilityReason;
type ProviderDescriptor = ProviderCapability;
export type AvailabilityPolicy = {
readonly processCaptureEnabled: boolean;
readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures;
readonly firmwareInspectionEnabled?: boolean;
readonly firmwareExtractionEnabled?: boolean;
readonly androidAnalysisEnabled?: boolean;
readonly javascriptRecoveryEnabled?: boolean;
readonly browserObservationEnabled?: boolean;
readonly browserScenarioEnabled?: boolean;
readonly electronObservationEnabled?: boolean;
@@ -126,7 +132,7 @@ export const buildCapabilityInventory = (
const descriptors = new Map<string, ProviderDescriptor>(
status.capabilities.map((descriptor) => [descriptor.operation, descriptor]),
);
return GENERATED_MCP_TOOL_CATALOG.map((contract): ToolAvailability => {
return TOOL_CONTRACTS.map((contract): ToolAvailability => {
const availability = availabilityFor({
name: contract.name,
kind: contract.kind,
@@ -186,6 +192,12 @@ const availabilityFor = (context: AvailabilityContext): Availability => {
return navigationContextAvailability(context.descriptors);
const javascriptApplication = javascriptApplicationAvailability(context);
if (javascriptApplication !== null) return javascriptApplication;
const failure = optionalProviderFailureFor(context);
if (failure !== undefined)
return {
reason: "provider_unavailable",
remediation: `Adapter ${failure.providerId} could not load: ${failure.reason}. Repair the adapter installation and restart REA; other capabilities remain available.`,
};
const workflowAvailability = workflowAvailabilityFor(context);
if (workflowAvailability !== null) return workflowAvailability;
const targetDecision = targetAvailability(context);
@@ -193,6 +205,23 @@ const availabilityFor = (context: AvailabilityContext): Availability => {
return providerAvailability(context);
};
const optionalProviderFailureFor = ({
name,
kind,
policy,
}: AvailabilityContext): OptionalProviderLoadFailure | undefined => {
const failures = policy.optionalProviderLoadFailures;
if (name === "capture_browser_scenario")
return failures?.browserScenarioCapture;
if (kind === "browser-provider") return failures?.browserObservation;
if (name === "capture_electron_scenario")
return failures?.electronActiveObservation;
if (kind === "electron-provider") return failures?.electronObservation;
if (kind === "runtime-provider")
return failures?.javascriptRuntimeObservation;
return undefined;
};
const javascriptApplicationAvailability = ({
name,
}: AvailabilityContext): Availability | null => {
@@ -208,6 +237,14 @@ const workflowAvailabilityFor = ({
kind,
policy,
}: AvailabilityContext): Availability | null => {
if (name === "recover_javascript_sources")
return policy.javascriptRecoveryEnabled === true
? { reason: "available", remediation: null }
: {
reason: "provider_missing",
remediation:
"On Linux x64, provide an absolute REA_WAKARU_COMMAND for Wakaru 1.13.0 and util-linux prlimit. No binary target is required.",
};
if (kind === "firmware-provider") {
const enabled =
name === "inspect_firmware_regions"
@@ -218,7 +255,7 @@ const workflowAvailabilityFor = ({
: {
reason: "provider_missing",
remediation:
"On Linux, provide an absolute REA_BINWALK_COMMAND (Binwalk 3.1.0) or REA_UNBLOB_COMMAND (Unblob 26.6.4) path and util-linux prlimit. Extraction also requires the selected format’s external extractor.",
"On Linux, provide an absolute REA_BINWALK_COMMAND (Binwalk 3.1.x, verified with 3.1.0) or REA_UNBLOB_COMMAND (Unblob 26.6.x, verified with 26.6.4) path and util-linux prlimit. Extraction also requires the selected format’s external extractor.",
};
}
if (kind === "android-provider")
@@ -22,7 +22,13 @@ describe.each([
expect(
parseClientConfiguration(`${bom}${JSON.stringify(document)}`, format),
).toEqual({ document, servers: document[serversKey], serversKey });
).toEqual({
document,
servers: document[serversKey],
serversPath: [serversKey],
dialect: format,
legacyServers: {},
});
});
it.each(["", "\uFEFF"])(
@@ -70,12 +76,16 @@ describe.each([
else servers.rea = registration;
const document = { ...parsed.document, [serversKey]: servers };
text = serializeClientConfiguration(document, format, text, "rea");
text = serializeClientConfiguration(document, format, text, [
[serversKey, "rea"],
]);
expect(parseClientConfiguration(text, format)).toEqual({
document,
servers,
serversKey,
serversPath: [serversKey],
dialect: format,
legacyServers: {},
});
expect(Buffer.from(text.slice(0, prefix.length))).toEqual(
Buffer.from(prefix),
@@ -92,14 +102,11 @@ describe.each([
});
describe("TOML client configuration BOM handling", () => {
it("retains the TOML parser's existing leading-BOM rejection", () => {
it.each(["", "\uFEFF"])("parses a document with prefix %j", (bom) => {
const text = '[mcp_servers.rea]\ncommand = "rea"\n';
expect(parseClientConfiguration(text, "toml").servers).toEqual({
expect(parseClientConfiguration(`${bom}${text}`, "toml").servers).toEqual({
rea: { command: "rea" },
});
expect(() => parseClientConfiguration(`\uFEFF${text}`, "toml")).toThrow(
"Invalid TOML document",
);
});
});
+124 -18
View File
@@ -14,11 +14,27 @@ import type { SetupClient } from "./SupportedClients.js";
export type ClientConfigurationFormat = NonNullable<SetupClient["format"]>;
export type ClientServersKey = "mcp_servers" | "mcpServers" | "mcp" | "servers";
/**
* Registration entry dialect. OpenCode V2 reads V1 `mcp.<name>` entries, but a
* document already using the native `mcp.servers` table takes native entries.
*/
export type ClientRegistrationDialect =
| ClientConfigurationFormat
| "opencode_v2";
/** Validated client document and its server table, preserving unrelated settings. */
export interface ClientConfigurationDocument {
readonly document: Record<string, unknown>;
/** Server table that registrations are written to. */
readonly servers: Record<string, unknown>;
readonly serversKey: ClientServersKey;
/** Key path of `servers` in `document`. */
readonly serversPath: readonly string[];
readonly dialect: ClientRegistrationDialect;
/**
* OpenCode V1 server members beside a native V2 `mcp.servers` table. OpenCode
* still loads them, and a native entry with the same name takes precedence.
*/
readonly legacyServers: Record<string, unknown>;
}
const objectSchema = z.record(z.string(), z.unknown());
@@ -77,6 +93,9 @@ const parseDocument = (
return objectSchema.parse(document);
};
/** Members of OpenCode V2's `mcp` object that are not server names. */
const OPENCODE_V2_MCP_SETTINGS = new Set(["servers", "timeout"]);
/** Parse a client document and reject malformed roots or MCP server tables. */
export const parseClientConfiguration = (
text: string,
@@ -88,7 +107,75 @@ export const parseClientConfiguration = (
const serversKey = clientConfigurationServersKey(format);
const value = document[serversKey];
const servers = value === undefined ? {} : objectSchema.parse(value);
return { document, servers, serversKey };
const native = servers.servers;
// A V1 server named `servers` has a string `type` discriminator; the V2
// table may instead hold a server object named `type`.
if (
format !== "opencode" ||
native === undefined ||
(typeof native === "object" &&
native !== null &&
"type" in native &&
typeof native.type === "string")
)
return {
document,
servers,
serversPath: [serversKey],
dialect: format,
legacyServers: {},
};
return {
document,
servers: objectSchema.parse(native),
serversPath: [serversKey, "servers"],
dialect: "opencode_v2",
legacyServers: Object.fromEntries(
Object.entries(servers).filter(
([name]) => !OPENCODE_V2_MCP_SETTINGS.has(name),
),
),
};
};
/** The registration a client loads for `name`, including OpenCode V1 entries. */
export const effectiveClientServer = (
parsed: ClientConfigurationDocument,
name: string,
): unknown =>
Object.hasOwn(parsed.servers, name)
? parsed.servers[name]
: parsed.legacyServers[name];
/**
* Return `parsed.document` with `servers` at its server table. A legacy OpenCode
* V1 member named in `removeLegacy` is removed beside a native V2 table.
*/
export const withClientServers = (
parsed: ClientConfigurationDocument,
servers: Record<string, unknown>,
removeLegacy?: string,
): Record<string, unknown> => {
const [key = "", nested] = parsed.serversPath;
if (nested === undefined) return { ...parsed.document, [key]: servers };
const outer = { ...objectSchema.parse(parsed.document[key]) };
if (removeLegacy !== undefined && !OPENCODE_V2_MCP_SETTINGS.has(removeLegacy))
delete outer[removeLegacy];
return { ...parsed.document, [key]: { ...outer, [nested]: servers } };
};
/** Read the value at a key path, or `undefined` when any member is absent. */
const valueAt = (
document: Record<string, unknown>,
path: readonly string[],
): unknown => {
let current: unknown = document;
for (const key of path) {
const object = objectSchema.safeParse(current);
if (!object.success || !Object.hasOwn(object.data, key)) return undefined;
current = object.data[key];
}
return current;
};
/** Serialize a validated client document, retaining JSONC comments. */
@@ -96,33 +183,43 @@ export const serializeClientConfiguration = (
document: Record<string, unknown>,
format: ClientConfigurationFormat | undefined,
originalText?: string,
editedServerName?: string,
editedPaths?: readonly (readonly string[])[],
): string => {
if (format === undefined || format === "unsupported")
throw new TypeError("client does not have a supported MCP config format");
if (format === "toml") return stringifyToml(document);
if (originalText !== undefined && editedServerName !== undefined) {
const serversKey = clientConfigurationServersKey(format);
const servers = objectSchema.parse(document[serversKey] ?? {});
const editedValue = Object.hasOwn(servers, editedServerName)
? servers[editedServerName]
: undefined;
const edits = modify(
if (originalText !== undefined && editedPaths !== undefined)
return editedPaths.reduce(
(text, path) =>
applyEdits(
text,
modify(text, [...path], valueAt(document, path), {
formattingOptions: { insertSpaces: true, tabSize: 2, eol: "\n" },
}),
),
originalText,
[serversKey, editedServerName],
editedValue,
{
formattingOptions: { insertSpaces: true, tabSize: 2, eol: "\n" },
},
);
return applyEdits(originalText, edits);
}
return `${JSON.stringify(document, null, 2)}\n`;
};
/** Key path of a named registration in the server table. */
export const clientServerPath = (
parsed: ClientConfigurationDocument,
name: string,
): string[] => [...parsed.serversPath, name];
/** Key path of a present OpenCode V1 entry beside a native V2 table. */
export const legacyClientServerPath = (
parsed: ClientConfigurationDocument,
name: string,
): string[] | undefined =>
Object.hasOwn(parsed.legacyServers, name)
? [parsed.serversPath[0] ?? "mcp", name]
: undefined;
/** Build the stdio entry shape expected by one client's configuration dialect. */
export const clientRegistrationEntry = (
format: ClientConfigurationFormat,
format: ClientRegistrationDialect,
command: readonly string[],
environment: Readonly<Record<string, string>>,
): Record<string, unknown> => {
@@ -137,6 +234,15 @@ export const clientRegistrationEntry = (
? {}
: { environment: { ...environment } }),
};
case "opencode_v2":
// V2 connects servers unless `disabled` is true.
return {
type: "local",
command: [...command],
...(Object.keys(environment).length === 0
? {}
: { environment: { ...environment } }),
};
case "vscode":
return {
type: "stdio",
+15 -10
View File
@@ -1,4 +1,7 @@
import { parseClientConfiguration } from "./ClientConfigurationDocument.js";
import {
effectiveClientServer,
parseClientConfiguration,
} from "./ClientConfigurationDocument.js";
import { access, readFile } from "node:fs/promises";
import { resolve } from "node:path";
@@ -85,8 +88,10 @@ export const readClientRegistrationStatuses = async (
continue;
try {
const content = await readFile(client.configPath, "utf8");
const { servers } = parseClientConfiguration(content, client.format);
const raw = servers[PRODUCT_IDENTITY.mcpServerKey];
const raw = effectiveClientServer(
parseClientConfiguration(content, client.format),
PRODUCT_IDENTITY.mcpServerKey,
);
if (raw === undefined) {
statuses.push(
unavailableStatus(client.name, client.configPath, "missing"),
@@ -133,13 +138,6 @@ const registrationAligned = (
return false;
if (!isOwnedClientRegistrationCommand(command, currentCommandPath))
return false;
if (
command.length === 3 &&
command[2] === "mcp" &&
resolve(command[0] ?? "") === resolve(process.execPath) &&
resolve(command[1] ?? "") === currentCommandPath
)
return true;
if (
client.name === "codex" &&
registration.startup_timeout_sec !==
@@ -155,6 +153,13 @@ const registrationAligned = (
return false;
if (client.format === "commandcode" && registration.transport !== "stdio")
return false;
if (
command.length === 3 &&
command[2] === "mcp" &&
resolve(command[0] ?? "") === resolve(process.execPath) &&
resolve(command[1] ?? "") === currentCommandPath
)
return true;
if (
command.length === 4 &&
command[0] === "npx" &&
+3 -1
View File
@@ -1,4 +1,6 @@
import { describe, expect, it } from "vitest";
import { SUPPORTED_NODE_VERSION_PROSE } from "../domain/runtimeVersion.js";
import type { ClientRegistrationStatus } from "./ClientRegistrationStatus.js";
import { createDoctorHostFixture as host } from "./Doctor.fixture.js";
import { runDoctor } from "./Doctor.js";
@@ -19,7 +21,7 @@ describe("doctor", () => {
result.checks.map(({ name, remediation }) => [name, remediation]),
),
).toEqual({
node: "Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",
node: `Install ${SUPPORTED_NODE_VERSION_PROSE}.`,
host: "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and the experimental Windows x64 Ghidra P0 boundary.",
hopper: "Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.",
target: "Supply a readable local app or program path.",
+5 -2
View File
@@ -1,6 +1,9 @@
import { homedir } from "node:os";
import { supportsNodeVersion } from "../domain/runtimeVersion.js";
import {
SUPPORTED_NODE_VERSION_PROSE,
supportsNodeVersion,
} from "../domain/runtimeVersion.js";
import { linuxHopperLauncherPath } from "./LinuxHopper.js";
import type {
DoctorCheck,
@@ -92,7 +95,7 @@ export const doctorHealthy = (
const nodeCheck = (host: DoctorHost): DoctorCheck =>
check("node", supportsNodeVersion(host.nodeVersion), host.nodeVersion, {
remediation: "Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",
remediation: `Install ${SUPPORTED_NODE_VERSION_PROSE}.`,
classification: "missing_dependency",
});
+1 -1
View File
@@ -5,7 +5,7 @@ import type { JsonValue } from "../domain/jsonValue.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
import { compareBundles } from "../domain/bundleComparison.js";
import { EvidenceLedger } from "./EvidenceLedger.js";
import { EvidenceLedger } from "./investigation/EvidenceLedger.js";
import {
readEvidenceBundle,
writeEvidenceBundle,
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import { evidenceInputSchema } from "../contracts/evidenceInputContracts.js";
import { JAVASCRIPT_FEATURE_TRACE_EXAMPLE } from "../contracts/javascriptApplicationWorkflowExamples.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { EvidenceLedger } from "./EvidenceLedger.js";
import { EvidenceLedger } from "./investigation/EvidenceLedger.js";
import { resolveEvidenceInput } from "./EvidenceInputResolver.js";
const evidence = JAVASCRIPT_FEATURE_TRACE_EXAMPLE.application;
+93 -21
View File
@@ -12,6 +12,11 @@ import {
interfaceBuilderLimitsSchema,
type InterfaceBuilderDocumentInput,
} from "../domain/interfaceBuilderGraph.js";
import {
mergeNibHierarchies,
projectNibViewHierarchy,
type NibHierarchyNode,
} from "./NibViewHierarchy.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
const MAX_DOCUMENT_BYTES = 64 * 1024 * 1024;
@@ -27,6 +32,7 @@ export const analyzeInterfaceBuilderBundle = async (input: {
const reader = new DirectoryArtifactReader(input.bundlePath);
const documents: InterfaceBuilderDocumentInput[] = [];
const invalid: string[] = [];
const incompleteHierarchies = new Map<string, number>();
let omitted = 0;
let attempted = 0;
try {
@@ -40,10 +46,13 @@ export const analyzeInterfaceBuilderBundle = async (input: {
attempted += 1;
try {
const bytes = await readEntry(reader, entry, input.signal);
const raw =
const nib =
bytes.subarray(0, 10).toString("ascii") === "NIBArchive"
? projectNibArchive(decodeNibArchive(bytes))
: decodePlist(bytes);
: null;
const raw = nib === null ? decodePlist(bytes) : nib.raw;
if (nib !== null && nib.omitted > 0)
incompleteHierarchies.set(entry.path, nib.omitted);
const documentHash = createHash("sha256").update(bytes).digest("hex");
documents.push({
relativePath: entry.path,
@@ -69,26 +78,71 @@ export const analyzeInterfaceBuilderBundle = async (input: {
documents,
limits,
});
return finalizeHierarchyCoverage(result, {
incompleteHierarchies,
invalid,
attempted,
omitted,
});
};
const finalizeHierarchyCoverage = (
result: ReturnType<typeof buildInterfaceBuilderAnalysis>,
counts: {
incompleteHierarchies: ReadonlyMap<string, number>;
invalid: readonly string[];
attempted: number;
omitted: number;
},
) => {
const { incompleteHierarchies, invalid } = counts;
return {
...result,
documents: result.documents.map((document) => ({
...document,
hierarchy_complete:
document.hierarchy_complete &&
!incompleteHierarchies.has(document.relative_path),
})),
graph: {
...result.graph,
coverage: [
...result.graph.coverage,
...result.graph.coverage.map((facet) => {
const omitted = incompleteHierarchies.get(
facet.facet.replace(/^hierarchy:/u, ""),
);
return facet.facet.startsWith("hierarchy:") && omitted !== undefined
? {
...facet,
status: "partial" as const,
reason: "serialized_view_hierarchy_incomplete",
omitted: facet.omitted + omitted,
}
: facet;
}),
{
facet: "archive_decode",
status:
invalid.length > 0 ? ("partial" as const) : ("complete" as const),
reason: invalid.length > 0 ? "one_or_more_archives_invalid" : null,
examined: attempted,
omitted,
examined: counts.attempted,
omitted: counts.omitted,
},
],
truncated: result.graph.truncated || omitted > 0 || invalid.length > 0,
truncated:
result.graph.truncated ||
counts.omitted > 0 ||
invalid.length > 0 ||
incompleteHierarchies.size > 0,
},
limitations: [
...result.limitations,
"Compiled Interface Builder archives are private serialized object graphs. The decoder reports only recognized keyed-archive objects and connections; unrecognized object classes and fields remain unknown.",
...(incompleteHierarchies.size === 0
? []
: [
"Some serialized hierarchy links could not be projected within the bounded hierarchy; their parentage remains unknown.",
]),
...(invalid.length === 0
? []
: [
@@ -99,7 +153,7 @@ export const analyzeInterfaceBuilderBundle = async (input: {
};
/** Project decoded NIB records into the same bounded graph input as ibtool. */
const projectNibArchive = (archive: NibArchiveDocument): JsonValue => {
const projectNibArchive = (archive: NibArchiveDocument) => {
const byId = new Map(archive.objects.map((object) => [object.id, object]));
const dereference = (value: JsonValue | undefined): JsonValue | undefined => {
if (typeof value !== "object" || value === null || Array.isArray(value))
@@ -181,7 +235,7 @@ const projectNibArchive = (archive: NibArchiveDocument): JsonValue => {
}),
);
const connections: Record<string, JsonValue[]> = {};
const hierarchy: JsonValue[] = [];
const hierarchy: NibHierarchyNode[] = [];
const reference = (value: JsonValue | undefined): number | null => {
if (typeof value !== "object" || value === null || Array.isArray(value))
return null;
@@ -214,23 +268,33 @@ const projectNibArchive = (archive: NibArchiveDocument): JsonValue => {
});
}
}
let hierarchyOmitted = 0;
const hierarchyFor = (
objectId: number,
seen: Set<number>,
depth: number,
): JsonValue | null => {
if (depth > 32 || seen.has(objectId)) return null;
): NibHierarchyNode | null => {
if (depth > 32 || seen.has(objectId)) {
hierarchyOmitted += 1;
return null;
}
const object = byId.get(objectId);
if (object === undefined) return null;
if (object === undefined) {
hierarchyOmitted += 1;
return null;
}
const nextSeen = new Set(seen).add(objectId);
const children: JsonValue[] = [];
const children: NibHierarchyNode[] = [];
for (const [key, value] of Object.entries(object.values)) {
if (!/(?:subviews|contentview|childviewcontrollers|view)$/iu.test(key))
continue;
const direct = reference(value);
if (direct === null) continue;
const target = byId.get(direct);
if (target === undefined) continue;
if (target === undefined) {
hierarchyOmitted += 1;
continue;
}
if (/array|set/iu.test(target.class_name)) {
for (const candidate of Object.values(target.values)) {
const child = reference(candidate);
@@ -254,14 +318,22 @@ const projectNibArchive = (archive: NibArchiveDocument): JsonValue => {
const root = hierarchyFor(hierarchyRoot, new Set(), 0);
if (root !== null) hierarchy.push(root);
}
return jsonValueSchema.parse({
"com.apple.ibtool.document.objects": objects,
"com.apple.ibtool.document.connections": connections,
"com.apple.ibtool.document.hierarchy": hierarchy,
"com.apple.ibtool.document.classes": Object.fromEntries(
archive.classes.map((name) => [name.replace(/\0+$/u, ""), {}]),
),
});
const viewHierarchy = projectNibViewHierarchy(
archive.objects,
new Set(Object.keys(objects).map(Number)),
);
const merged = mergeNibHierarchies(hierarchy, viewHierarchy.hierarchy ?? []);
return {
omitted: viewHierarchy.omitted + hierarchyOmitted + merged.omitted,
raw: jsonValueSchema.parse({
"com.apple.ibtool.document.objects": objects,
"com.apple.ibtool.document.connections": connections,
"com.apple.ibtool.document.hierarchy": merged.hierarchy,
"com.apple.ibtool.document.classes": Object.fromEntries(
archive.classes.map((name) => [name.replace(/\0+$/u, ""), {}]),
),
}),
};
};
const isInterfaceBuilderArchive = (path: string): boolean => {
@@ -53,6 +53,13 @@ export const ANDROID_APPLICATION_PROVIDER = {
version: "1",
} as const;
/** Provider identity for verified publication of retained website scripts. */
export const WEB_SCRIPT_EXPORT_PROVIDER = {
id: "rea-web-script-export",
name: "REA captured script export",
version: "1",
} as const;
/** Provider identity for deterministic static JavaScript application analysis. */
export const JAVASCRIPT_APPLICATION_PROVIDER = {
id: "rea-javascript-application",
@@ -0,0 +1,251 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Ajv2020 } from "ajv/dist/2020.js";
import { expect, it } from "vitest";
import { z } from "zod";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { javascriptApplicationAnalysisResultSchema } from "../domain/javascriptApplicationAnalysis.js";
import { matchJavaScriptApplicationVersions } from "../domain/javascriptApplicationVersionKeys.js";
import { findApplicationFeatureSeeds } from "../domain/javascriptFeatureSeed.js";
import { traceApplicationFeatureInputSchema } from "../domain/javascriptFeatureTraceSchemas.js";
import { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js";
const analyzeSource = async (
source: string,
extraFiles: Readonly<Record<string, string>> = {},
) => {
const inputPath = await createTestTempDirectory("rea-js-empty-literal-");
await writeFile(join(inputPath, "app.js"), `${source}\n`);
for (const [path, text] of Object.entries(extraFiles))
await writeFile(join(inputPath, path), text);
const result = await analyzeJavaScriptApplication({
input_path: inputPath,
format: "directory",
});
if (!result.ok)
throw new Error(`Expected analysis success: ${result.error.message}`);
return javascriptApplicationAnalysisResultSchema.parse(
result.value.normalized_result,
);
};
it.each([
"fetch('');",
"new WebSocket('');",
"navigator.serviceWorker.register('');",
"new Worker('');",
"const x = require('');",
"import y from ''; y();",
"import('');",
"localStorage.getItem('');",
"localStorage.setItem('', 1);",
"indexedDB.open('');",
"win.loadFile('');",
"win.loadURL('');",
"app.get('', handler);",
"process.on('', () => 1);",
"const { EventEmitter } = require('events'); new EventEmitter().on('', () => 1);",
"const { spawn } = require('child_process'); spawn('/bin/x').on('', () => 1);",
"const { ipcMain } = require('electron'); ipcMain.handle('', () => 1);",
"const { ipcRenderer } = require('electron'); ipcRenderer.invoke('');",
"const { contextBridge } = require('electron'); contextBridge.exposeInMainWorld('', {});",
])("analyzes the legal empty string literal in %s", async (source) => {
await expect(analyzeSource(source)).resolves.toBeDefined();
});
it("keeps exact values for empty literals without synthetic application labels", async () => {
const output = await analyzeSource(
"fetch(''); fetch('\"\"'); fetch('/api'); process.on('', () => 1);",
);
const endpoints = output.graph.nodes
.filter(({ kind }) => kind === "endpoint")
.map(({ identity, observations }) => ({
key: identity.strategy === "artifact-local-key" ? identity.key : null,
labels: observations.map(({ label }) => label),
values: observations.map(({ properties }) => properties.value),
}));
// The empty endpoint has no display label, so it cannot be confused with
// a literal two-quote endpoint; both keep exact identities.
expect(endpoints).toEqual(
expect.arrayContaining([
{ key: "", labels: [null], values: [""] },
{ key: '""', labels: ['""'], values: ['""'] },
{ key: "/api", labels: ["/api"], values: ["/api"] },
]),
);
expect(endpoints).toHaveLength(3);
expect(
output.semantic_graph?.nodes
.filter(({ kind }) => kind === "event")
.map(({ label, properties }) => ({
label,
eventName: properties.event_name,
})),
).toEqual([{ label: '""', eventName: "" }]);
});
it.each([
{ version: 3, sources: [""], names: [], mappings: "AAAA" },
{
version: 3,
sections: [
{
offset: { line: 0, column: 0 },
map: { version: 3, sources: [""], names: [], mappings: "AAAA" },
},
],
},
])("projects an empty source-map source name: %j", async (map) => {
const output = await analyzeSource("//# sourceMappingURL=app.js.map", {
"app.js.map": JSON.stringify(map),
});
const originals = output.graph.nodes
.filter(({ kind }) => kind === "source-module")
.map(({ identity, observations }) => ({
originalSource:
identity.strategy === "source-map-original"
? identity.original_source
: null,
sources: observations.map(({ properties }) => properties.source),
}));
expect(originals).toEqual([{ originalSource: "", sources: [""] }]);
});
it("keeps empty and two-quote source-map names distinct", async () => {
const output = await analyzeSource("//# sourceMappingURL=app.js.map", {
"app.js.map": JSON.stringify({
version: 3,
sources: ["", '""'],
names: [],
mappings: "AAAA,CCAA",
}),
});
expect(
output.graph.nodes
.filter(({ kind }) => kind === "source-module")
.map(({ identity }) =>
identity.strategy === "source-map-original"
? identity.original_source
: null,
)
.sort(),
).toEqual(["", '""']);
});
it("does not match an empty endpoint with a literal two-quote seed", async () => {
const output = await analyzeSource("fetch(''); fetch('\"\"');");
const endpoints = new Map(
output.graph.nodes
.filter(({ kind }) => kind === "endpoint")
.map((node) => [
node.node_id,
node.identity.strategy === "artifact-local-key"
? node.identity.key
: null,
]),
);
const matches = findApplicationFeatureSeeds(output.graph.nodes, {
kind: "string",
value: '""',
match: "exact",
case_sensitive: true,
}).filter(({ node_id: nodeId }) => endpoints.has(nodeId));
expect(matches.map(({ node_id: nodeId }) => endpoints.get(nodeId))).toEqual([
'""',
]);
});
const IPC_SOURCE =
"const { ipcMain } = require('electron'); ipcMain.handle('', () => 1); ipcMain.handle('\"\"', () => 2);";
const ipcChannels = (
nodes: Awaited<ReturnType<typeof analyzeSource>>["graph"]["nodes"],
) =>
new Map(
nodes
.filter(({ kind }) => kind === "ipc-channel")
.map((node) => [node.node_id, node.observations[0]?.properties.channel]),
);
it("selects an empty IPC channel with an exact empty seed", async () => {
const output = await analyzeSource(IPC_SOURCE);
const channels = ipcChannels(output.graph.nodes);
const matches = findApplicationFeatureSeeds(output.graph.nodes, {
kind: "channel",
value: "",
match: "exact",
case_sensitive: true,
});
expect(matches.map(({ node_id: nodeId }) => channels.get(nodeId))).toEqual([
"",
]);
});
const seedSchema = traceApplicationFeatureInputSchema.shape.seed;
const validateAdvertisedSeed = new Ajv2020({
strict: false,
validateFormats: false,
}).compile(z.toJSONSchema(seedSchema, { io: "input" }));
it.each([
[{ kind: "channel", value: "", match: "exact" }, true],
[{ kind: "string", value: "", match: "exact" }, true],
[{ kind: "channel", value: "/x" }, true],
[{ kind: "string", value: "" }, false],
[{ kind: "channel", value: "" }, false],
[{ kind: "channel", value: "", match: "contains" }, false],
[{ kind: "node-id", value: "", match: "exact" }, false],
] as const)(
"validates empty seed %j at runtime and in the advertised schema",
(seed, accepted) => {
expect(seedSchema.safeParse(seed).success).toBe(accepted);
expect(validateAdvertisedSeed(seed)).toBe(accepted);
},
);
it("pairs the same empty IPC channel across application versions", async () => {
const left = await analyzeSource(IPC_SOURCE);
const right = await analyzeSource(`${IPC_SOURCE} // v2`);
const leftChannels = ipcChannels(left.graph.nodes);
const rightChannels = ipcChannels(right.graph.nodes);
const matching = matchJavaScriptApplicationVersions(
left.graph.nodes,
right.graph.nodes,
);
expect(
matching.pairs
.filter(({ left: node }) => node.kind === "ipc-channel")
.map(({ left: leftNode, right: rightNode }) => [
leftChannels.get(leftNode.node_id),
rightChannels.get(rightNode.node_id),
]),
).toEqual(
expect.arrayContaining([
["", ""],
['""', '""'],
]),
);
expect(matching.unmatchedLeft.map(({ kind }) => kind)).not.toContain(
"ipc-channel",
);
});
it("selects an empty context-bridge API with an exact empty seed", async () => {
const output = await analyzeSource(
"const { contextBridge } = require('electron'); contextBridge.exposeInMainWorld('', {}); contextBridge.exposeInMainWorld('\"\"', {});",
);
const apis = new Map(
output.graph.nodes
.filter(({ kind }) => kind === "context-bridge-api")
.map((node) => [node.node_id, node.observations[0]?.properties.api_key]),
);
const matches = findApplicationFeatureSeeds(output.graph.nodes, {
kind: "api",
value: "",
match: "exact",
case_sensitive: true,
});
expect(matches.map(({ node_id: nodeId }) => apis.get(nodeId))).toEqual([""]);
});
@@ -75,7 +75,12 @@ export const analyzeJavaScriptApplicationValidated = async (
} catch (cause: unknown) {
if (cause instanceof ArtifactReaderFailure)
return err(
new ArtifactOperationError(OPERATION, cause.reason, cause.details),
new ArtifactOperationError(
OPERATION,
cause.reason,
cause.details,
cause.message,
),
);
if (cause instanceof z.ZodError)
return err(
+3 -14
View File
@@ -4,6 +4,7 @@ import { analyzeParsedJavaScriptStaticSource } from "../domain/javascriptStaticA
import { analyzeParsedJavaScriptSemantics } from "../domain/javascriptSemanticAnalysis.js";
import { parseJavaScriptSource } from "../domain/javascriptSourceParser.js";
import { hasValidSourceMapContents } from "../domain/sourceMapContents.js";
import { flattenSourceMapLeaves } from "../domain/sourceMapEnvelope.js";
import type {
JavaScriptSourceRange,
JavaScriptSourcePoint,
@@ -300,20 +301,8 @@ const unavailableSourceMap = (
const flattenSourceMaps = (
root: unknown,
): Readonly<Record<string, unknown>>[] | undefined => {
if (!isRecord(root) || root.version !== 3) return undefined;
const maps: Readonly<Record<string, unknown>>[] = [];
const pending = [root];
while (pending.length > 0) {
const map = pending.pop();
if (map === undefined || map.version !== 3) return undefined;
if (Array.isArray(map.sections)) {
for (const section of map.sections) {
if (!isRecord(section) || !isRecord(section.map)) return undefined;
pending.push(section.map);
}
} else maps.push(map);
}
return maps;
const leaves = flattenSourceMapLeaves(root);
return leaves === undefined ? undefined : [...leaves];
};
const invalidSourceMap = (

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