Compare commits

..
Author SHA1 Message Date
N0zoM1z0 c2f5fc6612 docs(hopper): clarify realpath fallback scope 2026-10-06 19:00:55 +08:00
Vinay PrasadandGitHub 6847bc9473 docs(hopper): document HopperTargetLease realpath fallback
Closes #642
2026-10-06 15:08:54 +05:30
N0zoM1z0andGitHub 9bab55337a feat(ghidra): add atomic function annotation edits (#709)
* feat(ghidra): edit function annotations atomically

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

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

* fix(ghidra): preserve qualified function annotation identity

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

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

* test(ghidra): align verification with annotation authority

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

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

* test(ghidra): exercise native Windows runtime boundaries

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

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

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

* fix(javascript): respect object property overwrite order

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

* test(javascript): cover uncertain spread discriminants

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix(native): preserve final demangled symbol whitespace

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

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

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

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

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

* fix(javascript): keep optional parameter input unknown

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

* fix(dotnet): report observed method metadata changes

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

* docs(dotnet): synchronize generated conformance skill digest

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

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

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

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

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

* docs: regenerate catalogs for integrated fix batch

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

---------

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

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

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

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

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

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

* fix(cli): describe memory inspection arguments

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

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

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

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

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

Remove dead re-exports newly flagged by knip 6.39.

* chore: drop stale typedoc and replay references

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

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

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

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

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

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

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

* fix(ci): allow slower real browser startup

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

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

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

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

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

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

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

* refactor(app): split Setup and ProcessOwnership modules

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

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

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

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

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

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

* fix(ci): allow slower real browser startup

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

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

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

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

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

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

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

* refactor(app): split Setup and ProcessOwnership modules

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

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

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

* fix(ci): allow slower real browser startup

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

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

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

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

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

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

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

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

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

* fix(ci): allow slower real browser startup

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

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

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

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

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

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

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

* fix(ci): allow slower real browser startup

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

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

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

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

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

* chore: normalize release product catalog

---------

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

* chore: normalize release product catalog

---------

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

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

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

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

---------

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

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

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

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

---------

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

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

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

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

---------

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

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

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

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

---------

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

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

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

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

---------

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

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

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

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

---------

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

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

* fix(managed): use typed signature parsing failures

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

* chore(managed): refresh signature schema commitments

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

---------

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

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

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

* fix(browser): deduplicate owned popup discovery

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

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

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

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

---------

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

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

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

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

---------

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

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

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

## Verification

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

* style: apply Prettier to the conditional exports differential test

---------

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

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

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

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

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

7 of the new cases fail without this change.

* style: apply Prettier to the HTML base resolution changes

---------

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

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

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

* fix(javascript): preserve projected result binding semantics

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

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

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

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

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

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

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

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

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

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

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

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

Closes #551

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

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

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

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

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

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

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

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

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

* refactor!: simplify runtime tools and remove replay engines

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

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

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

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

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

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

Addresses #548 and #545.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes #530

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

Closes #528

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

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

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

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

---------

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

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

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

---------

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

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

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

---------

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

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

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

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

---------

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

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

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

---------

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

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

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

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

---------

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

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

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

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

---------

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

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

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

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

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

---------

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

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

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

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

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

---------

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

* test(hopper): isolate real inventory verification

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

* fix: preserve address spaces in call path evidence

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

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

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

* test: prioritize real artifact E2E and captured golden regressions

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

* test: prune redundant cases and strengthen boundary assertions

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

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

* fix(catalog): separate portable metadata from native host availability
2026-10-04 16:02:36 +08:00
1200 changed files with 56086 additions and 41781 deletions
+72 -20
View File
@@ -105,7 +105,7 @@ jobs:
if: ${{ !cancelled() }}
with:
name: vitest-report-${{ strategy.job-index }}
path: .vitest-reports/*
path: .vitest/blob/*
include-hidden-files: true
if-no-files-found: error
overwrite: true
@@ -157,7 +157,7 @@ jobs:
- uses: actions/download-artifact@v4
if: needs.changes.outputs.code == 'true' && success()
with:
path: .vitest-reports
path: .vitest/blob
pattern: vitest-report-*
merge-multiple: true
- name: Merge coverage, JUnit, and timing reports
@@ -215,10 +215,31 @@ jobs:
- name: Verify fixtures from a clean checkout
run: npm run verify:fixtures
package-e2e:
name: Package E2E (${{ matrix.os }})
windows-native-build:
name: Build bundled Windows x64 native controls
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Prepare cross compiler
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
- run: npm run build:windows-native
- run: npm run verify:windows-native:artifact
- uses: actions/upload-artifact@v4
with:
name: windows-native-controls
path: native/windows/build/
if-no-files-found: error
package-e2e:
name: Package/install integration (${{ matrix.os }})
needs: [changes, windows-native-build]
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
strategy:
fail-fast: false
matrix:
@@ -231,8 +252,31 @@ jobs:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- uses: actions/download-artifact@v4
with:
name: windows-native-controls
path: native/windows/build/
- run: npm run verify:windows-native:artifact
- run: npm run verify:package
apple-artifacts:
name: Real Apple artifact CLI/MCP E2E
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
runs-on: macos-14
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run verify:keyed-archive
- run: npm run verify:asset-catalog
- name: Real binary metadata and Interface Builder integration
run: npm run verify:apple-dispatch && npm run verify:interface-builder
reconstruction-readiness:
name: Reconstruction readiness
needs: changes
@@ -263,10 +307,14 @@ jobs:
retention-days: 30
javascript-runtime-observation:
name: JavaScript runtime observation
name: JavaScript runtime observation (${{ matrix.os }})
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
@@ -280,7 +328,7 @@ jobs:
windows-curated:
name: Windows capability and package lane
needs: changes
needs: [changes, windows-native-build]
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
runs-on: windows-latest
timeout-minutes: 25
@@ -293,6 +341,11 @@ jobs:
- run: npm ci
- run: npm run build:cached
- run: npm run typecheck
- uses: actions/download-artifact@v4
with:
name: windows-native-controls
path: native/windows/build/
- run: npm run verify:windows-native:artifact
- name: Record named Windows host capabilities
run: node scripts/windows-capabilities.mjs | Out-File -Encoding utf8NoBOM windows-capabilities.json
- name: Validate Windows host capability report
@@ -305,29 +358,33 @@ jobs:
# ignores unmatched paths, so a stale entry would otherwise report green
# while skipping coverage.
#
# Every path here must pass on windows-latest WITHOUT verified Windows
# native authority. src/ghidra/GhidraProvider.test.ts,
# src/ghidra/GhidraProvider.behavior.test.ts successors, and
# tests/boundary/process/providerProcess.test.ts are deliberately absent:
# they require Job Object process ownership and private-runtime DACL
# enforcement, which this runner does not provide by design. Those claims
# belong behind the same approval boundary as the real Ghidra Windows
# workflow, not in the default lane.
# The matching native artifact establishes real Job Object and private
# runtime boundaries for these fixtures. Ghidra and Java are not needed;
# real engine claims remain in the separate controlled-provider workflow.
- name: Run curated Windows capability tests
run: >-
npx vitest run --coverage.enabled=false
tests/boundary/filesystem/binaryTarget.test.ts
tests/boundary/filesystem/setupUnavailableProvider.test.ts
tests/boundary/filesystem/asarInventoryFreshness.test.ts
tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts
tests/conformance/providers/ghidra/windowsCapabilities.test.ts
tests/conformance/providers/ghidra/ghidraWindowsFixture.test.ts
src/ghidra/GhidraInstallation.test.ts
tests/boundary/providers/ghidra/ghidraTransport.test.ts
tests/boundary/providers/ghidra/ghidraTargetSnapshot.test.ts
tests/boundary/providers/ghidra/ghidraLauncher.test.ts
src/windows/WindowsNativeLoader.test.ts
src/windows/WindowsOwnedProcess.test.ts
src/ghidra/GhidraLauncherEnvironment.test.ts
src/process/ProcessOwnership.test.ts
src/process/ProcessOwnership.part2.test.ts
src/process/ProcessOwnership.part3.test.ts
src/process/ProcessOwnership.validation.test.ts
src/application/Update.test.ts
tests/process-global/updateMaintenance.test.ts
tests/conformance/providers/ghidra/realGhidraWindowsWorkflow.test.ts
- run: npm run verify:windows-native
- run: npm run verify:package:windows
- uses: actions/upload-artifact@v4
if: always()
@@ -348,8 +405,3 @@ jobs:
cache: npm
- run: npm ci
- run: npm run docs:check
- uses: actions/upload-artifact@v4
with:
name: api-docs
path: build/api-docs
retention-days: 14
+33 -1
View File
@@ -11,9 +11,29 @@ permissions:
contents: read
jobs:
native-artifact:
name: Build matching Windows native artifact
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Prepare cross compiler
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
- run: npm run build:windows-native
- run: npm run verify:windows-native:artifact
- uses: actions/upload-artifact@v4
with:
name: real-ghidra-windows-native
path: native/windows/build/
if-no-files-found: error
verify:
name: Real Ghidra 12.1.4 Windows x64 PE proof
if: github.ref == 'refs/heads/main'
needs: native-artifact
environment: real-ghidra-windows
runs-on: [self-hosted, Windows, x64, ghidra-12-1-2]
timeout-minutes: 20
@@ -23,13 +43,25 @@ jobs:
with:
node-version-file: .nvmrc
- run: npm ci
- uses: actions/download-artifact@v4
with:
name: real-ghidra-windows-native
path: native/windows/build/
- run: npm run verify:windows-native:artifact
- run: npm run verify:windows-native
- name: Verify all admitted Windows Ghidra operations
env:
REA_ANALYSIS_PROVIDER: ghidra
run: npm run verify:ghidra:windows | Tee-Object -FilePath windows-ghidra-proof.log
- name: Verify ordinary-user CLI and MCP operations
env:
REA_ANALYSIS_PROVIDER: ghidra
run: npm run verify:ghidra:windows:package | Tee-Object -FilePath windows-ghidra-package-proof.log
- uses: actions/upload-artifact@v4
if: always()
with:
name: windows-ghidra-proof
path: windows-ghidra-proof.log
path: |
windows-ghidra-proof.log
windows-ghidra-package-proof.log
retention-days: 30
@@ -1,40 +0,0 @@
name: Real JavaScript replay verification
on:
pull_request:
paths:
- ".github/workflows/real-javascript-replay.yml"
- "scripts/verify-real-javascript-replay.mjs"
- "src/replay/**"
- "src/application/JavaScriptReplay*"
- "src/domain/javascriptReplay.ts"
- "tests/fixtures/replay/**"
- "package.json"
- "package-lock.json"
workflow_dispatch:
concurrency:
group: real-replay-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
HUSKY: "0"
permissions:
contents: read
jobs:
real-replay:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: sudo apt-get update && sudo apt-get install -y bubblewrap
- run: npm run verify:replay
env:
REA_REPLAY_ALLOW_UNAVAILABLE: "true"
+16 -11
View File
@@ -108,6 +108,12 @@ jobs:
- run: npm ci
- name: Prepare Windows native build toolchain
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
- name: Build and verify bundled Windows native controls
run: npm run build:windows-native && npm run verify:windows-native:artifact
- name: Verify packaged release before publishing
run: npm run verify:package
@@ -129,23 +135,22 @@ jobs:
verification_root="$(mktemp -d)"
trap 'rm -rf -- "${verification_root}"' EXIT
cd "${verification_root}"
for attempt in $(seq 1 20); do
published_version="$(npm view "rea-agents@${version}" version --json 2>/dev/null || true)"
for attempt in $(seq 1 80); do
published_version="$(npm view --prefer-online "rea-agents@${version}" version --json 2>/dev/null || true)"
if [ "${published_version}" = "\"${version}\"" ]; then
if npm exec --yes --package="rea-agents@${version}" -- rea --help \
&& npm exec --yes --package="rea-agents@${version}" -- rea --llms \
&& node "${GITHUB_WORKSPACE}/scripts/verify-published-package.mjs" "${version}"; then
exit 0
fi
echo "Published rea-agents@${version} is visible, but CLI verification failed on attempt ${attempt}" >&2
echo "rea-agents@${version} is visible on npm after attempt ${attempt}; verifying the published CLI."
npm exec --yes --package="rea-agents@${version}" -- rea --help
npm exec --yes --package="rea-agents@${version}" -- rea --llms
node "${GITHUB_WORKSPACE}/scripts/verify-published-package.mjs" "${version}"
exit 0
else
echo "rea-agents@${version} is not visible on npm yet (attempt ${attempt}/20)" >&2
echo "rea-agents@${version} is not visible on npm yet (attempt ${attempt}/80; up to 20 minutes)" >&2
fi
if [ "${attempt}" -lt 20 ]; then
if [ "${attempt}" -lt 80 ]; then
sleep 15
fi
done
echo "Published rea-agents@${version} failed registry and CLI verification" >&2
echo "rea-agents@${version} did not become visible on npm within 20 minutes" >&2
exit 1
publish-mcp:
+4 -2
View File
@@ -6,8 +6,7 @@ __pycache__/
# Distribution
dist/
build/
docs/api/
node_modules/
node_modules
*.egg-info/
*.egg
@@ -42,3 +41,6 @@ test-results.xml
# OS
.DS_Store
Thumbs.db
# Local reference sources and analysis artifacts
/_reference/
+2 -2
View File
@@ -1,6 +1,6 @@
{
"_comment": "Test files are intentionally NOT excluded: the gate used to report zero clones because it skipped **/*.test.ts, hiding all test-tree duplication. Current actual is ~2.6% lines across src+ and tests/, down from 3.4% when tests were included. Lower this as duplication is removed.",
"threshold": 2.7,
"_comment": "Test files are intentionally NOT excluded: the gate used to report zero clones because it skipped **/*.test.ts, hiding all test-tree duplication. Current actual is ~2.4% lines across src+ and tests/, down from 3.4% when tests were included. Lower this as duplication is removed.",
"threshold": 2.5,
"reporters": ["consoleFull"],
"ignore": [
"node_modules/**",
+15
View File
@@ -0,0 +1,15 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 80,
"sortImports": false,
"sortPackageJson": false,
"ignorePatterns": [
"dist/**",
"build/**",
"docs/api/**",
"node_modules/**",
"CHANGELOG.md",
"package-lock.json",
"_reference/**"
]
}
+21 -1
View File
@@ -17,6 +17,15 @@
"no-var-requires": "error"
},
"overrides": [
{
"files": [
"scripts/electron-active-hook*.cjs",
"tests/conformance/readiness/electron/*.js"
],
"rules": {
"no-var-requires": "off"
}
},
{
"files": [
"src/{contracts,domain,server}/**/*.ts",
@@ -120,5 +129,16 @@
"node": true,
"es2023": true
},
"ignorePatterns": ["dist", "node_modules", "docs/api", ".codex", ".devin"]
"ignorePatterns": [
"dist",
"build",
"node_modules",
"docs/api",
"coverage",
".turbo",
".cache",
".codex",
".devin",
".factory"
]
}
-5
View File
@@ -1,5 +0,0 @@
dist
docs/api
node_modules
CHANGELOG.md
package-lock.json
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "3.2.1"
".": "4.0.1"
}
+38 -8
View File
@@ -2,7 +2,7 @@
## Product Direction
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper and the bring-your-own Ghidra adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and has an experimental Windows x64 P0 boundary for approved native x86-64 PE applications; it supplies admitted read-only inventory and function-analysis operations but no GUI or mutation authority. Keep provider-specific code out of the domain and application layers.
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper and the bring-your-own Ghidra adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86-64 PE applications; on Linux and macOS it supplies inventory, function analysis, and atomic function annotation edits in an ephemeral database, without modifying executable bytes or controlling a GUI. Windows P0 has no mutation authority. Keep provider-specific code out of the domain and application layers.
Prioritize:
@@ -13,14 +13,14 @@ Prioritize:
Installers must not install or upgrade Homebrew, Node.js, npm, Java, Ghidra, or other unrelated software. Ghidra is bring-your-own. `rea setup` must print its planned changes and require approval before writing files or installing Hopper.
REA is a local-only tool; do not sanitize actionable local diagnostics such as artifact paths, digests, mismatch locations, or analysis metadata, while continuing to redact genuine secrets such as credentials and authorization headers.
REA is a local-only tool. Preserve caller-selected inputs, captured output, URLs, paths, digests, mismatch locations, and analysis metadata. Do not guess that local evidence is secret from environment-variable names, argument names, or text patterns. Redact transport authentication credentials and values the caller explicitly marks sensitive; do not persist the entire ambient environment merely because a child inherits it.
## Project Structure & Module Organization
REA is a layered ESM TypeScript application. Dependencies flow inward from pure domain logic through contracts, providers, application workflows, and CLI/MCP adapters. See [docs/architecture.mermaid](docs/architecture.mermaid) for the component map.
- `src/domain/` owns pure provider-neutral semantics; `src/contracts/` owns caller-visible schemas and the canonical tool inventory.
- `src/hopper/`, `src/ghidra/`, `src/browser/`, `src/replay/`, `src/native/`, `src/artifacts/`, and `src/dotnet/` own provider-specific boundaries. Keep provider protocols out of domain and application code.
- `src/hopper/`, `src/ghidra/`, `src/browser/`, `src/native/`, `src/artifacts/`, and `src/dotnet/` own provider-specific boundaries. Keep provider protocols out of domain and application code.
- `src/application/` composes shared CLI/MCP workflows; `src/server/` translates MCP requests; `src/cli.ts` and `src/main.ts` are the CLI and MCP entry points.
- `src/process/` owns shared process lifecycle primitives, not provider wire protocols. `bridge/` contains provider-side adapters.
- `tests/` contains unit, composition, boundary, acceptance, and conformance tests. `scripts/verify-*` contains real-toolchain checks.
@@ -30,9 +30,12 @@ REA is a layered ESM TypeScript application. Dependencies flow inward from pure
- `npm ci`: install the locked dependencies.
- `npm run build:cached`: build the CLI and MCP server.
- `npm run test:local`: run focused tests for changed code.
- `npm run test:local`: run changed source tests without building; pass exact source test paths to run them regardless of Git status.
- `npm run test:focused -- PATH...`: run exact test files; build first for boundary, acceptance, or process-global tests.
- `npm run check:changed`: run cached static checks and source tests affected since the branch merge base (default `origin/main`).
- `npm run check:fast`: run cached typecheck and lint checks.
- `npm run check:pr`: run the deterministic PR gate and generated-document checks.
- `npm run check:pr`: opt into the complete local deterministic gate and generated-document checks for broad changes; CI owns full coverage. Routine iterations need focused tests and relevant checks, not the whole gate each time.
- `npm run docs:check`: check committed generated documents; `npm run docs:generate` regenerates them.
- For provider-dependent changes, see [docs/testing.md](docs/testing.md) and run the matching real-provider verification.
- Keep each verification lane's prerequisites limited to the claim it checks. Use host-native fixtures for host/provider acceptance; put optional cross-target formats and their external toolchains in a separate lane. Preflight required commands and report the missing dependency and lane clearly.
@@ -44,12 +47,28 @@ Configuration is parsed and validated by `src/config.ts` and `src/config/`. Keep
## Coding Style & Naming Conventions
Use ESM TypeScript, two-space indentation, and Prettier defaults. Keep compiler strictness intact (`strict`, `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`, `verbatimModuleSyntax`). Use `camelCase` for values/functions, `PascalCase` for classes/types, and `UPPER_SNAKE_CASE` for constants. Parse unknown values at every MCP, environment, and subprocess boundary. Avoid `any`, unchecked casts, non-null assertions, import-time I/O, and floating promises. Exported APIs require concise JSDoc. Model expected failures with the tagged error algebra and `Result`, not broad exception wrappers.
Use ESM TypeScript, two-space indentation, and the committed Oxfmt configuration. Keep compiler strictness intact (`strict`, `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`, `verbatimModuleSyntax`). Use `camelCase` for values/functions, `PascalCase` for classes/types, and `UPPER_SNAKE_CASE` for constants. Parse unknown values at every MCP, environment, and subprocess boundary. Avoid `any`, unchecked casts, non-null assertions, import-time I/O, and floating promises. Exported APIs require concise JSDoc. Model expected failures with the tagged error algebra and `Result`, not broad exception wrappers.
## Boundary Contracts
Treat a boundary as a contract between the producer's actual representation and the consumer's required meaning. When implementing or auditing a boundary, trace the value through parsing, normalization, authorization, serialization, and the CLI/MCP result. Establish affected callers from their code paths; similar tool names or workflows do not prove that they share a schema or failure mode.
Keep portable evidence and scenario validation distinct from host-native execution checks. Absolute filesystem paths, file URLs, and HTTP paths have different semantics; do not substitute one platform's syntax for the domain concept. Interpret provider metadata according to its documented or observed producer behavior. When a transformation loses information, preserve the reported value and an explicit unknown rather than guessing a canonical identity.
Preserve meaningful failure reasons through application and adapter layers. Malformed input is distinct from an unsupported target, unavailable provider, or host operating-system permission denial. Diagnostics should identify the failed constraint and the target or lifecycle request it applies to. Recovery advice must address that reason and point to an available workflow; generic catches must not erase actionable validation details.
Leave meaningful target, action, capture, and output choices to the agent. A selected operation already expresses intent; do not require approval booleans or repeated permission declarations. Trace each setting to its consumer: remove ignored options and single-value confirmations, derive built-in lifecycle behavior, and supply defaults for omitted optional metadata. Report actual effects and limitations where they help interpret results rather than asking callers to restate them.
Verify the representation consumed at the next boundary. Internal validator success does not establish JSON Schema validity or client compatibility: validate advertised input and output schemas against their declared dialect after SDK conversion, and keep generated contracts aligned. Use representative producer data and regressions that exercise the failed behavior. Platform and provider support claims require the corresponding real workflow; package startup, capability probes, and mock transport tests establish only their narrower claims. Report unverified coverage explicitly.
Bind tool handlers to named contracts, preserving their exact input and output types. Catalog ordering is presentation metadata; changing that order must not select a different schema or operation for an existing handler.
## Designing MCP Tools
Start from the analyst question and desired result, not a provider API. Before adding a tool, inspect the existing contract and its nearest alternative.
Let agents compose experiments with ordinary commands, scripts, and local fixture servers. REA tools should perform the requested inspection or capture and return evidence. A custom orchestration language, replay engine, or separate prepare/execute plan needs an observed requirement that those primitives cannot satisfy. A plan-only tool without an executor does not establish runtime behavior; expose the useful inspection directly.
- Prefer reusable, composable primitives: inspect one explicit object or
relationship and return evidence that can support different analyst
questions. Compose primitives into a workflow only for a recurring outcome
@@ -57,7 +76,7 @@ Start from the analyst question and desired result, not a provider API. Before a
business-domain interpretations out of general tool contracts.
- Use **inspect/search** tools for facts about a target or candidate set; use **trace** tools for relationships; use **compare** tools for explicitly paired inputs.
- Add a **workflow** when observed agent use shows a repeated sequence that REA can compose without losing analyst control. Return useful results inline so callers can choose their next action.
- Use **observe/capture/replay** only when runtime activity is required, and declare authority and lifecycle effects in the contract.
- Use **observe/capture** only when runtime activity is required, and declare authority and lifecycle effects in the contract.
- Extend an existing tool when intent and result contract are unchanged. Add a tool for a distinct analyst outcome or materially different authority.
- Keep caller-facing names and results provider-neutral. Put engine-specific behavior in provider adapters and report each provider's exact coverage.
- Keep results complete by default. Add a limit only when it follows from a real format, protocol, authority, or resource-safety constraint; explain truncation and unsupported facets. Keep observed, derived, inferred, and unknown results distinct.
@@ -68,10 +87,21 @@ See [docs/tool-design.md](docs/tool-design.md) for the design checklist. When us
## Testing Guidelines
Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use fake launcher/socket seams; MCP tests connect with the client SDK version pinned in `package.json`. Preserve the canonical tool inventory defined by `TOOL_CONTRACTS` and verified through `CATALOG_IDENTITY` and generated product metadata. Cover malformed input, cancellation, lifecycle cleanup, and actual format, protocol, and permission boundaries. Do not add tests that merely freeze arbitrary caps or prescribed call sequences. Real Hopper, Ghidra, browser, JavaScript replay, managed conformance, and any real managed-tool claims cannot be replaced by mocks; use the corresponding `verify:*` command.
Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use fake launcher/socket seams; MCP tests connect with the client SDK version pinned in `package.json`. Preserve the canonical tool inventory defined by `TOOL_CONTRACTS` and verified through `CATALOG_IDENTITY` and generated product metadata. Cover malformed input, cancellation, lifecycle cleanup, and actual format, protocol, host-permission, and target-identity boundaries. Do not add tests that merely freeze arbitrary caps or prescribed call sequences. Real Hopper, Ghidra, browser, managed conformance, and any real managed-tool claims cannot be replaced by mocks; use the corresponding `verify:*` command.
Keep tool catalogs complete and self-describing; prefer capability- and session-scoped availability over schema truncation. Serialized bytes alone do not measure agent usability or model context cost.
## Commit & Pull Request Guidelines
Use Conventional Commit subjects because Release Please derives versions and changelogs from them. Examples: `feat: add historical source import`, `fix(process): stop timers after exit`, and `docs: update architecture`. Use `!` or a `BREAKING CHANGE:` footer for breaking changes. Pull request titles must follow the same format because squash merges use the title as the release commit. Pull requests should describe contract or behavior changes, list verification commands, link issues, and include sanitized MCP examples when schemas change. State whether real Hopper/Ghidra verification was performed. Never commit binaries, Hopper or Ghidra project documents, credentials, `dist/`, `node_modules/`, or local planning artifacts (e.g. `.codex/`).
<!-- BEGIN:turborepo-agent-rules -->
# This is NOT the Turborepo you know
Turborepo configuration, task behavior, and CLI commands can vary between installed versions and may differ from your training data. Resolve the `turbo` package from this file's directory or relevant workspace; in monorepos, it may not be visible from the repository root. For example, run `node -p "require.resolve('turbo/package.json')"` from a workspace that depends on `turbo`.
Read `docs/README.md` inside that installed package first, then read the relevant pages from its `docs/` directory before changing Turborepo configuration or commands. Heed deprecation notices. These bundled docs match the installed package version and are available without network access.
This block is written and re-added by `turbo` before repository-scoped commands when an AI agent is detected. In the Turborepo source repository, its template is defined in `crates/turborepo-cli/src/cli/agent_guidance.rs`. Removing the managed block while updates are enabled means a later qualifying invocation will add it again. Set `"agentGuidance": false` in the root `turbo.json` or `turbo.jsonc` to opt out; this does not remove an existing block. Keep the block committed with your work to avoid an uncommitted change on the next agent invocation.
<!-- END:turborepo-agent-rules -->
+134
View File
@@ -1,5 +1,139 @@
# Changelog
## [4.0.1](https://github.com/morluto/rea/compare/rea-agents-4.0.0...rea-agents-4.0.1) (2026-10-05)
### Bug Fixes
* **release:** allow npm propagation time ([2ef02ce](https://github.com/morluto/rea/commit/2ef02ced1603809f0fc6984612de31dce1801dd8))
## [4.0.0](https://github.com/morluto/rea/compare/rea-agents-3.2.1...rea-agents-4.0.0) (2026-10-05)
### ⚠ BREAKING CHANGES
* Managed comparison metadata adds exact-signature matching and its count, and name_matching now reports exact-signature-fallback.
* **setup:** unscoped setup refreshes only existing REA-owned registrations. Use --all-detected to configure every detected client. Setup JSON contains a compact doctor summary; use rea doctor for full diagnostics. Cancellation and successful dry runs now exit successfully.
* Remove controlled replay, Node characterization, finite replay-machine, managed runtime planning, and redundant string-tracing tools. Retire process replay, shims, reactive scenarios, custom checkpoints, and active browser replay and origin-scope inputs.
* **tools:** Removed permission configuration and policy commands, approval fields, filesystem scope inputs, and fixed confirmation options. Process capture inherits the host environment and accepts command names; callers must refresh tool schemas and migrate removed fields.
### Features
* **ghidra:** add DOS MZ analysis and complete function extents ([#556](https://github.com/morluto/rea/issues/556)) ([4f099a1](https://github.com/morluto/rea/commit/4f099a10f819125d52bd8baed9a9018496212eab))
* **native:** add inspection primitives, dispatch traces and approved UI observation ([#500](https://github.com/morluto/rea/issues/500)) ([4fbc501](https://github.com/morluto/rea/commit/4fbc501121e96b33665a76cad2e5c3e8ac2e2c6d))
* **setup:** expand agent integrations and verify updates ([#577](https://github.com/morluto/rea/issues/577)) ([6256b68](https://github.com/morluto/rea/commit/6256b6866616fa0c3137a77ebd31cf34155a642f))
### Bug Fixes
* **artifacts:** break directory child collation ties ([#587](https://github.com/morluto/rea/issues/587)) ([b37e47c](https://github.com/morluto/rea/commit/b37e47c9f729a66008b1dc8b0a1bb266e79c2776))
* **artifacts:** exclude removed paths from unchanged counts ([#569](https://github.com/morluto/rea/issues/569)) ([52bf709](https://github.com/morluto/rea/commit/52bf709bcefb17ab07e1f9c47c5fadf5eca9bd4e))
* **artifacts:** expand nested ASARs on Windows ([#582](https://github.com/morluto/rea/issues/582)) ([ebb4c45](https://github.com/morluto/rea/commit/ebb4c45b6892ab60f2c81e05fbb4cb528ffbffc4))
* **artifacts:** interrupt ZIP extraction under backpressure ([#594](https://github.com/morluto/rea/issues/594)) ([50f7e81](https://github.com/morluto/rea/commit/50f7e81432001239ca8b5de3a9cfe8f5980d2711))
* **artifacts:** preserve archive entry types and host paths ([#581](https://github.com/morluto/rea/issues/581)) ([28e0f4e](https://github.com/morluto/rea/commit/28e0f4e9ec09ae35b26920e4c6925f48770bf5ed))
* **artifacts:** refresh cached ASAR headers for each inventory ([#574](https://github.com/morluto/rea/issues/574)) ([f5013c5](https://github.com/morluto/rea/commit/f5013c50ca81795f91fd945fae2fb76f9dbe0f97))
* **artifacts:** resolve parents after archive enumeration ([#578](https://github.com/morluto/rea/issues/578)) ([8eac290](https://github.com/morluto/rea/commit/8eac290e8d716817526ab7f628e83c75fb33d06b))
* **artifacts:** respect XML plist byte-order marks ([#514](https://github.com/morluto/rea/issues/514)) ([23c4eba](https://github.com/morluto/rea/commit/23c4eba5b38b8de4f9547d879f2c3e54f741b1e1))
* **artifacts:** retain expanded ASAR container parents ([#583](https://github.com/morluto/rea/issues/583)) ([00fa974](https://github.com/morluto/rea/commit/00fa97422f585db878ffe5d1613c56608916f972))
* **artifacts:** retain relative paths in directory identities ([#580](https://github.com/morluto/rea/issues/580)) ([348323d](https://github.com/morluto/rea/commit/348323dd23360bbac3841a9632ffcd08290566d2))
* **browser:** capture popup network and report early event gaps ([#614](https://github.com/morluto/rea/issues/614)) ([f557207](https://github.com/morluto/rea/commit/f557207887ca9336f3d3c5fc02c76c354c1b2945))
* **browser:** capture selected events in popup pages ([#600](https://github.com/morluto/rea/issues/600)) ([a3bf4a0](https://github.com/morluto/rea/commit/a3bf4a0944ce5a52e100470273b91fdd48ebce49))
* **browser:** derive source-map imports from syntax nodes ([#588](https://github.com/morluto/rea/issues/588)) ([bae87b4](https://github.com/morluto/rea/commit/bae87b4ded343b34c4226b5e5d491145db7f85ff))
* **browser:** distinguish duplicate screenshot frame events ([#612](https://github.com/morluto/rea/issues/612)) ([1bd641d](https://github.com/morluto/rea/commit/1bd641d39f58461566e4a3f07fa00f84f34e87ad))
* **browser:** ignore duplicate frame navigations and read block-form source map directives ([#546](https://github.com/morluto/rea/issues/546)) ([828395d](https://github.com/morluto/rea/commit/828395d510bd4f29525e5a0807a550a2daad885c))
* **browser:** keep subresource redirects out of page navigation scope ([#524](https://github.com/morluto/rea/issues/524)) ([9c28f3c](https://github.com/morluto/rea/commit/9c28f3c4c0840fd7424dc669db1fb9baa1dde5d7))
* **browser:** preserve RGB PNG transparent samples ([#595](https://github.com/morluto/rea/issues/595)) ([9997152](https://github.com/morluto/rea/commit/9997152c2d3127162161a2b945f506f0e834be38))
* **browser:** release cancelled CDP cleanup without waiting for responses ([#517](https://github.com/morluto/rea/issues/517)) ([488fd5f](https://github.com/morluto/rea/commit/488fd5f60be3b2900c27a915c73322b9cb5eabb5))
* **browser:** release discarded source-map response bodies ([#611](https://github.com/morluto/rea/issues/611)) ([8642055](https://github.com/morluto/rea/commit/864205570a0a9452f96c11feacd00338d026f17b))
* **browser:** resolve redirected source map paths from final URL ([#518](https://github.com/morluto/rea/issues/518)) ([056c123](https://github.com/morluto/rea/commit/056c1237591de3710cb96a51ddd71fa94317b906))
* **cli:** fail commands returning projected analysis errors ([#506](https://github.com/morluto/rea/issues/506)) ([2fd1196](https://github.com/morluto/rea/commit/2fd11961cfba0c2d211a1aa643cbf05fa4e8c20d))
* **cli:** reject invalid UTF-8 JSON file bytes ([#613](https://github.com/morluto/rea/issues/613)) ([22acb0d](https://github.com/morluto/rea/commit/22acb0d5d730379fbe49e9e491f446e11425190e))
* **comparison:** account for unpaired records after explicit bundle pairing ([#603](https://github.com/morluto/rea/issues/603)) ([12a2e37](https://github.com/morluto/rea/commit/12a2e3793af63542448124311e17eacbd5235fc0))
* **comparison:** match generated names without swallowing symbol prefixes ([#596](https://github.com/morluto/rea/issues/596)) ([5f3ba3e](https://github.com/morluto/rea/commit/5f3ba3e6886b6b3186711e025da14875dc3c9684))
* **comparison:** normalize CFG identity by numeric addresses ([#593](https://github.com/morluto/rea/issues/593)) ([d1e5c0e](https://github.com/morluto/rea/commit/d1e5c0e1fd04d2d5a0d663cbe43787a010e3d783))
* **comparison:** require decoded signatures for structural method matches ([#604](https://github.com/morluto/rea/issues/604)) ([4a98c3e](https://github.com/morluto/rea/commit/4a98c3ea035480561d138d79288974a860c2aa22))
* **conformance:** preserve own JSON keys during comparison ([#573](https://github.com/morluto/rea/issues/573)) ([3460eda](https://github.com/morluto/rea/commit/3460eda9ca01ccf41e3b9895f3cbe975c0eaed51))
* correct native inventories and bridge error classification ([#503](https://github.com/morluto/rea/issues/503)) ([405732a](https://github.com/morluto/rea/commit/405732a7f55e3033c29533b18f7d8313dbd28570))
* **docs:** allow README installation wording to vary ([92617f9](https://github.com/morluto/rea/commit/92617f965720aaa74ec20ad67291adc5e83677a1))
* **ghidra:** avoid nested quotes in Windows Java options ([a574b74](https://github.com/morluto/rea/commit/a574b744b8fe95e906524684f6a3b28cdc656499))
* **ghidra:** recover evidenced switch cases and default targets ([#579](https://github.com/morluto/rea/issues/579)) ([b225b0f](https://github.com/morluto/rea/commit/b225b0f64bfa86586c17f193d30e278d8124b14e))
* **inspector:** preserve lossy Node discovery locations ([#542](https://github.com/morluto/rea/issues/542)) ([84e4361](https://github.com/morluto/rea/commit/84e4361c0936a4bb30951699fb68970d4d7a2327)), closes [#530](https://github.com/morluto/rea/issues/530)
* **javascript:** avoid invented computed destructuring properties ([#507](https://github.com/morluto/rea/issues/507)) ([80777d9](https://github.com/morluto/rea/commit/80777d96daf64d0266e5cf3f9aad9338b97be4ff))
* **javascript:** bind named class expressions in their class scope ([#510](https://github.com/morluto/rea/issues/510)) ([55f0267](https://github.com/morluto/rea/commit/55f0267c409cbf096bcdbd07d7604c44bcde3fbe))
* **javascript:** classify assignment targets and retain compound-assignment reads ([#537](https://github.com/morluto/rea/issues/537)) ([333d118](https://github.com/morluto/rea/commit/333d118c0fa65c8c418baaa337270c0ad25cd7cc))
* **javascript:** correct lexical and dynamic scope analysis ([#538](https://github.com/morluto/rea/issues/538)) ([6fa6f1f](https://github.com/morluto/rea/commit/6fa6f1f9b17318220abb2683c327146785dcc5d3))
* **javascript:** honor computed keys when naming callables, exports, requests and options ([#536](https://github.com/morluto/rea/issues/536)) ([4c8dc39](https://github.com/morluto/rea/commit/4c8dc39f439b5283b077c6aa47f262714d7c4bbd))
* **javascript:** honor package exports condition order ([#591](https://github.com/morluto/rea/issues/591)) ([8289595](https://github.com/morluto/rea/commit/82895959d1494f16bc59909a2575facf3ab19d79))
* **javascript:** isolate lexical loop bindings ([#511](https://github.com/morluto/rea/issues/511)) ([c30878f](https://github.com/morluto/rea/commit/c30878f616c4496c6cd1993d677aa019b2412b7e))
* **javascript:** prefer package entrypoints over directory indexes ([#575](https://github.com/morluto/rea/issues/575)) ([bb91abd](https://github.com/morluto/rea/commit/bb91abd3df52802ec8f9d3d1fb215dce5eb6ea49))
* **javascript:** preserve literal CommonJS path punctuation ([#576](https://github.com/morluto/rea/issues/576)) ([063cd25](https://github.com/morluto/rea/commit/063cd25a93ee9f16b45635fea196dde0d9309ab9))
* **javascript:** preserve Node exports fallback stop reasons ([#606](https://github.com/morluto/rea/issues/606)) ([adc07f5](https://github.com/morluto/rea/commit/adc07f5eaab005f013c88617aa09b6f2b4af4111))
* **javascript:** respect HTML base URL components ([#589](https://github.com/morluto/rea/issues/589)) ([ffbde92](https://github.com/morluto/rea/commit/ffbde92f9ab65a868b660abf17ce9d3c4577d125))
* **javascript:** respect lexical shadowing of require ([#509](https://github.com/morluto/rea/issues/509)) ([6a431d8](https://github.com/morluto/rea/commit/6a431d806a1574a660b96ba49412b24df9e93b50))
* **javascript:** retain property reads during updates ([#508](https://github.com/morluto/rea/issues/508)) ([6b7923c](https://github.com/morluto/rea/commit/6b7923cdd2a099d1481783f34970a3d1a71a8206))
* **managed:** classify truncated signatures as malformed ([#598](https://github.com/morluto/rea/issues/598)) ([1b6f054](https://github.com/morluto/rea/commit/1b6f05470d6fb1903346ef7a25fb5f9d23079cb7))
* **managed:** distinguish metadata handles from member accesses ([#607](https://github.com/morluto/rea/issues/607)) ([c73742f](https://github.com/morluto/rea/commit/c73742f3003ec6c1469f77ecc9ccb093df636b4a))
* **managed:** recognize PE Thumb and ARMNT machines ([#605](https://github.com/morluto/rea/issues/605)) ([0ce529a](https://github.com/morluto/rea/commit/0ce529af4e0280fcbd4e647d0fcb0a9ca9f3496e))
* **mcp:** emit valid schemas for empty arrays ([#535](https://github.com/morluto/rea/issues/535)) ([4007b20](https://github.com/morluto/rea/commit/4007b20fc277b4d20b52cae328dea58ae033be63)), closes [#528](https://github.com/morluto/rea/issues/528)
* **mcp:** return complete inline Evidence records ([#585](https://github.com/morluto/rea/issues/585)) ([ea9c3da](https://github.com/morluto/rea/commit/ea9c3dab6b2ff4fa943c8ca4fec7ae8b304dbd93)), closes [#551](https://github.com/morluto/rea/issues/551)
* **native:** capture Mach-O header metadata alongside load commands ([#525](https://github.com/morluto/rea/issues/525)) ([a8195ba](https://github.com/morluto/rea/commit/a8195ba2cc34a37f68d7bb8a8087d73ace40984b))
* **native:** decode little-endian fat headers ([bede3e3](https://github.com/morluto/rea/commit/bede3e3c60e53d445209096b360a81eaa0c446cc)), closes [#592](https://github.com/morluto/rea/issues/592)
* **native:** decode Mach protection bits in their proper positions ([#586](https://github.com/morluto/rea/issues/586)) ([6aba6ae](https://github.com/morluto/rea/commit/6aba6ae2316fe6299c855a3db52b221fff63c00c))
* **native:** inspect Apple dispatch metadata in FAT64 binaries ([#590](https://github.com/morluto/rea/issues/590)) ([f3e6582](https://github.com/morluto/rea/commit/f3e6582e9ed327c5accb8f9068ea0eb9bac49b1f))
* **native:** inspect the selected universal Mach-O architecture ([#601](https://github.com/morluto/rea/issues/601)) ([2b7bb3b](https://github.com/morluto/rea/commit/2b7bb3bb6a87704c18469768bc82d87e97b5c99c))
* **native:** match compound otool keys exactly and badge unknown thread entrypoints ([#534](https://github.com/morluto/rea/issues/534)) ([d40502a](https://github.com/morluto/rea/commit/d40502aa3b9a8c87fde49e5887547066128f19ba))
* **native:** normalize otool line endings and strip plist JSON BOM ([#533](https://github.com/morluto/rea/issues/533)) ([3e4d4d9](https://github.com/morluto/rea/commit/3e4d4d93fdc3eb5fdffda3a8dc8bec43201cdd98))
* **native:** preserve text-valued load command metadata ([#520](https://github.com/morluto/rea/issues/520)) ([bcfb407](https://github.com/morluto/rea/commit/bcfb4072f2ff665e93477206e14ce4b0581f97b9))
* **native:** reject signal-terminated command captures ([#608](https://github.com/morluto/rea/issues/608)) ([b81d18e](https://github.com/morluto/rea/commit/b81d18eeee9d93dafcb15e3d723de951509c2612))
* **native:** reject trailing text in lipo integers and keep multi-word otool keys ([#531](https://github.com/morluto/rea/issues/531)) ([9e30d93](https://github.com/morluto/rea/commit/9e30d93af3d2c0cc1a7536a10e3141f9f2fb499c))
* **native:** retain lazy dylibs, normalize version-min builds, limit thread entrypoints ([#532](https://github.com/morluto/rea/issues/532)) ([0f77ada](https://github.com/morluto/rea/commit/0f77ada55e6d1d01cb925993d871bf4a94f94bb2))
* **native:** retain re-exported Mach-O library dependencies ([#516](https://github.com/morluto/rea/issues/516)) ([4a5673e](https://github.com/morluto/rea/commit/4a5673e1dc6f2213cf94121384cff405e390677a))
* **package:** separate agent and Hopper setup support ([#584](https://github.com/morluto/rea/issues/584)) ([205a977](https://github.com/morluto/rea/commit/205a9777776476b4f361e38f25d1c75baaccd420))
* **platform:** accept absolute paths for any host and stop blaming the ceiling for a missing grant ([#552](https://github.com/morluto/rea/issues/552)) ([e793e12](https://github.com/morluto/rea/commit/e793e124a27b8087057f58f39028144ea6f6e173))
* **process:** align provider detach with configured platform ([3fff74f](https://github.com/morluto/rea/commit/3fff74fb5bb9725be2456e8c39bc0b98181d0687))
* repair boundary contracts across CLI and MCP ([#597](https://github.com/morluto/rea/issues/597)) ([9c25dc4](https://github.com/morluto/rea/commit/9c25dc4ed2915a81fc83a1d4dc22e7ba814b8dff))
* **replay:** keep __proto__ own properties and reject symbol-keyed results ([#540](https://github.com/morluto/rea/issues/540)) ([05bb630](https://github.com/morluto/rea/commit/05bb630a00aa521229cde53ded9a5345b8bd1a2b))
* **replay:** link the complete ESM graph before evaluating ([#515](https://github.com/morluto/rea/issues/515)) ([4200a42](https://github.com/morluto/rea/commit/4200a425d50eab80847edf3baa9ea4c5244e8be8))
* **replay:** preserve CommonJS object as ESM default export ([#519](https://github.com/morluto/rea/issues/519)) ([2df6b51](https://github.com/morluto/rea/commit/2df6b51ebb87e8d406d6201d8f3158aefefa7cc5))
* **replay:** preserve Date call and explicit constructor semantics ([#512](https://github.com/morluto/rea/issues/512)) ([9975bd2](https://github.com/morluto/rea/commit/9975bd23cdba7e196ca97c3fcec41a6eee925e57))
* **replay:** preserve sparse array positions in result projection ([#522](https://github.com/morluto/rea/issues/522)) ([9d1c363](https://github.com/morluto/rea/commit/9d1c3631b8deccb5fb91906130f38e3f24aa254e))
* **replay:** share one seeded generator and present builtins with real identity ([#539](https://github.com/morluto/rea/issues/539)) ([5b51fa0](https://github.com/morluto/rea/commit/5b51fa01e2391cd4b6d5e9ba7b6aa7615de891b1))
* **repo:** keep dependency links out of version control ([#617](https://github.com/morluto/rea/issues/617)) ([b3223e3](https://github.com/morluto/rea/commit/b3223e3f61a23b79152939ef755aca95e7640f09))
* **skill:** remove obsolete grants and audit tool effects ([2919def](https://github.com/morluto/rea/commit/2919def828b3c8fc7947989f1de3f4f4ac4cdd36))
* **test:** improve capture reliability and runner cleanup ([#609](https://github.com/morluto/rea/issues/609)) ([d19686a](https://github.com/morluto/rea/commit/d19686a2a5122c691f7885997906ecb31dd9827d))
* **tools:** simplify direct invocation and harden boundary contracts ([#555](https://github.com/morluto/rea/issues/555)) ([b5e9891](https://github.com/morluto/rea/commit/b5e98915c60968213141f33839e0c5c4b34df4db))
* **web:** retain uncertainty for incomplete capture inventories ([#513](https://github.com/morluto/rea/issues/513)) ([d8c18f6](https://github.com/morluto/rea/commit/d8c18f604f292c95b6b0b185d23517cb6881c5fb))
* **win32:** probe .cmd/.bat runtime shims via cmd.exe to avoid spawn EINVAL ([#505](https://github.com/morluto/rea/issues/505)) ([31ed005](https://github.com/morluto/rea/commit/31ed005183c315f1bc72f6e10730a47be73d7f1c))
* **workflows:** preserve punctuation in filesystem source paths ([#521](https://github.com/morluto/rea/issues/521)) ([5b28a8b](https://github.com/morluto/rea/commit/5b28a8b331259e026678e681210e9c3c4f48f2fa))
* **workflows:** retain container presence in export shape comparisons ([#523](https://github.com/morluto/rea/issues/523)) ([3854458](https://github.com/morluto/rea/commit/385445804ab45605a47edcb996ebbc026e2f0a43))
### Code Refactoring
* **config:** take the environment as an input at every configuration read ([#554](https://github.com/morluto/rea/issues/554)) ([53fe7d6](https://github.com/morluto/rea/commit/53fe7d6d3b3630472124563ccafdad3f4964b4bc))
* **domain:** consolidate duplicated semantic member and literal helpers ([#550](https://github.com/morluto/rea/issues/550)) ([09830da](https://github.com/morluto/rea/commit/09830dad053f2b1440e0b1cde63747606684be4b))
* **domain:** own digest and prefixed identifier shapes in one module ([#547](https://github.com/morluto/rea/issues/547)) ([ea58834](https://github.com/morluto/rea/commit/ea5883494036ddb30d138fdb381d54b2d9808b49))
* **host:** inject platform and environment instead of reading them ambiently ([#549](https://github.com/morluto/rea/issues/549)) ([5b59b87](https://github.com/morluto/rea/commit/5b59b87726b459cf9887dd4edc176981ef4e6afc))
* simplify runtime tools and remove replay engines ([#572](https://github.com/morluto/rea/issues/572)) ([e07b816](https://github.com/morluto/rea/commit/e07b81649e96668e5ce166cfc6db3b59a7777697))
### Documentation
* add boundary contract guidance ([42e6a8a](https://github.com/morluto/rea/commit/42e6a8a6f6da4c3aebb10235a27d471dc2590954))
* add Discord community links to README ([79bf26f](https://github.com/morluto/rea/commit/79bf26f33f980c20367986080a16d4a9f7952048))
* align roadmap with direct tool use ([eba709e](https://github.com/morluto/rea/commit/eba709e5c096796b0afe3ce9203659df9eaf0a7d))
* align translated READMEs and clarify support guides ([5b383b2](https://github.com/morluto/rea/commit/5b383b2ddd4195cb15e412b81853955b38419721))
* clarify README wording and analysis tool support ([97822f2](https://github.com/morluto/rea/commit/97822f233be49a8b582b4c2aa982af3854ca0bb2))
* move README image above Discord community ([ad86d39](https://github.com/morluto/rea/commit/ad86d39d2cb4f2d1ce0da08a17d5f7948c0797ec))
* place community invitation below README navigation ([69a45e0](https://github.com/morluto/rea/commit/69a45e0bbc03d87624de866a821b976e4aaca069))
* place Discord community below setup command ([778e464](https://github.com/morluto/rea/commit/778e464c206ba75a2f94edaad3c05e8bf63bed10))
* place setup command above README image ([c6d3bdf](https://github.com/morluto/rea/commit/c6d3bdf597d9cd75a6f7396236e1a47f5ee4288d))
### Tests
* **setup:** isolate client fixtures across host platforms ([f73bb0f](https://github.com/morluto/rea/commit/f73bb0fe116678543a73d13f58eaa3b48133afc5))
## [3.2.1](https://github.com/morluto/rea/compare/rea-agents-3.2.0...rea-agents-3.2.1) (2026-10-03)
+45 -29
View File
@@ -10,7 +10,7 @@ REA development requires Node.js 24.18.x and npm 11.16.x. Real-Hopper verificati
```bash
npm ci
npm test
npm run check:fast
```
`npm ci` installs the exact dependencies and prepares the Husky hooks without
@@ -20,38 +20,46 @@ Turbo. Turbo caches deterministic builds and static checks across Git
worktrees. After a package, lockfile, or managed-skill version change, run
`npm run metadata:generate` before building.
Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Prefer capability- and session-scoped tool advertisement over schema truncation.
Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Keep tool discovery complete and report capability- and session-scoped availability through `binary_session`.
Before submitting a pull request, run:
## Development feedback and PR verification
For an ordinary edit, run the relevant regression and cached static checks:
```bash
npm run check:pr
npm run verify:package
npm pack --dry-run
npm run test:focused -- src/config.test.ts
npm run check:fast
```
`npm run check:fast` runs cached typecheck and lint for rapid local feedback.
`npm run check` adds formatting, dead-code, and package-metadata freshness
checks; use `npm run check:test` when the complete test suite is relevant.
Pre-commit formats then lints staged source files, and pre-push runs
`check:fast`. `check:pr` additionally renders API documentation and checks all
committed generated metadata. Real
provider and replay execution remains uncached, but their deterministic build
prerequisite uses Turbo. CI uploads the rendered TypeDoc site as an `api-docs`
artifact; the generated HTML is not committed.
`test:local` selects dirty source tests without building; explicit source test
paths run even on a clean tree. `check:changed` adds source tests affected since
the branch merge base with `origin/main`. Use `npm run test:changed -- --base
REVISION` to choose another base. Changed-test selection follows the import
graph and is feedback, not complete correctness evidence. Inspect relevant
boundary and provider behavior explicitly; see [docs/testing.md](docs/testing.md).
Local `npm test` runs every deterministic Vitest project without coverage,
retries, or verbose output. Focused boundary, acceptance, changed-test, and
watch commands are documented in [docs/testing.md](docs/testing.md), together
with the path-based behavioral-depth rules. Changed-test selection follows the
import graph and is feedback, not the release gate. CI splits the complete suite
across four native Vitest shards, then merges coverage, JUnit, and JSON timing
reports. Coverage thresholds remain in `vitest.config.ts`.
Before handing off a PR, run the relevant tests, `npm run check`, and
`npm run docs:check` when contracts or generated metadata change. Record which
checks ran. CI owns complete deterministic tests and aggregate coverage. Use
`npm run check:pr` for a deliberate full local gate on broad changes or when
investigating CI failures; it is not required after each edit, rebase, or push.
Packaging, setup, installation, or distribution changes also require
`npm run verify:package` and `npm pack --dry-run`. Provider behavior changes
require the matching real-provider `verify:*` lane.
CI installs dependencies once for all static checks, cancels superseded PR
runs, and skips package, Windows, and full test lanes for documentation-only
pull requests. TypeDoc renders only in pull-request CI; it does not run in local
commit or pre-push hooks, or in post-merge `main` CI.
`check:fast` runs cached typecheck and lint, reporting diagnostics on failure.
`check` adds formatting, dead-code, and package-metadata freshness checks.
Formatting uses Oxfmt and the committed `.oxfmtrc.json`; generated sources use
the same configuration. Pre-commit formats and lints staged files; pre-push runs
`check:fast`.
`docs:check` checks committed generated metadata. `docs:generate` regenerates
those files, and the docs CI lane checks them.
Real-provider execution remains uncached; deterministic builds use Turbo.
Local `npm test` runs every deterministic Vitest project without coverage or
retries. CI runs four coverage shards and merges JUnit and timing reports.
CI cancels superseded PR runs and skips package, Windows, and full test lanes
for documentation-only PRs. Coverage thresholds remain in `vitest.config.ts`.
Tests that need a temporary directory must use
`createTestTempDirectory` from `tests/fixtures/temporaryDirectory.ts`. The
@@ -92,13 +100,21 @@ Describe the behavior change and verification performed in the pull request. Nev
## Maintainer release checklist
Run the full checks, isolated package verifier, package dry run, and two-target real-Hopper verifier described above. Build a local tarball and exercise the executable through the package boundary:
Run `npm run check:pr`, the isolated package verifier, package dry run, and two-target real-Hopper verifier described above. Build a local tarball and exercise the executable through the package boundary:
```bash
npm pack
npm exec --yes --package ./morluto-rea-0.1.0.tgz -- rea --help
```
Use the exact filename printed by `npm pack` to run the packaged executable:
```bash
npm exec --yes --package ./rea-agents-VERSION.tgz -- rea --help
```
Replace `VERSION` with the packed version; do not use a tarball from an earlier
build.
Publish the public package:
```bash
@@ -110,6 +126,6 @@ After npm registry propagation, verify the published CLI and connect the client
```bash
npx -y rea-agents@latest --help
npx -y rea-agents@latest doctor
npx -y rea-agents@latest setup --yes
npx -y rea-agents@latest setup --yes --all-detected
npx -y rea-agents@latest mcp
```
+192 -337
View File
@@ -13,44 +13,54 @@
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#tool-catalog-for-investigation)
[![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
[![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM)
<a href="https://trendshift.io/repositories/82054?utm_source=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>
[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [Tool catalog](#tool-catalog-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
<br />
<code>npm install --global rea-agents && rea setup</code>
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="REA launching its analysis bridge inside Hopper while inspecting a native binary" width="1200" />
<br />
<table aria-label="REA community">
<tr>
<td align="center" width="360">
<a href="https://discord.gg/GkcryMnJDM">
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
<strong>Join the Reverse Engineering Community</strong>
</a><br />
<sub>Discord · Q&amp;A · Show and Tell</sub>
</td>
</tr>
</table>
<br />
</div>
---
See a feature in an app that you want in your own product? Give the app to your agent—even without its source code. With REA, the agent can investigate the feature, explain how it works, show its evidence, and build a version adapted to your stack and requirements.
See a feature in an app that you want in your own product? Ask your agent to investigate it with REA. It can inspect the app without its source code, explain how the feature works, show the evidence, and build a version for your project.
REA gives agents one consistent way to investigate software. Today that includes deep native analysis and function dossiers through Hopper or bring-your-own Ghidra on Linux, plus an experimental Windows x64 Ghidra P0 for approved native PE applications; execution-free managed PE/CLI triage; reproducible Evidence records; controlled process capture; passive website, Electron page, and Node/Electron V8 Inspector observation; JavaScript/source-map reconstruction; and provider-neutral graphs for connecting application layers without confusing static inference with runtime observation. The longer-term toolkit extends the same agent workflow to APIs, protocols, mobile artifacts, firmware, richer runtime behavior, and differences between versions.
REA connects your agent to tools for inspecting native binaries, JavaScript and Electron apps, .NET assemblies, and websites. You can also use the same tools from your terminal. Analysis runs locally, and results include the evidence and limitations behind each conclusion.
Reverse engineering normally makes the operator choose a tool, learn its API, move evidence between programs, and decide what to inspect next. REA gives that work to the agent through commands, skills, structured results, and repeatable investigation workflows.
Setup configures your agent and connects it to Hopper or Ghidra. If you need an analysis tool, setup can install Hopper for you.
## Just ask your agent
Run setup once. Agent integration installs an aligned MCP registration and the
bundled routing skill together:
```bash
npx rea-agents setup
```
Then ask:
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.
```
Notes is only an example. Name any app you want to understand, or ask the agent to start with an overview.
Replace Notes with the app you want to understand, or ask for an overview first.
## The investigation model
@@ -79,119 +89,69 @@ REA shows how it reached its conclusions. It does not claim to recover original
| ------------------------ | ----------------------------------------------------------------------------------------------------- |
| **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. |
| **Complexity handled** | REA installs and manages the reverse-engineering tools behind the scenes. |
| **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
### Run setup (recommended)
Set up REA with your agent:
```bash
npx --yes rea-agents@latest setup
npx rea-agents setup
```
The npm package-runner prompt, when shown, approves downloading REA for this
invocation; it does not approve any setup changes. The REA wizard separately
shows its complete plan and asks before applying it. Setup does not update
Homebrew, Node.js, or npm. The setup command opens with the work it
enables: investigate local apps from an agent, recover evidence through a
deep-analysis provider, and reuse REA's guided workflow. It summarizes the
detected agents, then asks which capabilities to set up: agent integration
(MCP plus the matching guided workflow) and—when needed—the Hopper provider.
Nothing is preselected. Choosing agent integration opens a second empty
checklist for the specific detected agents that should receive a registration.
Choose which supported agents should use REA, then review the exact paths and changes before approving. Existing REA registrations are selected by default; newly detected agents are available to select, but detection alone does not select them. Setup adds MCP access and REA's guided workflow for selected agents. Hopper is a separate optional choice with its own consent. Setup can also record an existing Ghidra installation.
`@latest` makes the requested release explicit and asks npm for the release
currently published under that tag. REA does not silently replace the package
version npm selected. Intentional rollbacks therefore remain available through
an exact package request.
Setup shows its changes before applying them and backs up existing configuration. See [Installation and setup](docs/installation.md) for requirements and setup options.
REA keeps the journey inline so its history remains in the terminal. Selecting
a capability does not select every detected target or authorize a change.
Before anything changes, REA validates existing configuration, prints exact
paths and external effects, and asks for final approval with **No** as the
default. The screen keeps the available keys visible while you choose; Ctrl-C
and declining leave the system unchanged.
### With an agent (recommended)
REA detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, and Devin. It configures the first six when detected; Devin is reported but left unchanged because it has no documented local MCP configuration boundary. Registrations are additive, backup-first, and read back after writing. You can safely rerun setup.
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.
Use `rea setup --dry-run` to inspect the plan, repeat `--client` to select exact
agents, and `--accessible` for sequential vertical prompts. Machine output
remains available through `--json`; prompt UI and progress go to stderr.
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).
After a successful setup, REA reports the capabilities now ready to use and a
concrete next step, such as restarting a configured agent before asking it to
investigate an application. It does not claim an integration or provider is
ready unless setup and its final diagnostic check verified it.
### From the terminal with npx
An optional curl wrapper installs the same CLI package and starts setup only when a terminal is available:
After setup, run:
```bash
npx -y rea-agents@latest doctor
npx -y rea-agents@latest analyze /Applications/Notes.app
```
### Install the rea command
Install the command-line interface:
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
```
Pass installer options after `bash -s --`, for example `--dry-run`, `--no-setup`, or `--version 1.0.0`. The curl wrapper never installs prerequisites or configures integrations itself. See [Installation and setup](docs/installation.md) for its exact mutation boundary.
The installer adds `rea` to your system and starts setup when run in a terminal. It requires Node.js and npm to be installed already.
### With an agent — recommended
```bash
npx --yes rea-agents@latest setup
```
Choose Agent Integration in the reviewed setup plan. REA installs the pinned MCP
registration and its matching routing skill as one transaction. After setup,
restart the configured agent so it loads the aligned integration.
Review the setup plan, approve it if appropriate, then describe the app or feature you want to understand. Hopper can run in its free demo mode; if it shows a first-run prompt, choose the demo or enter an existing license.
### From Terminal — no installation
```bash
npx --yes rea-agents@latest setup
npx -y rea-agents@latest doctor
npx -y rea-agents@latest analyze /Applications/Notes.app
```
Review the setup plan before confirming it. Restart a configured agent so it loads REA.
### From Terminal — install the `rea` command
Alternatively, install with npm, then run setup:
```bash
npm install --global rea-agents
rea setup
rea doctor
rea analyze /Applications/Notes.app
```
Update that global installation in place:
```bash
rea upgrade
```
REA checks npm for the latest release and verifies that the running package is
the global installation it will replace. Source, local, and `npx` copies report
the manual `npm install --global rea-agents@latest` command instead of updating
an unrelated global package.
Choose either the no-install commands or the global installation. You do not need both.
`npm install rea-agents` without `--global` installs `rea` only into the
current project's `node_modules/.bin`; it does not add `rea` to your shell
`PATH`. Use the `npx` commands above for one-off runs or `--global` when you
want a shell-visible `rea` command.
Update either installation with `rea update`.
### Requirements
- macOS 12 or newer
- Ubuntu 24.04+, Fedora 41+, or 64-bit Arch Linux
- Windows x64 for the experimental, Ghidra-only native PE P0 boundary
- Node.js 22.19+ or 24.11+ (including newer releases)
- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+
- npm; REA does not require or install a particular npm version
Deep binary operations use [Hopper](https://www.hopperapp.com/), a separate desktop application with its own license, or a caller-selected Ghidra provider. Ghidra supplies read-only inventory, function metadata, decompilation, assembly, resolved calls, typed references, xrefs, CFG, and function dossiers; GUI state and mutations remain unavailable through that provider. Setup reuses an existing Hopper installation or an operator-supplied Ghidra installation. It never downloads Ghidra or installs Java. If neither provider is ready, interactive setup proposes Hopper; unattended Hopper installation requires `rea setup --yes --install-hopper`.
Native binary analysis requires [Hopper](https://www.hopperapp.com/) or [Ghidra](#ghidra-analysis-provider). Hopper is separate software with its own license; its demo supports analysis with vendor-defined limits. REA can use Ghidra that you have already installed.
Windows Ghidra support is experimental for native x86-64 PE applications on local NTFS. The package bundles native Job Object, private-DACL, and path-admission controls. See [Windows Ghidra P0](docs/windows-ghidra-p0.md) for prerequisites and verified scope.
If something is not working, run:
@@ -199,13 +159,13 @@ If something is not working, run:
npx -y rea-agents@latest doctor
```
`rea doctor --json` is read-only and distinguishes unsupported hosts, missing dependencies, a missing local analysis engine, configuration drift, and healthy checks. Paid-license activation is optional: on Linux, REA runs the supported Hopper demo build on a private Xvfb display and selects Hopper's offered demo mode for each analysis session.
`doctor` checks your host, dependencies, analysis tools, and agent configuration without changing them. Use `--json` for structured diagnostics.
### Linux installation and troubleshooting
On macOS, approved setup downloads Hopper's official DMG, verifies it, and installs the app into `~/Applications` without Homebrew or administrator privileges. Hopper may show its demo or license prompt when first opened; no manual drag-and-drop is required.
On macOS, setup can install Hopper in `~/Applications` after approval. It verifies the official download and does not need Homebrew or administrator privileges.
On Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux, approved setup downloads the pinned official Hopper 6.4.2 package, restricts downloads to Hopper's public origin, verifies the published size and checksum, and invokes `apt-get`, `dnf`, or `pacman` to install Hopper and the Xvfb, Python, X11, and XTEST packages used by demo sessions. When REA is not already running as root, `pkexec` presents the system authorization prompt. REA never invokes `sudo`. Demo sessions run on an isolated 1280×1024 Xvfb display. REA verifies the exact supported Hopper binary, its owned process ancestry, the expected dialog geometry, and bridge state before selecting `Try the Demo`; any mismatch fails closed.
On supported Linux distributions, setup can install Hopper and its demo-session dependencies through your system package manager. You may see a system authorization prompt. Demo sessions use a private virtual display, leaving your desktop alone. See [Hopper installation](docs/installation.md#hopper) for download verification and platform details.
The normal Linux launcher is `/opt/hopper/bin/Hopper`. If Hopper was installed elsewhere:
@@ -220,13 +180,15 @@ If doctor reports a missing analysis engine even though the file exists, inspect
ldd /opt/hopper/bin/Hopper | grep 'not found'
```
Install the missing distribution packages and rerun `rea setup`. Linux demo automation requires `Xvfb`, Python 3, `libX11.so.6`, and `libXtst.so.6`; approved setup installs those direct runtime dependencies and does not interact with the user's desktop display. Hopper's free demo supports analysis with vendor-defined limits, and a paid license is optional. The curl installer places the `rea` command in `~/.local/bin` on Linux; add that directory to future shell `PATH` values if it is not already present.
Install the missing packages and rerun `rea setup`. The Linux demo needs Xvfb, Python 3, X11, and XTEST; approved setup installs these dependencies. If you use the curl installer, add `~/.local/bin` to your shell `PATH` when needed.
REA defaults `HOPPER_LAUNCHER_PATH` to `/Applications/Hopper Disassembler.app/Contents/MacOS/hopper` on macOS and `/opt/hopper/bin/Hopper` on Linux. Explicit configuration always takes precedence.
### Ghidra read-only analysis provider
### Ghidra analysis provider
The Ghidra adapter supports the exact official Ghidra 12.1.4 release with a 64-bit full JDK 21 on Linux x64. macOS is not an admitted Ghidra host. The adapter also provides an experimental Windows x64 P0 limited to approved native x86-64 PE applications. Download and extract those projects yourself, then configure absolute paths:
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.
Set the installation paths, then run setup:
```bash
export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.4_PUBLIC
@@ -236,21 +198,15 @@ rea setup
rea providers --json
```
Doctor distinguishes missing configuration, a bad installation root, the wrong Ghidra or Java version, a JRE without `javac`, a missing `support/analyzeHeadless`, and an unsupported platform or architecture. Approved setup only copies the verified non-secret paths into detected MCP registrations; it does not modify the Ghidra installation or install/download Ghidra or Java.
Setup checks the installations and saves their paths in your selected agents' configuration after approval. Ghidra and Java must already be installed; REA does not download or change them.
On Windows, set the same variables in PowerShell and run `rea doctor --json`; automated `rea setup` and Hopper installation remain unavailable. The P0 target boundary rejects DLLs, managed PE files, non-x86-64 images, mutable/hostile inputs, and non-PE formats. See the [Windows Ghidra P0 operations guide](docs/windows-ghidra-p0.md) for registration, exact limitations, CI evidence, and acceptance gates.
The adapter exposes inventory, function, memory and load-image inspection, plus atomic function annotation edits on Linux and macOS. `annotate_native_function` edits names and entry comments in the session database and returns a refreshed function dossier; executable bytes stay unchanged. Ghidra does not provide GUI controls through REA.
REA loads its packaged Java `HeadlessScript` with `-scriptPath`, copies and digest-verifies the target in an ephemeral runtime, enables `-readOnly` and `-deleteProject`, and authenticates every request. Auto-analysis completes before operations are served; startup has a deadline, while tool requests run until a result, caller cancellation, or provider shutdown. Linux uses a mode-0600 Unix socket. Windows P0 uses token-authenticated IPv4 loopback and a token-free endpoint record because Node path-based IPC does not connect to Java AF_UNIX sockets on Windows. The bridge verifies Ghidra's imported-byte SHA-256 before serving any operation.
REA analyzes a temporary copy of the target and removes the temporary project when the session closes. Results identify what Ghidra observed and what it could not resolve. Decompilation produces pseudocode rather than the original source.
The Ghidra adapter declares 19 direct and enhanced operations. Its ten inventory operations are `list_documents`, `list_procedures`, `list_strings`, `list_names`, `list_segments`, `address_name`, `procedure_address`, `resolve_containing_procedure`, `search_procedures`, and `search_strings`. It also admits `procedure_info`, `procedure_pseudo_code`, `procedure_assembly`, `read_function_instructions`, `procedure_callers`, `procedure_callees`, `procedure_references`, `xrefs`, and `analyze_function`. `read_function_instructions` is the offset-paginated fast path for raw instruction windows: it does not invoke the decompiler or whole-program name/string inventories, and is also exposed as `rea instructions`. These capabilities enable the shared Swift/Objective-C inventory workflows, `binary_overview`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, `trace_feature`, and complete function dossiers. Default-space addresses are lowercase `0x` hexadecimal. Other spaces, including `EXTERNAL`, use `<percent-encoded-space>:0x<hex>`. Symbol results identify primary, dynamic, external, type, and source facts; procedures distinguish external functions and thunks; strings identify charset, missing-terminator state, byte length, and value truncation; memory-block ends are exclusive and permissions come directly from Ghidra.
Ghidra also imports DOS MZ executables with an explicit 16-bit x86 real-mode profile. Function results include complete observed body ranges, distinguishing owned bytes from the enclosing span. See the [DOS analysis guide](docs/ghidra-dos.md) for addresses, packing, and verification boundaries.
The bridge serves operations only after auto-analysis completes. Each Program owns one persistent `DecompInterface`, and a serial FIFO keeps Ghidra API access on the owning program's thread; it has no fixed queue length or per-operation deadline. Reference results preserve Ghidra's call/jump/data/read/write/indirect/computed/external facts, while unresolved targetless flows remain explicitly unknown. Synthetic entry-point references without actionable memory sources are omitted. Pseudocode and assembly are provider-specific observations, not original source or Hopper-equivalent text. Caller cancellation and provider shutdown remain available; provider results are returned without a fixed response-size ceiling.
`npm run verify:ghidra` builds debug and stripped fixtures for the supported Linux x64 ELF host. Against real Ghidra 12.1.4 it validates every admitted operation, direct and indirect calls, imports/exports/thunks, typed references, strings/xrefs, multi-block CFG, cancellation, deadlines, concurrency, malformed inputs, and complete process/project cleanup. It needs a host C compiler and the Ghidra/JDK prerequisites above.
`npm run verify:ghidra:cross-format` adds AArch64 ELF, x86-64 PE, and x86-64 Mach-O fixtures. This separate lane requires `clang`, LLD, and `lld-link`; use `REA_CLANG`, `REA_LLD`, or `REA_LLD_LINK` to select alternate command paths. It preflights the required toolchain before compiling fixtures.
`npm run verify:ghidra:windows` uses a deterministic source-owned native x86-64 PE application and requires all 19 operations, target/snapshot/import digest linkage, authenticated loopback transport, and cleanup on a controlled Windows x64 Ghidra 12.1.4 runner. This proof does not establish Job Object ownership, private DACLs, or reparse-point-safe authority.
Windows Ghidra P0 uses bundled native controls for its read-only native x86-64 PE boundary on local NTFS; see the [Windows Ghidra P0 guide](docs/windows-ghidra-p0.md). See [Ghidra installation](docs/installation.md#ghidra), [provider evaluation](docs/provider-evaluation.md), and [testing](docs/testing.md) for configuration details, coverage, and real-provider verification.
To remove only REA-owned MCP registrations and the managed skill:
@@ -263,15 +219,15 @@ Uninstall preserves Hopper, Node.js, Evidence files, captures, unrelated skills,
### CLI or agent?
| If you want to… | Use |
| ---------------------------------------------------------------- | ------------------------------------------------------------------------- |
| Ask an agent to investigate an app and build a feature | Install the skill, then talk to your agent |
| Inspect or decompile one part of an app from the Terminal | `rea analyze` or `rea decompile` |
| Validate, canonicalize, or compare Evidence bundles | `rea evidence-import`, `rea evidence-export`, or `rea compare` |
| Map a local JavaScript/Electron application without executing it | `rea analyze PATH` or `rea analyze-javascript-application` |
| Reuse immutable analysis results without relaunching a provider | Pass `--snapshot /approved/path/analysis.json` to a deep-analysis command |
| Import source as historical reference | `rea import-reference-source` |
| Capture or compare controlled process behavior | `rea capture-process` or `rea compare-process-captures` |
| If you want to… | Use |
| ---------------------------------------------------------------- | ------------------------------------------------------------------- |
| Ask an agent to investigate an app and build a feature | Run setup, restart your agent, then describe the task |
| Inspect or decompile one part of an app from the Terminal | `rea analyze` or `rea decompile` |
| Validate, canonicalize, or compare Evidence bundles | `rea evidence-import`, `rea evidence-export`, or `rea compare` |
| Map a local JavaScript/Electron application without executing it | `rea analyze PATH` or `rea analyze-javascript-application` |
| Reuse immutable analysis results without relaunching a provider | Pass `--snapshot /path/to/analysis.json` to a deep-analysis command |
| Import source as historical reference | `rea import-reference-source` |
| Capture or compare controlled process behavior | `rea capture-process` or `rea compare-process-captures` |
```bash
rea evidence-import /absolute/path/to/evidence/bundle.json
@@ -279,8 +235,7 @@ rea evidence-export /absolute/path/to/evidence/bundle.json /absolute/path/to/evi
rea compare /absolute/path/to/evidence/left.json /absolute/path/to/evidence/right.json
```
JavaScript application analysis reads the selected directory or ASAR directly;
the command uses the supplied path directly:
Analyze a JavaScript application directory or ASAR without executing it:
```bash
rea analyze /absolute/path/to/releases/app.asar --json
@@ -292,25 +247,15 @@ static JavaScript application provider when neither `--provider` nor
`--snapshot` is supplied. Both routes return the analysis and its Evidence
context inline.
Historical source import takes the directory directly and never treats source
as current behavioral authority:
Import an older source tree as a reference. REA keeps it separate from observations of the current app:
```bash
rea import-reference-source /absolute/path/to/source
```
Imports read the path supplied to the command and validate every Evidence ID and manifest. Exports never replace an existing file unless `--overwrite` is explicit.
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.
Provider-neutral analysis snapshots persist successful, immutable REA calls and
their Evidence records. They are exact caches rather than Hopper databases:
REA reuses a v2 entry only when the binary digest, kind, format, architecture,
operation parameters, concrete provider build, and canonical analysis-profile
digest match. Hopper loader defaults and configured overrides are normalized by
the Hopper adapter and committed to that profile, so overrides occupy a distinct
safe cache partition instead of disabling snapshots. Cursor-dependent and
mutating calls are never cached. Snapshot files can contain proprietary analysis
results and local paths, so REA keeps them local and writes them with owner-only
permissions. The caller supplies the snapshot path directly:
Use a snapshot to save successful analysis results and reuse them on later runs. REA reuses a result only when the target bytes, operation, parameters, analysis tool, and settings match. It does not cache changes or cursor-dependent calls. Snapshot files stay local and use owner-only permissions.
```bash
rea analyze /absolute/path/to/app --snapshot /absolute/path/to/analysis/app.json
@@ -318,10 +263,10 @@ rea analyze /absolute/path/to/app --snapshot /absolute/path/to/analysis/app.json
rea analyze /absolute/path/to/app --snapshot /absolute/path/to/analysis/app.json
```
Exact CLI evidence replays happen before any provider process starts. In MCP sessions,
Exact CLI cached-evidence reads happen before any provider process starts. In MCP sessions,
pass `snapshot_path` to `open_binary` to import a snapshot atomically while
opening its matching target; MCP providers may still start before a cached call
is replayed. Pass `snapshot_path` and, when required, `overwrite: true` to
result is returned. Pass `snapshot_path` and, when required, `overwrite: true` to
`close_binary` to save atomically before Hopper resources are released. If the
save fails, REA deliberately leaves the session open.
@@ -343,7 +288,7 @@ REA gives the agent a clear path from that request to working code:
| 5 | Decompiles the relevant routines | `procedure_pseudo_code`, `procedure_assembly`, `batch_decompile` |
| 6 | Builds the feature in your project | code adapted to your stack, product, and requirements |
REA handles the app analysis in steps 1–5. The agent performs step 6 with its normal file-editing and test tools, using what it learned about the app.
REA handles the app analysis in steps 1 through 5. The agent performs step 6 with its normal file-editing and test tools, using what it learned about the app.
## What agents can do
@@ -356,155 +301,119 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
- Analyze Swift and Objective-C metadata without manually untangling every mangled symbol.
- Leave names, comments, and bookmarks in Hopper so human and agent analysis reinforce each other.
See [native investigation](docs/native-investigation.md) for keyed archives, instruction/call/type primitives, typed dispatch metadata, value traces and native desktop observation.
## Tool catalog for investigation
| Tool family | Count | Examples |
| ------------------------- | ----: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Native inspection | 36 | procedures, pseudocode, assembly, strings, names, segments, callers, callees, xrefs, annotations, bounded byte reads, file-offset translation |
| Investigation workflows | 14 | `binary_overview`, `analyze_function`, `inspect_native_api`, `inspect_native_dispatch_metadata`, `batch_decompile`, `trace_feature`, `trace_native_investigation`, exact string-to-code lookup, bounded call paths, call graphs, Swift and Objective-C discovery |
| Native macOS utilities | 5 | Mach-O metadata, code signatures, plists, architectures, Swift demangling; Hopper-free and provenance-bearing |
| Artifact graph | 3 | complete inline inspection of directories and supported packages, compiled Interface Builder UI graph decoding, plus explicitly selected extraction into an absent owned tree |
| Managed PE/CLI | 8 | PE/CLI identity, metadata members, CIL hashes, P/Invoke/native-boundary declarations and verification, application-graph projection, decompiler reconstruction import, token remapping, runtime-correlation plans, and version comparison |
| Browser observation | 9 | exact-origin passive CDP capture, bundle and source-map analysis, WebMCP discovery, session timelines, capture diff, visual evidence, and bounded Playwright scenarios |
| Electron analysis | 5 | passive root-confined observation, static application mapping, evidence-backed static/runtime reconciliation, and provider-owned click/wait scenarios |
| JavaScript runtime | 2 | approved attach-only Node/Electron Inspector target discovery plus bounded script and execution-context observation without evaluation or instrumentation |
| Application workflows | 10 | complete cross-layer traces, unique-only version matching, historical-source to bundle mapping, static export return-shape comparison, approved Linux-isolated extracted-module replay, managed-runtime characterization, reconstruction coverage closure, and deterministic obligation ledgers |
| Workspace and observation | 22 | target lifecycle, inline Evidence bundle retrieval, aggregate navigation/address context, direct finite replay-machine evaluation, process/artifact/function comparison, evidence-linked residual-unknown lifecycle |
| 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 |
| 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 |
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 implements controlled behavioral capture.
The public interface describes what the agent is trying to learn. Providers decide how to answer. macOS utilities handle common semantic inspection without launching Hopper; Hopper handles deeper native analysis; the process harness records direct behavioral captures.
## Current status
REA is already useful for native application, browser, and Electron investigation on supported macOS and Linux hosts, plus the bounded Windows Ghidra P0 described above:
REA supports native application, JavaScript, Electron, .NET, and browser investigation on macOS and Linux. Individual tools have platform and runtime prerequisites; use `rea capabilities` to check what is available on your host.
- Open Mach-O, ELF, PE, `.app`, ZIP, APK, IPA, ASAR, plist, JavaScript, source-map, and generic analysis-database targets; Hopper remains the only adapter that accepts legacy `.hop` databases.
- Discover deep-analysis candidates without starting them, choose deterministically, and retain one immutable provider/profile binding until an explicit switch or close; provider failures never trigger transparent fallback.
- Attach to a user-owned Chrome-family browser over a configured loopback CDP endpoint; capture exact-origin web structure, safe metadata, approved value-free payload shapes, bundle/source-map evidence, WebMCP declarations, user-action timelines, capture diffs, and explicitly approved screenshots without navigation or JavaScript evaluation.
- Inspect Electron `file://` renderer pages through a separate canonical-root permission boundary without invoking Electron APIs; script contents remain separately approved and byte bounded.
- Attach to one exact approved Node or Electron V8 Inspector target and retain bounded `scriptParsed` plus execution-context lifecycle metadata without evaluation, breakpoints, resume, source reads, or instrumentation. require/import edges, EventEmitter activity, Electron IPC, PID identity, and role identity remain unknown. See [passive Node and Electron runtime observation](docs/javascript-runtime-observation.md).
- Validate and canonically serialize a provider-neutral [JavaScript Application Graph](docs/javascript-application-graph.md) spanning packages, ASAR entries, Electron roles, JavaScript/source-map entities, browser/runtime instances, IPC, endpoints, storage, and native add-ons. This shipped domain contract performs no extraction or I/O by itself.
- Reconstruct static package, entrypoint, Webpack/Rspack module, import, worker, endpoint, storage, source-map, BrowserWindow, preload, contextBridge, IPC, utility-process, and native-add-on structure from a selected local directory or ASAR through `analyze_javascript_application` or `rea analyze-javascript-application`. Results and Evidence context are returned inline. The AST-only [application service](docs/javascript-artifact-reconstruction.md) never executes bootstrap code, pairs only unique exact literal IPC channels, and reports dynamic or ambiguous channels as unresolved.
- Reconcile that static graph with existing passive web or Electron Evidence through `reconcile_javascript_runtime` or `rea reconcile-javascript-runtime`. Exact captured bytes outrank caller-declared file/URL mappings; target, frame, script, worker, cache, and asset ambiguity stays explicit, source-map authority stays separate, and a module resident in an observed bundle is never reported as executed. See [JavaScript static/runtime reconciliation](docs/javascript-runtime-reconciliation.md).
- Trace a literal route, string, API, IPC channel, module, or native export through the complete reachable graph in authenticated application Evidence, then hand exact native artifact digests and requested exports to retained Ghidra or Hopper Evidence without automatic provider switching. Compare application versions using unique-only digest, source-map, structural, and semantic tiers; map a committed historical source inventory to bundle nodes with explicit digest and path scores; compare one exact JavaScript export's static return shapes through unique literal discriminants and JSON Pointer changes. Duplicate, dynamic, incomplete, ambiguous, and truncated facts stay unknown. See [cross-layer JavaScript application workflows](docs/javascript-application-workflows.md).
- Classify PE/CLI managed artifacts with `inspect_managed_artifact` / `rea inspect-managed-artifact`, inspect file-backed metadata members, signatures, raw CIL hashes, decoded-instruction-tuple fingerprints, separately reported exception regions, call edges, and field-access anchors with `inspect_managed_members` / `rea inspect-managed-members`, inventory declared ModuleRef/ImplMap/PInvoke and non-IL method boundary indicators with `inspect_managed_native_boundaries` / `rea inspect-managed-native-boundaries`, then compare two authenticated member observations with `compare_managed_members` / `rea compare-managed-members`. `verify_managed_native_boundaries` / `rea verify-managed-native-boundaries` checks managed P/Invoke declarations against authenticated native export or function Evidence while keeping verified, inferred, contradicted, and unresolved states distinct. The comparison treats build-local tokens as build-local and uses unique decoded-CIL/signature and structural method-shape tiers, never names alone; tuple fingerprints do not themselves resolve tokens or fully commit control flow. `project_managed_application_graph` / `rea project-managed-application-graph` projects authenticated managed artifact/member/native-boundary Evidence into the existing application graph for cross-layer feature tracing. `import_managed_reconstruction` / `rea import-managed-reconstruction` admits user-supplied decompiler C#/IL/pseudocode as analyst inference only after exact artifact SHA-256, MVID, signature, and decoded-IL commitments match. Separately, `plan_managed_runtime_correlation` / `rea plan-managed-runtime-correlation` can admit a default-disabled, permission-gated runtime-correlation plan locked to the same build evidence. These paths never load the assembly, resolve CLR dependencies, execute target code, run a decompiler, or translate managed tokens into native addresses; complete normalized-CIL semantics, native-body bridge mapping, and an actual runtime executor remain future managed-code contracts.
- Configure `REA_ILSPY_CMD_PATH=/absolute/path/to/ilspycmd` only when you want
doctor and `verify:managed` to inspect a bring-your-own ILSpy command as a
real reconstruction oracle. REA does not install ILSpy and does not treat
decompiler text as canonical metadata or CIL observation.
- Traverse content-addressed artifact graphs without extraction; on macOS, read-only DMG traversal additionally requires `native_mount_approved: true` and `REA_ARTIFACT_NATIVE_MOUNT_ENABLED=true`. Materialize only approved occurrences into absent output roots.
- Build function dossiers with pseudocode, assembly, CFG edges, comments, calls, references, strings, and names.
- Search and trace features across symbols, strings, metadata, references, and call paths.
- Record every successful result as deterministic Evidence with artifact and provider identity, confidence, authority, limitations, and locations.
- Export and import evidence bundles across sessions.
- Capture approved PTY scenarios as Process Capture Evidence, including committed run manifests, raw and rendered terminal frames, scripted interactions, descendant settlement, named filesystem checkpoints, deterministic command shims, and loopback HTTP/WebSocket exchanges.
- Validate finite replay machines without launching a target through `run_replay_machine` or `rea run-replay-machine`; ordered events return typed decisions, actions, captured aliases, transition journals, final state, and exact limit use without echoing request or captured values.
- Compare complete artifact inventories by stable path, content, metadata, and relations; incomplete evidence never implies equivalence.
- Compare explicit function dossiers across text, calls, references, strings, and address-normalized CFG topology with per-facet unknowns.
- Compare canonical Evidence bundles by exact membership, explicit observation pairs, and residual-unknown histories without turning omissions into behavioral absence.
- Aggregate runtime comparisons into observed behavior changes while keeping static artifact/function differences labeled as candidates.
- Build Evidence-cited direct call paths by exact address without treating missing dossiers as graph leaves.
- Correlate exact static/runtime findings through explicit hypotheses without claiming causality from cochange.
- Verify finite behavioral and structural reconstruction specifications with pass, fail, and unknown kept distinct.
- Track residual unknowns through immutable CAS revisions, evidence-qualified resolution, contradictions, probes, and validated dependency relationships.
- Evidence-producing workflows record returned residual uncertainty as residual unknowns linked to their result Evidence. Errors without supporting Evidence do not create registry records.
- Start six [guided MCP workflows](docs/mcp-prompts.md) with live, session-aware completion for documents, procedures, providers, evidence, captures, artifact IDs, and active unknowns.
- **Native binaries:** Open Mach-O, ELF, PE, and Mac `.app` targets through Hopper or Ghidra. Inspect functions, strings, assembly, decompilation, calls, and references. Hopper also accepts `.hop` databases and supports annotations.
- **Packages and resources:** Inspect directories, ZIP, APK, IPA, ASAR, plists, compiled Interface Builder files, and Apple asset catalogs. Artifact requests name the input and requested extraction or traversal directly; macOS DMG traversal also requires the host's native mounting support.
- **JavaScript and Electron:** Map modules, imports, source maps, routes, IPC channels, storage, and native add-ons without running the app. Compare builds and trace a feature across the recovered graph. Dynamic and ambiguous relationships remain unresolved. See [JavaScript application workflows](docs/javascript-application-workflows.md).
- **Websites:** Inspect a selected page in an existing Chrome-family browser. Capture page structure, network metadata, script evidence, and screenshots requested by the call. Passive observation does not navigate or execute page JavaScript. See [browser observation](docs/browser-observation.md).
- **Electron and Node runtime observation:** Inspect selected Electron pages or attach to a Node/Electron V8 Inspector target. Inspector observation records script locations and execution-context events; it does not infer imports, IPC activity, or which modules executed. See [runtime observation](docs/javascript-runtime-observation.md).
- **.NET assemblies:** Inspect metadata and CIL instructions, compare builds, and check declared native dependencies without loading or running the assembly. Imported decompiler output is labeled as analyst inference. See [managed-code analysis](docs/managed-code-analysis.md).
- **Controlled behavior capture:** Run process, browser, or Electron scenarios with the target, actions, and lifecycle declared in each request, then compare the resulting evidence. Missing observations cannot establish that two runs behaved the same way.
- **Evidence and comparison:** Save results with artifact identity, provider, locations, confidence, and limitations. Export or import bundles, compare artifacts and functions, and connect static findings to runtime observations without claiming causality from correlation.
- **Open questions:** Track unresolved findings, contradictions, and follow-up probes. Reconstruction checks report pass, fail, or unknown rather than treating missing evidence as a pass.
- **Guided workflows:** Start six [MCP investigation workflows](docs/mcp-prompts.md) with suggestions based on your current session.
Hopper is the first provider, not the boundary of the project. Some current workflows still require Hopper and macOS; every evidence record identifies the provider and limitations behind its result.
Windows Ghidra P0 supports the read-only native x86-64 PE boundary on local NTFS. Hopper-only features, such as GUI controls and annotations, are not available through Ghidra.
### Website observation with CDP
REA can inspect an already-running Chrome-family browser that you own. Browser observation is disabled by default and requires a literal loopback CDP endpoint plus exact approved page origins:
REA can inspect an already-running Chrome-family browser through a literal loopback CDP endpoint. Each request names the endpoint and target; an optional origin filter can narrow discovery:
```bash
export REA_BROWSER_OBSERVE_ENABLED=true
export REA_BROWSER_CDP_ENDPOINTS_JSON='["http://127.0.0.1:9222"]'
export REA_BROWSER_ALLOWED_ORIGINS_JSON='["http://127.0.0.1:3000"]'
rea list-browser-targets http://127.0.0.1:9222 --approved --json
rea inspect-web-page http://127.0.0.1:9222 TARGET_ID --approved --json
rea list-browser-targets http://127.0.0.1:9222 --json
rea inspect-web-page http://127.0.0.1:9222 TARGET_ID --json
```
All eight browser tools expose the same Evidence contracts over CLI and MCP. Inspection is passive: REA does not evaluate page JavaScript, navigate, click, close the page, or close the browser. Query values, credentials, cookies, authorization headers, storage values, and raw JSON or WebSocket values are never retained. Separately approved captures can retain bounded redacted console primitives, value-free JSON/WebSocket shapes, script sources, accessibility text, or screenshot pixels. Existing activity before attach is explicitly unavailable. See [Website observation with CDP](docs/browser-observation.md) for browser startup, schemas, limits, and the threat model.
The eight passive browser tools work through both CLI and MCP. They inspect the selected page without navigating, clicking, or evaluating its JavaScript. Credentials, cookies, authorization headers, and raw payload values are not retained. A request selects whether to include script sources, accessibility text, screenshots, or console and payload summaries. REA cannot observe activity that happened before it attached. See [browser observation](docs/browser-observation.md) for browser startup, capture options, and limits.
### Controlled browser scenarios
`capture_browser_scenario` is a separate, explicitly mutating browser boundary.
It runs only the fixed, versioned scenario vocabulary through Playwright and
returns step-indexed Evidence for screenshots, DOM, accessibility, URL/history,
storage, console/errors, network, WebSockets, frames, workers, popups, and
cancelled downloads. Missing or truncated sections can never support equality
claims.
`capture_browser_scenario` runs a caller-declared sequence of browser actions through
Playwright. Unlike passive observation, it can interact with the page. Each
step records evidence such as screenshots, page structure, navigation, and
network activity. Missing or truncated observations cannot establish that two
runs behaved the same way.
```bash
export REA_BROWSER_SCENARIO_ENABLED=true
export REA_BROWSER_SCENARIO_EXECUTABLE_ROOTS_JSON='["/usr/bin"]'
export REA_BROWSER_SCENARIO_CDP_ENDPOINTS_JSON='["http://127.0.0.1:9222"]'
export REA_BROWSER_SCENARIO_ALLOWED_ORIGINS_JSON='["http://127.0.0.1:3000"]'
export REA_BROWSER_SCENARIO_ALLOWED_ENV_JSON='["REA_TEST_PASSWORD"]'
rea capture-browser-scenario ./scenario.json --json
```
Launch mode owns a temporary browser profile and removes it after terminating
the launched browser. Connect mode accepts one exact loopback CDP target and
disconnects without closing the external browser. Automation has no default
grant: use the shared project/session policy, or set
`REA_BROWSER_SCENARIO_AUTO_GRANT=true` only for a trusted unattended
environment. Scenario JSON contains secret references and environment-variable
names, never secret values. See the
disconnects without closing the external browser. The request supplies the
selected executable or endpoint, actions, and any origin or environment
selections needed by the scenario. Scenario JSON contains secret references and
environment-variable names, never secret values. See the
[browser scenario contract](docs/browser-scenario-contract.md).
### Node and Electron V8 Inspector observation
Attach-only JavaScript runtime observation is separately disabled by default:
Node and Electron runtime observation attaches to an existing Inspector target named in the request:
```bash
export REA_V8_INSPECTOR_OBSERVE_ENABLED=true
rea list-javascript-runtime-targets http://127.0.0.1:9229 --json
rea observe-javascript-runtime http://127.0.0.1:9229 TARGET_ID \
--runtime-kind node --json
```
REA sends only `Runtime.enable` and `Debugger.enable`. It retains validated
script locations and execution-context lifecycle events;
require/import edges, EventEmitter activity, Electron IPC, PID identity, and
Electron role identity stay explicit unknowns. See
[passive Node and Electron runtime observation](docs/javascript-runtime-observation.md).
REA records script locations and execution-context events without evaluating
code or setting breakpoints. These observations do not establish import
relationships, event activity, IPC, process identity, or Electron roles. See
[Node and Electron runtime observation](docs/javascript-runtime-observation.md)
for the exact coverage.
Exact package, tool-family, provider, setup-client, schema, and CLI facts are generated from source in [`docs/product-catalog.json`](docs/product-catalog.json). PR CI verifies this catalog, narrative documentation, generated schemas, and a clean TypeDoc render.
Exact package, tool-family, provider, setup-client, schema, and CLI facts are generated from source in [`docs/product-catalog.json`](docs/product-catalog.json). PR CI verifies this catalog, narrative documentation, and generated schemas.
## Roadmap
REA is growing into a toolkit for understanding software across static artifacts and observed behavior. The [current status](#current-status) above is the shipped baseline; the items below are planned work.
The [current status](#current-status) section describes shipped capabilities. These are the next areas of work.
### Now
1. **Maintain truthful product metadata** — extend the shipped canonical catalog and drift checks whenever versions, tools, providers, schemas, setup clients, or CLI capabilities change.
2. **Cross-provider conformance growth** — add source-owned architectures and difficult indirect/thunk cases while preserving semantic comparison and provider-specific text boundaries.
1. **Keep documentation accurate:** update the generated catalog and documentation checks when tools, providers, setup options, or versions change.
2. **Test more native binaries:** expand architecture and indirect-call coverage across Hopper and Ghidra.
### Next
1. **Controlled replay conformance growth** — extend the shipped Linux extracted-module sandbox with more source-owned hostile fixtures and cross-kernel conformance; browser and Electron scenarios remain separately permissioned authorities.
2. **Broader application graph evidence** — extend authenticated cross-layer traces with additional static extractors and additional runtime authorities.
3. **Professional managed-code analysis** — extend shipped PE/CLI triage, CIL evidence, managed/native declaration inventory, source-owned conformance, and obfuscation-resistant comparisons toward verified native-provider composition under the accepted [managed-code boundary](docs/managed-code-analysis.md).
4. **Deterministic behavior harnesses** — extend process ownership, protocol fixtures, filesystem observation, reconnects, and cross-version behavioral comparison.
1. **Connect more application layers:** add static extractors and runtime observations to feature traces.
2. **Extend .NET analysis:** improve comparisons of obfuscated assemblies and connect managed findings to verified native analysis. See the [managed-code guide](docs/managed-code-analysis.md).
3. **Compare more runtime behavior:** expand process, protocol, filesystem, reconnect, and version-comparison coverage.
### Later
1. **Broader controlled application interaction** — extend the separately authorized browser and Electron scenario surfaces beyond the current bounded click/wait actions without widening passive observation or extracted-module replay authority.
2. **Native runtime observation** — approval-gated LLDB, Frida, system logs, process/filesystem observers, and native API tracing.
3. **Additional providers and targets** — evaluate IDA/Hex-Rays, Binary Ninja, Rizin, LIEF, Windows-native providers, mobile artifacts, firmware, document formats, and other software-defined systems.
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.
New providers must produce the same evidence and safety metadata as existing capabilities before they become part of the public workflow. Once REA has multiple optional toolchains, setup can become capability-selective; the consent rules for that future work are recorded in the [installation roadmap](docs/roadmap.md).
Setup already lets you choose agent integration and Hopper installation. Support for installing additional analysis tools is future work, described in the [installation roadmap](docs/roadmap.md).
See the [static-analysis provider evaluation](docs/provider-evaluation.md) for the shipped Ghidra function-analysis boundary, remaining admission gates, and provider comparison matrix; [ADR-0001](docs/adr/0001-provider-selection-and-analysis-profiles.md) for binding, selection, profile, snapshot, and compatibility decisions; the [controlled replay guide](docs/controlled-javascript-replay.md) plus [ADR-0002](docs/adr/0002-controlled-replay-authority-and-sandbox.md) for the shipped JavaScript replay boundary; and [ADR-0003](docs/adr/0003-managed-code-evidence-and-provider-boundary.md) for the managed-code evidence and provider design.
See the [native UI and dispatch investigation guide](docs/native-investigation.md) for compiled Interface Builder decoding, symbol-derived metadata limits, and the current p-code value-flow boundary.
See [provider evaluation](docs/provider-evaluation.md) for coverage and remaining requirements, and the [native investigation guide](docs/native-investigation.md) for UI, dispatch, and value-flow analysis.
## Using REA with other agents
Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, and Devin. It automatically configures the first six when present; detected Devin installations are reported but left unchanged. Any agent that supports local MCP servers can use REA with the configuration below.
Setup offers supported agent integrations for selection. Existing REA registrations are selected by default; newly detected agents remain unselected until chosen. Any agent that supports local MCP servers can use the configuration below.
### Manual MCP configuration
@@ -515,7 +424,7 @@ Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf,
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@3.2.1", "mcp"]
"args": ["-y", "rea-agents@4.0.1", "mcp"]
}
}
}
@@ -524,11 +433,11 @@ Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf,
<!-- x-release-please-end -->
Persistent registrations should use one exact package version. `rea setup`
maintains that pin, upgrades the bundled skill at the same time, and gives Codex
a 30-second startup allowance for a cold package-runner start. An interactive
`rea upgrade` opens the updated setup plan after installing the new executable;
structured or non-interactive upgrades tell you to run that sync explicitly.
Restart clients whose approved registration changed.
maintains that pin, updates the bundled skill at the same time, and gives Codex
a 30-second startup allowance for a cold package-runner start. `rea update`
installs an exact release and verifies the new executable. It returns an
unapplied maintenance plan for existing REA integrations, with a scoped setup
command to review and approve their changes. Restart affected agents afterward.
MCP clients that support prompts can also discover six ordered investigation
workflows through `prompts/list`. Their optional identifier arguments use the
@@ -544,7 +453,7 @@ flowchart LR
REA --> Session["Target-bound session router"]
Session --> Registry["Deep-provider registry<br/>deterministic selection"]
Registry --> Hopper["Hopper provider"]
Registry --> Ghidra["Ghidra provider<br/>read-only inventory + function analysis"]
Registry --> Ghidra["Ghidra provider<br/>inventory + function analysis + annotations"]
Hopper --> Runtime["Owned provider runtime<br/>deadline + bounded diagnostics + cleanup"]
Ghidra --> Runtime
Session --> Native["Native macOS provider"]
@@ -584,7 +493,7 @@ Or install the `rea` command globally:
```bash
npm install --global rea-agents
rea --help
rea upgrade
rea update
rea mcp
```
@@ -592,8 +501,7 @@ REA accepts a Mac `.app` folder directly. If an agent cannot find an app by name
### Choosing a deep-analysis provider
Every deep-analysis open resolves a provider before creating its client. The
same selector and precedence apply to the CLI, MCP, and startup configuration:
Choose which analysis tool to use from the CLI:
```bash
rea providers --json
@@ -610,47 +518,29 @@ For MCP, pass the optional selector on `open_binary`:
}
```
The request-level `provider_id` or `--provider` wins over
`REA_ANALYSIS_PROVIDER`; all accept a provider ID or `auto`. Automatic selection
binds the sole usable deep candidate, reports `ambiguous` when several are
usable, and can leave an artifact-only target unbound so its disjoint artifact
operations still work. An explicit unknown, unavailable, or unsupported
provider fails with candidate IDs, stable rejection codes, and actionable local
diagnostics. `binary_session`, `rea providers`, and `rea capabilities` expose
the authoritative `analysis_provider_candidates` and
`analysis_provider_binding` fields. Each open target also has an
`analysis_run.run_id` allocated before provider startup. When dynamic providers
start, `analysis_run.process_lineage` changes from `not_observed` to `snapshots`
and retains one provider-attributed, token-verified observation per started
provider. Each observation carries `observed_at` and remains `unavailable` or
`verified`; a verified empty descendant list is distinct from both. Snapshots
describe bounded observations, not current live state or historical absence.
For providers with serial work, `analysis_activity` distinguishes `idle`,
`busy`, and `timed_out_busy`, and reports the active operation, elapsed time,
caller state, timeout, and queued-request count. A caller timeout therefore
does not falsely imply that Hopper's Python thread is available. `close_binary`
clears the REA session but returns `cleanup_incomplete` when authenticated
document shutdown, owned process cleanup, or private runtime removal cannot be
verified.
Reopening same target without a selector keeps its binding; runtime failure
never selects another provider silently.
Ghidra can appear as an available, target-compatible candidate after doctor
validates its exact installation. Its capability list contains the 19 admitted
read-only inventory and function-analysis operations; selecting it still does
not make Hopper-only GUI or mutation operations available and never triggers a
silent fallback.
Use `--provider`, or `provider_id` in MCP, to choose Hopper or Ghidra for a target. This choice overrides `REA_ANALYSIS_PROVIDER`.
With `auto`, REA selects the only available tool that supports the target. If both are available, specify one before opening the target. The session keeps that choice until you explicitly switch or close it; a failure never silently switches tools. Artifact-only analysis can work without a native analysis tool.
Run `rea providers` and `rea capabilities` to check availability and supported operations. Ghidra supports inspection and atomic function annotation edits on Linux and macOS. Its database edits are discarded on close; GUI controls require Hopper. Windows Ghidra P0 supports read-only analysis of native x86-64 PE applications on local NTFS with bundled native controls.
The session also reports active work and cleanup status. If a caller times out, the analysis tool may still be busy; `analysis_activity` reports that state. A `cleanup_incomplete` result identifies resources whose shutdown or removal could not be verified. See [provider selection and analysis profiles](docs/adr/0001-provider-selection-and-analysis-profiles.md) for session, cache, and process-tracking details.
### CLI exit status
| Status | Meaning |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `0` | The requested operation completed. Truthful unknowns, warnings, and partial or truncated evidence remain successful results. |
| `1` | Arguments, policy, permission, provider analysis, integrity checking, cancellation, timeout, setup, diagnostics, update, uninstall, output encoding, or output writing prevented completion. Structured output identifies the failure category when REA could encode it. |
| `128 + N` | The process ended from signal `N`, where the shell or runtime preserves the conventional signal-derived status. |
| Status | Meaning |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `0` | The operation completed. Results may still include warnings, partial evidence, or unresolved questions. |
| `1` | The operation could not complete, for example because of invalid input, host permission denial, cancellation, or timeout. Structured output reports the reason when available. |
| `128 + N` | The process ended from signal `N`, where the shell or runtime preserves the conventional signal-derived status. |
`setup` returns `1` for `planned`, `needs_confirmation`, or `needs_human`
`setup --dry-run` returns status `planned` and exits `0`. A setup result with
status `cancelled` also exits `0`.
`setup` returns `1` for `needs_confirmation` or `needs_human`
because configuration is not ready; rerun it after approval or remediation.
`doctor` returns `1` when any check is unhealthy. Output format, full envelopes,
`doctor` returns `1` when required checks for its readiness scope are unhealthy.
Unavailable optional provider prerequisites remain visible as informational
diagnostics and do not block setup or unrelated providers. Output format, full envelopes,
filters, and token controls never change the operation status.
When REA feeds a shell pipeline, enable `pipefail` so a downstream formatter
@@ -658,33 +548,25 @@ cannot hide its failure:
```bash
set -o pipefail
rea inventory-artifact ./app.asar --json | jq . > inventory.json
rea inspect-artifact ./app.asar --json | jq . > inspection.json
```
## Current Hopper provider
REA starts Hopper when needed; Hopper does not need to be running first. Hopper's launcher internally activates the application, so opening a target may bring Hopper to the foreground. REA asks macOS to start Hopper hidden and in the background when possible, but cannot guarantee that it will remain behind the current application.
REA starts Hopper when an operation needs it. On macOS, Hopper may bring its window or a dialog to the foreground even though REA requests background startup. Demo or license prompts may need your attention.
REA derives explicit format and architecture arguments to prevent common FAT and ARM selection dialogs. Other Hopper or macOS dialogs may still require a person. REA reports startup failures and remediation through CLI or MCP results instead of attempting to answer UI prompts.
Hopper handles one analysis request at a time. Cancelling your wait does not stop work already running inside Hopper; the session reports whether it is still busy. Successful decompilation results are cached until a relevant rename or comment change.
Hopper bridge calls pass through a serial FIFO because Hopper's Python API runs on one dedicated thread. Calls wait for a response or caller cancellation. Cancelling an active call settles that caller but keeps its wire slot until Hopper replies, preserving request/response correlation; `binary_session.analysis_activity` exposes active work and MCP progress reports elapsed time. Successful decompilation text is cached per document and procedure, shared by pseudocode and dossier requests, and invalidated after rename or comment mutation.
Use `rea instructions` when you only need assembly instructions for a function. It avoids decompilation and a whole-program inventory.
Hopper's synchronous public Python calls return when the operation completes; callers may cancel their wait. Use `read_function_instructions` or `rea instructions` when raw instruction orientation answers the question, without requesting a decompilation or whole-program name/string inventory.
Closing a session shuts down REA's bridge and removes its temporary socket directory while preserving a Hopper application you may be using. If cleanup cannot be verified, `close_binary` reports `cleanup_incomplete` and the affected resources.
Closing a REA session shuts down its bridge and removes its private socket directory. It does not quit a Hopper application the user may be using. If shutdown or cleanup cannot be verified, `close_binary` returns `cleanup_incomplete` with the affected local resources.
## Process capture
## Advanced process-capture setup
Process capture is disabled by default. Enabling it requires
`REA_PROCESS_CAPTURE_ENABLED=true`, approved executable and working roots in
`REA_PROCESS_EXECUTABLE_ROOTS_JSON` and `REA_PROCESS_WORKING_ROOTS_JSON`, and an
environment allowlist in `REA_PROCESS_ALLOWED_ENV_JSON`. Because the current PTY
adapter uses host networking, it also requires
`REA_PROCESS_ALLOW_EXTERNAL_NETWORK=true`.
Set `REA_PROCESS_CAPTURE_AUTO_GRANT=false` to configure those process-capture
limits as a ceiling without implicitly granting them. This mode remains
fail-closed until a narrower grant is established.
Process capture runs the exact executable and scenario declared in the request,
with the requested working directory and environment. Filesystem observation
paths select what to snapshot. The process runs with your user permissions;
Process Capture records behavior and is not a security sandbox.
Capture a scenario or compare two saved Process Capture Evidence records:
@@ -695,9 +577,9 @@ rea compare-process-captures authority.json reconstruction.json
```
The comparison reports each observed dimension separately and identifies the
first terminal, interaction, exit, filesystem, protocol, process, or shim
divergence. See [Process Capture](docs/process-capture.md) for scenario
fields, command-shim replay, checkpoint triggers, limits, and safety behavior.
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,
@@ -714,7 +596,11 @@ verified or absent.
## Security model
REA does not provide a hosted analysis service. Hopper and Linux Ghidra bridge communication uses authenticated private local sockets. Windows Ghidra P0 uses authenticated IPv4 loopback but does not claim named-pipe DACL or hostile-local-user isolation. Dynamic capabilities are disabled by default and require both operator policy and explicit per-call approval. Shipped providers, passive observers, and Process Capture are not security sandboxes: providers and launched targets run with the current user's permissions. Extracted JavaScript replay is a distinct Linux-only capability that fails closed unless Bubblewrap namespaces, architecture-checked seccomp, private runtime mounts, and delegated cgroup limits are available; it never inherits browser, Electron, or Process Capture authority. See [ADR-0002](docs/adr/0002-controlled-replay-authority-and-sandbox.md). Report vulnerabilities through the private process in [SECURITY.md](SECURITY.md).
Analysis runs locally. REA communicates with Hopper and Ghidra through authenticated private local sockets. Your agent or model provider has its own data policy.
Runtime requests act on the declared target and lifecycle. Analysis tools and launched targets run with your user permissions, and native UI capture still depends on macOS Accessibility and Screen Recording access. Static JavaScript analysis does not execute extracted modules; use direct browser, Electron, or process capture when runtime behavior is needed.
Windows Ghidra P0 automatically uses native Job Objects, protected private-runtime DACLs, and handle-based admission. Its experimental scope is native x86-64 PE applications on local NTFS. Report vulnerabilities through the private process in [SECURITY.md](SECURITY.md).
## FAQ
@@ -728,7 +614,7 @@ No. REA starts Hopper when an operation needs it. An already-running Hopper appl
<details>
<summary><strong>Why did Hopper appear in front of my other windows?</strong></summary>
Hopper's launcher internally activates the application. REA requests background startup, but macOS and Hopper may still bring a window or dialog forward. See [Hopper application behavior](#hopper-application-behavior).
Hopper's launcher internally activates the application. REA requests background startup, but macOS and Hopper may still bring a window or dialog forward. See [Current Hopper provider](#current-hopper-provider).
</details>
@@ -742,7 +628,7 @@ No. Setup can install Hopper for you, but Hopper remains separate software with
<details>
<summary><strong>Does REA install or include Ghidra or Java?</strong></summary>
No. Ghidra support is bring-your-own. REA packages only its Java bridge source, validates the exact supported Ghidra 12.1.4 and 64-bit JDK 21 installation, and loads that bridge through Ghidra's external script path after setup approval records the paths.
No. REA connects to an existing Ghidra installation. Run setup after providing the Ghidra and Java paths shown in the [Ghidra section](#ghidra-analysis-provider).
</details>
@@ -763,48 +649,17 @@ No decompiler can guarantee the original source. REA gives an agent pseudocode,
<details>
<summary><strong>Which agents can use REA?</strong></summary>
Any agent that can run a local MCP server can use the manual configuration. Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, and Devin; it automatically configures the first six when present and reports Devin without modifying it.
Any agent that can run a local MCP server can use the manual configuration. Setup offers the supported integrations listed in [Installation and setup](docs/installation.md#supported-agents); existing REA registrations are selected by default, and other detected agents require selection.
</details>
## Development
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, architecture, and release
instructions, and [docs/testing.md](docs/testing.md) for the behavioral test
depths, focused developer commands, coverage floors, and CI evidence. PR CI
publishes generated API documentation as the `api-docs` workflow artifact.
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and contribution checks, and [docs/testing.md](docs/testing.md) for test scopes and real-tool verification. `npm run docs:check` checks committed generated documents; `npm run docs:generate` regenerates them.
`npm run verify:agent` runs brandless native, JavaScript-application, managed,
and browser prompts through a real local Codex CLI. Its JSON report measures
natural MCP use, first-tool routing, repeated calls, actual Codex token usage,
completion quality, and explicit treatment of authority and unknowns.
`npm run verify:agent` evaluates native, JavaScript, managed, and browser investigation tasks through a real local Codex CLI. Its report covers tool selection, repeated calls, token use, completion quality, and handling of permissions and unknowns.
`npm run evidence:generate` regenerates the managed conformance manifest and
Evidence completion ledger from live verifier output. `npm run
evidence:check` reruns the verifier and fails when artifacts, scenarios,
providers, schemas, claim counts, Evidence IDs, or the bundled skill have
drifted. Unsupported claims remain explicit and never count as passes.
Verifier JSON reports include an ephemeral `verifier_run` UUID allocated before
the verifier performs work and inherited by its child processes through
`REA_PROCESS_RUN_ID`. The final report includes the verifier and parent PIDs
plus `process_lineage` and its ISO `observed_at` timestamp: POSIX verifiers
report a token-verified, point-in-time launcher process group and live
descendants, while platforms without an owned
lineage primitive report `status: "unavailable"` and a reason. An empty verified
descendant list means no child was live during the final observation; it does
not claim the verifier launched no children earlier. Nested verifier entrypoints
in the same process reuse its UUID; each new verifier process replaces any
inherited parent token with a fresh run identity.
Generated completion commitments deliberately exclude this per-execution
identity, so check mode remains deterministic while live reports remain
attributable.
Owned Hopper shutdown logs retain the launcher PID, process-group ID, cleanup
status, and whether a verified group signal was required. They omit the run
token and unexpected exception text. The real-Hopper verifier also starts an
unrelated `Hopper`-named sentinel in a distinct process group and fails unless
that process survives every session close; the sentinel is removed only by the
verifier after the survival check.
`npm run evidence:generate` regenerates the managed conformance manifest and Evidence completion ledger from live verification results. `npm run evidence:check` reruns verification and checks for drift. Unsupported claims remain explicit and do not count as passes. See [testing](docs/testing.md) for verification commands and process-cleanup checks.
## Project links
+126 -85
View File
@@ -4,7 +4,7 @@
# REA: هندسة أي شيء عكسيًا
### واجهة CLI وخادم MCP واحدان يمكّنان وكلاء البرمجة من إجراء هندسة عكسية لأي شيء
### استقصِ سلوك التطبيقات والملفات التنفيذية الأصلية باستخدام وكيلك.
**اعثر على ميزة تعجبك. افهم آلية عملها. ابنها بالطريقة التي تريدها.**
@@ -13,109 +13,152 @@
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#منصة-أدوات-التحقيق)
[![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
[![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM)
[البدء السريع](#البدء-السريع) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [دليل الأدوات](#منصة-أدوات-التحقيق) · [كيف يعمل](#كيف-يعمل) · [الأسئلة الشائعة](#الأسئلة-الشائعة)
<a href="https://trendshift.io/repositories/82054?utm_source=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>
[البدء السريع](#البدء-السريع) · [حالة الدعم](#حالة-الدعم) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [منصة أدوات التحقيق](#منصة-أدوات-التحقيق) · [خطة العمل](#خطة-العمل) · [كيف يعمل؟](#كيف-يعمل)
<table aria-label="REA community">
<tr>
<td align="center" width="360">
<a href="https://discord.gg/GkcryMnJDM">
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
<strong>انضم إلى مجتمع الهندسة العكسية</strong>
</a><br />
<sub>Discord · أسئلة وأجوبة · شارك أعمالك</sub>
</td>
</tr>
</table>
<br />
<code>curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash</code>
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="REA يشغّل جسر التحليل داخل Hopper لفحص ملف تنفيذي أصلي" width="1200" />
</div>
هل وجدت في تطبيق ما ميزة تريدها في منتجك؟ أعط التطبيق إلى وكيل البرمجة حتى من دون شيفرته المصدرية. باستخدام REA، يستطيع الوكيل استقصاء الميزة وفهم آلية عملها ثم بناء نسخة ملائمة لتقنياتك وتصميمك ومتطلباتك.
---
يوفر REA هذه العملية عبر CLI وخادم MCP واحدين. يستطيع الوكيل قراءة شيفرة التطبيق وتتبع طريقة عمل الميزات ثم استخدام الأدلة مباشرة في عمله البرمجي المعتاد. ويتولى REA أدوات الهندسة العكسية المعقدة خلف واجهة واحدة.
هل وجدت في تطبيق ميزة تريدها في منتجك؟ اطلب من وكيلك استقصاءها باستخدام REA. يستطيع فحص التطبيق من دون شيفرته المصدرية، وشرح طريقة عمل الميزة وعرض الأدلة، ثم بناء نسخة تناسب مشروعك.
يوفر REA أدوات لتحليل الملفات التنفيذية الأصلية وتطبيقات JavaScript وElectron وتجميعات .NET والمواقع. يمكنك استخدامها من وكيلك أو من الطرفية. يعمل التحليل محليًا، وتتضمن النتائج الأدلة والقيود.
يضبط Setup إعدادات الوكيل ويربطه بتثبيت موجود من Hopper أو Ghidra. ويمكنه تثبيت Hopper بعد موافقتك.
## اطلب من وكيلك مباشرة
```bash
npx rea-agents setup
```
ثم اطلب:
بعد [الإعداد](#البدء-السريع)، أعد تشغيل الوكيل ثم اطلب:
```text
أعد REA وأجر هندسة عكسية لتطبيق Notes. اشرح طريقة عمل البحث وكيف توصلت إلى ذلك،
ثم ابنِ ميزة مشابهة لمشروعي.
استقصِ طريقة عمل البحث في تطبيق Notes، واعرض الأدلة، ثم ابنِ ميزة مشابهة لمشروعي.
```
تطبيق Notes مجرد مثال. يمكنك تسمية أي تطبيق تريد فهمه أو طلب البدء بنظرة عامة.
استبدل Notes بالتطبيق الذي تريد فهمه، أو اطلب نظرة عامة أولًا.
## من الملف التنفيذي إلى السلوك
| فك الترجمة | الفهم | إعادة الإنشاء |
| ----------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------- |
| فك الترجمة | الفهم | إعادة الإنشاء |
| ---------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------ |
| استخرج الشيفرة شبه المصدرية، والتعليمات، والسلاسل النصية، والرموز | تتبع تدفق التحكم والمراجع المتبادلة والاستدعاءات والأنواع | حوّل ما تعلمه الوكيل إلى ميزة تناسب تقنياتك وواجهتك ومتطلباتك |
يوضح REA كيف وصل إلى نتائجه. وهو لا يدّعي استعادة الشيفرة المصدرية الأصلية أو استنساخ التطبيق تلقائيًا.
## لماذا REA؟
| | |
| -------------------- | ----------------------------------------------------------------------------------------- |
| **مصمم للوكلاء** | اسأل عما يفعله تطبيق مترجم ودع الوكيل يجمع الأدلة بدلًا من التخمين. |
| **CLI وMCP** | استخدم قدرات الهندسة العكسية نفسها من الطرفية أو وكيل البرمجة. |
| **يتولى التعقيد** | يدير REA إعداد الأدوات وفتح التطبيق واستمرار الاستقصاء والتنظيف بعد الانتهاء. |
| | |
| -------------------- | ------------------------------------------------------------------------------------- |
| **مصمم للوكلاء** | اسأل عما يفعله تطبيق مترجم ودع الوكيل يجمع الأدلة بدلًا من التخمين. |
| **CLI وMCP** | استخدم قدرات الهندسة العكسية نفسها من الطرفية أو وكيل البرمجة. |
| **يتولى التعقيد** | يدير REA إعداد الأدوات وفتح التطبيق واستمرار الاستقصاء والتنظيف بعد الانتهاء. |
| **سير عمل كامل** | انتقل من النظرة الأولى إلى الشيفرة شبه المصدرية وعلاقات الاستدعاء والأنواع وأدلة التنفيذ. |
| **محلي حسب التصميم** | يجري التحليل على جهاز Mac ولا يرفع REA الملف التنفيذي إلى خدمة تحليل مستضافة. |
| **يحافظ على السياق** | استقصِ عدة ملفات تنفيذية من دون بدء التحليل من جديد عند كل سؤال. |
| **محلي حسب التصميم** | يجري التحليل على جهازك المدعوم ولا يرفع REA الملف التنفيذي إلى خدمة تحليل مستضافة. |
| **يحافظ على السياق** | استقصِ عدة ملفات تنفيذية من دون بدء التحليل من جديد عند كل سؤال. |
## البدء السريع
### باستخدام وكيل برمجة — موصى به
### تشغيل الإعداد (موصى به)
اربط REA بوكيلك:
```bash
npx --yes rea-agents@latest setup
npx rea-agents setup
```
اطلب من الوكيل إعداد REA. سيفحص جهاز Mac ويشرح ما يحتاج إلى تثبيته ويطلب الموافقة ويرشدك خلال رسائل النظام. أعد تشغيل الوكيل بعد الإعداد إذا طلب ذلك لتحميل مجموعة الأدوات الكاملة.
يعرض Setup أولاً قائمة متعددة الاختيار للوكلاء الذين تريد ربطهم. تكون تسجيلات REA الموجودة محددة افتراضياً، ولا يؤدي اكتشاف عميل جديد وحده إلى تحديده؛ ويمكنك اختيار عميل غير مهيأ. راجع المسارات والتغييرات المحددة قبل الموافقة. يثبت Setup سير عمل REA للوكلاء المحددين افتراضياً. Hopper خيار منفصل يتطلب موافقة مستقلة، ويمكن أيضاً حفظ مسارات تثبيت Ghidra الموجودة.
### قبل البدء
يعرض التغييرات قبل تطبيقها وينشئ نسخة احتياطية من الإعدادات الموجودة. راجع [دليل التثبيت والإعداد](docs/installation.md) للمتطلبات والخيارات الإضافية.
- macOS 12 أو أحدث
- Ubuntu 24.04+ أو Fedora 41+ أو Arch Linux ‏64 بت
- Node.js 22.19+ أو 24.11+ وnpm المرفق مع Node
### الاستخدام مع وكيل
يعرض `rea setup` خطة التغييرات كاملة ويطلب التأكيد قبل تطبيقها. لا يثبت أو يحدّث Homebrew أو Node.js أو npm. إذا كان [Hopper](https://www.hopperapp.com/) مفقودًا، يقترح Setup الحزمة الرسمية. Hopper برنامج منفصل يحتاج إلى ترخيص خاص به.
بعد الإعداد، أعد تشغيل الوكيل واشرح التطبيق أو الميزة التي تريد استقصاءها. يدعم REA Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin وOpenCode وAntigravity وGitHub Copilot CLI وCommand Code وVS Code. تكون تسجيلات REA الموجودة محددة افتراضياً، ويجب اختيار العملاء المكتشفين الآخرين يدوياً. يمكن للوكلاء الآخرين استخدام إعداد MCP أدناه.
إذا كان Ghidra 12.1.4 PUBLIC وJDK 21 الكامل بنواة 64 بت مثبتين مسبقًا على Linux ‏64 بت، فيمكن لـ Setup أيضًا تسجيل `GHIDRA_INSTALL_DIR` و`JAVA_HOME` الاختياري بعد الموافقة. لا ينزّل REA أو يثبت أو يعدّل Ghidra أو Java. يوفّر مزوّد Ghidra ضمن جلسة headless معزولة وللقراءة فقط الجرد، وفك الترجمة، والتجميع، وعلاقات الاستدعاء، والمراجع ذات الأنواع، وxref، وCFG، وملفات تحليل الدوال؛ وتبقى حالة الواجهة الرسومية وعمليات التعديل غير متاحة.
يمكن تشغيل Hopper في الوضع التجريبي. إذا ظهرت رسالة عند التشغيل الأول، فاختر التجربة أو أدخل ترخيصًا موجودًا.
#### التثبيت على Linux واستكشاف الأخطاء
### الاستخدام من الطرفية
على Ubuntu 24.04+ وFedora 41+ وArch Linux ‏64 بت، ينزّل REA حزمة DEB أو RPM أو Arch الرسمية، ويتحقق من الحجم وقيمة التحقق المنشورين، ثم يستخدم `apt-get` أو `dnf` أو `pacman` لحل الاعتماديات. عند التشغيل دون root يعرض `pkexec` طلب تفويض النظام. لا يستدعي REA الأمر `sudo`.
مسار المشغّل الافتراضي هو `/opt/hopper/bin/Hopper`. استخدم `HOPPER_LAUNCHER_PATH` للمسارات الأخرى. إذا أبلغ Doctor عن غياب محرك التحليل، شغّل `ldd /opt/hopper/bin/Hopper | grep 'not found'`، وثبّت المكتبات الناقصة ثم أعد `rea setup`. يشغّل REA إصدار Hopper التجريبي المدعوم على شاشة Xvfb خاصة ويختار وضع التجربة الذي يقدمه Hopper لكل جلسة تحليل؛ لا يلزم `DISPLAY` لسطح المكتب ولا ترخيص مدفوع، مع بقاء حدود الإصدار التجريبي التي يحددها المورّد. أضف `~/.local/bin` إلى `PATH` أيضًا.
### 1. إعداد REA
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx --yes rea-agents@latest setup
```
إذا طلب macOS أو برنامج التثبيت تأكيدًا، فأكمل الخطوة ثم شغّل الأمر نفسه مرة أخرى.
### 2. إعادة تشغيل وكيل البرمجة
يكتشف Setup كلاً من Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin. ويهيئ تلقائيًا أول ستة عملاء عند اكتشافهم؛ أما Devin فيُبلّغ عن اكتشافه فقط ولا يُعدّله لعدم وجود حد موثق لإعداد MCP المحلي. أعد تشغيل العملاء الذين جرى إعدادهم كي يحمّلوا REA.
### 3. اطلب من الوكيل
يمكنك ذكر التطبيق باسمه. سيعثر وكيل البرمجة عليه ويمرر ملف البرنامج الذي يحتاجه REA.
```text
استخدم REA لإجراء هندسة عكسية لتطبيق Notes. اشرح طريقة عمل البحث واعرض الأدلة،
ثم ابنِ ميزة مشابهة لمشروعي باستخدام SQLite.
```
إذا واجهت مشكلة، فشغّل:
بعد الإعداد، شغّل:
```bash
npx -y rea-agents@latest doctor
rea uninstall
rea uninstall --purge-data
npx -y rea-agents@latest analyze /Applications/Notes.app
```
### تثبيت أمر rea
ثبت واجهة سطر الأوامر:
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
```
يجب تثبيت Node.js وnpm مسبقًا. عند تشغيله من طرفية، يثبت البرنامج `rea` ويبدأ الإعداد.
يمكنك بدلًا من ذلك التثبيت باستخدام npm ثم تشغيل الإعداد:
```bash
npm install --global rea-agents
rea setup
```
### المتطلبات
- macOS 12 أو أحدث
- Ubuntu 24.04+ أو Fedora 41+ أو Arch Linux بنواة 64 بت
- Node.js 22.x (>=22.19) أو 24.x (>=24.11) أو 26+ وnpm
يتطلب تحليل الملفات التنفيذية الأصلية Hopper أو Ghidra. Hopper برنامج منفصل، وللوضع التجريبي قيود يحددها المورّد، لكن الترخيص المدفوع ليس إلزاميًا.
يدعم Ghidra نظامي Linux x64 وmacOS x64/arm64. ثبت Ghidra 12.1.4 وJDK 21 الكامل بنواة 64 بت بصورة منفصلة، ثم اضبط REA لاستخدامهما. يحتاج macOS أيضًا إلى أداة فك الترجمة الأصلية المطابقة لمعمارية الجهاز.
يستطيع Setup التحقق من التثبيت وحفظ المسارات؛ ولا يثبت أو يحدّث Ghidra أو Java أو Node.js أو npm أو Homebrew.
تحليل Ghidra على Windows غير متاح. لم تُنفذ بعد ضوابط ملكية العمليات وأذونات المجلدات الخاصة والتحقق الآمن من المسارات. حتى التثبيت الصحيح لـ Ghidra وJava لا يفعّل التحليل. راجع [Windows Ghidra P0](docs/windows-ghidra-p0.md).
### تشخيص المشكلات
يشخّص `npx -y rea-agents@latest doctor` الجهاز والاعتماديات وأدوات التحليل وإعدادات الوكيل من دون تغييرها. أضف `--json` للحصول على تشخيص منظم.
مسار Hopper الافتراضي على Linux هو `/opt/hopper/bin/Hopper`. استخدم `HOPPER_LAUNCHER_PATH` للمسارات الأخرى. إذا كان الملف موجودًا لكن محرك التحليل لا يظهر، فشغّل `ldd /opt/hopper/bin/Hopper | grep 'not found'` للتحقق من المكتبات الناقصة. راجع [دليل Hopper](docs/installation.md#hopper).
### التحديث والإزالة
- يحدّث `rea update` تثبيت REA الحالي.
- يزيل `rea uninstall` تسجيلات الوكلاء وملفات سير العمل التي يديرها REA، ويترك Hopper مثبتًا.
- يزيل `rea uninstall --purge-data` أيضًا ذاكرة REA المؤقتة وحالته. استخدمه فقط إذا أردت حذف هذه البيانات.
## حالة الدعم
يوفر REA عبر CLI وMCP تحليل الملفات التنفيذية الأصلية وتطبيقات JavaScript وElectron وتجميعات .NET والمواقع. تعتمد العمليات المتاحة على الجهاز والهدف وأداة التحليل المختارة.
- يدعم Hopper التحليل الأصلي والتعليقات؛ ويختلف سلوك واجهته الرسومية حسب المنصة.
- يقدم Ghidra على Linux x64 وmacOS x64/arm64 عددًا قدره 22 عملية للقراءة فقط: الجرد والبحث وفك الترجمة والتعليمات والاستدعاءات والمراجع والأنواع. لا يقدم عمليات واجهة رسومية أو تعديل.
- لسير عمل المتصفح وElectron وتشغيل العمليات متطلبات مستقلة للإعداد والموافقة وإدارة دورة الحياة. راجع [English README](README.md#current-status) للتفاصيل الكاملة.
- تحليل Ghidra على Windows غير مفعّل. تابع التقدم في [#527](https://github.com/morluto/rea/issues/527).
## استقصاء كامل بطلب واحد
بعد الإعداد، يمكنك أن تطلب من وكيلك:
@@ -159,22 +202,26 @@ rea uninstall --purge-data
## منصة أدوات التحقيق
| عائلة الأدوات | العدد | أمثلة |
| --------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| فحص الملفات التنفيذية | 36 | الدوال، والشيفرة شبه المصدرية، والتعليمات، والسلاسل، والأسماء، والمراجع، والتعليقات |
| التحليل المركب | 14 | النظرة العامة، و`inspect_native_api`، وفك الترجمة الدفعي، ومخططات الاستدعاء، والمراجع، واكتشاف الأنواع |
| أدوات macOS الأصلية | 5 | بيانات Mach-O والتوقيعات وملفات plist والمعماريات وفك رموز Swift دون تشغيل Hopper |
| رسم القطع الأثرية | 3 | فحص كامل مباشر للدلائل والحزم المدعومة، واستخراج معاملاتي محدد صراحة |
| Managed PE/CLI | 8 | هوية PE/CLI، أعضاء البيانات الوصفية، بصمات CIL، تصريحات P/Invoke/الحدود الأصلية والتحقق منها، إسقاط رسم التطبيق، استيراد إعادة البناء من فك الترجمة، إعادة ربط الرموز، خطط ترابط وقت التشغيل ومقارنة الإصدارات |
| مراقبة المتصفح | 9 | التقاط CDP محدود الأصل وتحليل الحزم وخرائط المصدر واكتشاف WebMCP والفروق والأدلة المرئية |
| تحليل Electron | 5 | مراقبة سلبية ضمن جذور canonical، ورسم ثابت ومحدود للتطبيق، ومطابقة ثابتة/وقت تشغيل قائمة على Evidence |
| وقت تشغيل JavaScript | 2 | اكتشاف أهداف Node/Electron Inspector المعتمدة للاتصال فقط، ومراقبة محدودة للبرامج النصية وسياقات التنفيذ دون تقييم أو تجهيز |
| سير عمل التطبيقات | 10 | تتبع محدود ومقارنة إصدارات فريدة ومقارنة ثابتة لأشكال قيم إرجاع الصادرات وإعادة تشغيل وحدات معزولة ومعتمدة على Linux وتوصيف وقت التشغيل المُدار وإغلاق تغطية إعادة البناء ودفاتر التزامات حتمية |
| جلسة الملف التنفيذي | 22 | فتح الأهداف وحزم الأدلة وتشغيل آلة إعادة محدودة ومقارنة العمليات والقطع والدوال وسجل المجهولات المتبقية |
| عائلة الأدوات | العدد | الاستخدام |
| --------------------- | ----: | -------------------------------------------------------------------------------------- |
| فحص الملفات التنفيذية | 41 | الدوال والشيفرة شبه المصدرية والتعليمات والسلاسل والرموز والمراجع والتعليقات |
| التحليل المركب | 14 | النظرة العامة وتحليل الدوال وفك الترجمة الدفعي ومخطط الاستدعاء وفحص Swift وObjC |
| أدوات macOS الأصلية | 7 | بيانات Mach-O والتوقيعات وplist والمعماريات واستعادة أسماء Swift |
| الملفات والحزم | 5 | فحص الدلائل والحزم وInterface Builder وموارد Apple والاستخراج |
| .NET PE/CLI | 7 | هوية التجميع والبيانات الوصفية وCIL والاستدعاءات الأصلية والمقارنة واستيراد إعادة البناء |
| مراقبة المتصفح | 9 | الصفحات والبرامج النصية وخرائط المصدر وWebMCP والصور ومقارنة الالتقاط |
| تحليل Electron | 5 | مراقبة الصفحات وبنية التطبيق وربط النتائج الثابتة بنتائج التشغيل |
| وقت تشغيل JavaScript | 2 | الاتصال بهدف Node/Electron Inspector موجود لمراقبة البرامج وسياقات التنفيذ |
| سير عمل التطبيقات | 7 | تتبع الميزات ومقارنة الإصدارات وبنية قيم الإرجاع والتحقق من إعادة الإنشاء |
| جلسة الملف التنفيذي | 21 | تبديل الأهداف وحفظ الأدلة ومقارنة العمليات والدوال وتسجيل الأسئلة غير المحسومة |
## خطة العمل
يشمل العمل التالي توسيع التحقق من الأهداف الأصلية، ومقارنة إصدارات JavaScript و.NET، ومراقبة وقت التشغيل والتحقق من إعادة الإنشاء. يتيح Setup بالفعل اختيار تكامل الوكيل وتثبيت Hopper. تثبيت أدوات تحليل أخرى ما زال عملًا لاحقًا. راجع [خطة التثبيت](docs/roadmap.md) و[تقييم أدوات التحليل](docs/provider-evaluation.md).
## استخدام REA مع وكلاء برمجة آخرين
يكتشف Setup كلاً من Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin، ويهيئ تلقائيًا أول ستة عملاء. أما Devin فيُبلّغ عن اكتشافه فقط ولا يُعدّله. يمكن لأي وكيل برمجة يدعم خوادم MCP المحلية استخدام REA بالإعداد التالي:
يدعم Setup Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin وOpenCode وAntigravity وGitHub Copilot CLI وCommand Code وVS Code. تكون تسجيلات REA الموجودة محددة افتراضياً، ويجب اختيار العملاء المكتشفين الآخرين يدوياً. يمكن لأي وكيل يدعم خوادم MCP المحلية الاتصال باستخدام الإعداد التالي:
<!-- x-release-please-start-version -->
@@ -183,7 +230,7 @@ rea uninstall --purge-data
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@3.2.1", "mcp"]
"args": ["-y", "rea-agents@4.0.1", "mcp"]
}
}
}
@@ -204,7 +251,7 @@ flowchart LR
Ghidra --> App
```
يستخدم CLI وخادم MCP محرك التحليل نفسه. تغلق أوامر الطرفية التطبيق عند انتهائها، بينما تبقيه جلسة الوكيل مفتوحًا أثناء الاستقصاء.
يستخدم CLI وخادم MCP سير عمل التحليل نفسه. تحرر أوامر الطرفية جلسة الجسر الخاصة بها عند انتهائها، بينما يمكن لجلسة الوكيل الحفاظ على الاتصال أثناء الاستقصاء. إغلاق جلسة REA لا يغلق تطبيق Hopper الذي يستخدمه المستخدم.
## أوامر CLI
@@ -221,15 +268,9 @@ npx -y rea-agents@latest compare /absolute/path/to/left-evidence.json /absolute/
## سلوك Hopper
يستطيع REA تشغيل Hopper تلقائيًا؛ لا حاجة إلى افتراض أنه يعمل مسبقًا. لكنه تطبيق macOS رسومي، لذلك قد يطلب التركيز أو يعرض حوارًا عندما يفرض Hopper قرارًا تفاعليًا. لا يمكن لـ REA ضمان تشغيل غير مرئي تمامًا لأن هذه الواجهة يملكها Hopper وmacOS.
يشغّل REA برنامج Hopper عند الحاجة. على macOS قد تظهر نافذته في المقدمة أو يحتاج أحد الحوارات إلى استجابة منك. لا يمكن ضمان بقاء النافذة في الخلفية.
لتجنب مربع اختيار معمارية ملف شامل، حدد معطيات المحمل مسبقًا:
```bash
export HOPPER_LOADER_ARGS_JSON='["-l", "Mach-O", "--aarch64"]'
```
يدير REA دورة حياة Hopper ويحاول إغلاق العمليات التي بدأها عند انتهاء الأمر أو جلسة MCP.
يحدد REA تنسيق الهدف ومعماريته لتجنب حوارات الاختيار المعتادة. عند إغلاق الجلسة، يوقف الجسر وينظف موارده الخاصة، لكنه لا يغلق تطبيق Hopper الذي تستخدمه.
## الأمان والخصوصية
@@ -258,7 +299,7 @@ export HOPPER_LOADER_ARGS_JSON='["-l", "Mach-O", "--aarch64"]'
<details>
<summary><strong>هل يعمل REA على Linux أو Windows؟</strong></summary>
نعم، يعمل REA على macOS 12+ وUbuntu 24.04+ وFedora 41+ وArch Linux ‏64 بت؛ Windows غير مدعوم حاليًا. يستخدم التحليل العميق Hopper أو مزوّد Ghidra للقراءة فقط الذي يختاره المستدعي؛ ويتطلب Ghidra على Linux الإصدار 12.1.4 وJDK 21 الكامل.
يدعم REA نظام macOS 12+ وUbuntu 24.04+ وFedora 41+ وArch Linux بنواة 64 بت. يعمل تحليل Ghidra للقراءة فقط على Linux x64 وmacOS x64/arm64 مع Ghidra 12.1.4 وJDK 21 الكامل. تحليل Ghidra على Windows غير متاح بسبب الضوابط الأصلية غير المنفذة؛ راجع [Windows Ghidra P0](docs/windows-ghidra-p0.md).
</details>
+121 -75
View File
@@ -4,7 +4,7 @@
# REA:あらゆるものをリバースエンジニアリング
### コーディングエージェントがあらゆるものをリバースエンジニアリングするための、ひとつの CLI / MCP サーバー
### エージェントでアプリの動作からネイティブバイナリまで調べる。
**気になる機能を見つけ、仕組みを理解し、自分の形で実装する。**
@@ -13,35 +13,51 @@
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#調査ツールカタログ)
[![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
[![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM)
[クイックスタート](#クイックスタート) · [バイナリから動作へ](#バイナリから動作へ) · [ツールカタログ](#調査ツールカタログ) · [仕組み](#仕組み) · [FAQ](#faq)
<a href="https://trendshift.io/repositories/82054?utm_source=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>
[クイックスタート](#クイックスタート) · [現在の対応状況](#現在の対応状況) · [バイナリから動作へ](#バイナリから動作へ) · [調査ツールカタログ](#調査ツールカタログ) · [ロードマップ](#ロードマップ) · [仕組み](#仕組み)
<table aria-label="REA community">
<tr>
<td align="center" width="360">
<a href="https://discord.gg/GkcryMnJDM">
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
<strong>リバースエンジニアリングのコミュニティに参加</strong>
</a><br />
<sub>Discord · 質問と回答 · 成果の共有</sub>
</td>
</tr>
</table>
<br />
<code>curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash</code>
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="Hopper 内で解析ブリッジを起動し、ネイティブバイナリを調べる REA" width="1200" />
</div>
---
アプリの気になる機能を自分のプロダクトにも取り入れたいと思ったことはありませんか。ソースコードがなくても、そのアプリをコーディングエージェントに渡せます。REA を使えば、エージェントが機能を調査して仕組みを理解し、あなたの技術スタック、デザイン、要件に合わせた形で実装できます。
アプリの機能を自分のプロダクトにも取り入れたいときは、REA を使ってエージェントに調査を依頼できます。ソースコードがなくても、アプリを調べ、仕組みと根拠を示し、あなたのプロジェクト向けに同様の機能を実装できます。
REA はこの流れをひとつの CLI / MCP サーバーで実現します。エージェントはコンパイル済みのアプリを調べ、機能の流れを追い、分かったことを通常のコーディング作業に活用できます。複雑なリバースエンジニアリングツールは REA がひとつのインターフェースの裏側で扱います。
REA はネイティブバイナリ、JavaScript/Electron アプリ、.NET アセンブリ、Web サイトの解析ツールを提供します。エージェントからもターミナルからも使えます。解析はローカルで実行され、結果には根拠と制限が含まれます。
Setup はエージェントを設定し、既存の Hopper または Ghidra に接続します。承認後に Hopper をインストールすることもできます。
## エージェントに頼むだけ
```bash
npx rea-agents setup
```
次のように依頼します:
[セットアップ](#クイックスタート)後にエージェントを再起動し、次のように依頼します:
```text
メモアプリをリバースエンジニアリングしてください。
検索の仕組みと判断の理由を示し、私のプロジェクト向けに同様の機能を実装してください。
メモアプリの検索機能を調べて根拠を示し、私のプロジェクト向けに同様の機能を実装してください。
```
メモアプリは例にすぎません。調べたいアプリを指定するか、まず概要から始めるよう依頼できます。
メモアプリを調べたいアプリに置き換えるか、まず概要を依頼してください。
## バイナリから動作へ
@@ -53,70 +69,96 @@ REA は調査をバイナリ上の根拠に結び付けます。元のソース
## REA を選ぶ理由
| | |
| ---------------------- | ------------------------------------------------------------------------------------- |
| **エージェント向け** | コンパイル済みアプリについて質問し、推測ではなく根拠を集めさせることができます。 |
| **CLI と MCP** | ターミナルとコーディングエージェントから同じリバースエンジニアリング機能を使えます。 |
| **複雑さを処理** | ツール設定、アプリの読み込み、調査の維持、終了後のクリーンアップを REA が担います。 |
| **一連の調査に対応** | 最初の概要から疑似コード、呼び出し関係、型、実装の手掛かりまで掘り下げられます。 |
| **ローカルで解析** | 解析は Mac 上で実行され、REA がバイナリをホスト型解析サービスへ送ることはありません。 |
| **コンテキストを維持** | 質問ごとに解析を最初からやり直さず、複数のバイナリを続けて調査できます。 |
| | |
| ---------------------- | ---------------------------------------------------------------------------------------------------- |
| **エージェント向け** | コンパイル済みアプリについて質問し、推測ではなく根拠を集めさせることができます。 |
| **CLI と MCP** | ターミナルとコーディングエージェントから同じリバースエンジニアリング機能を使えます。 |
| **複雑さを処理** | ツール設定、アプリの読み込み、調査の維持、終了後のクリーンアップを REA が担います。 |
| **一連の調査に対応** | 最初の概要から疑似コード、呼び出し関係、型、実装の手掛かりまで掘り下げられます。 |
| **ローカルで解析** | 解析は対応するローカルホストで実行され、REA がバイナリをホスト型解析サービスへ送ることはありません。 |
| **コンテキストを維持** | 質問ごとに解析を最初からやり直さず、複数のバイナリを続けて調査できます。 |
## クイックスタート
### コーディングエージェントから(推奨)
### セットアップを実行(推奨)
REA をエージェントで使うための設定を行います:
```bash
npx --yes rea-agents@latest setup
npx rea-agents setup
```
エージェントに REA のセットアップを依頼してください。Mac を確認し、必要なインストールを説明して承認を求め、システムプロンプトを案内します。完全なツールを読み込むため再起動を求められた場合は、セットアップ後に再起動してください。
Setup は最初に連携するエージェントを複数選択できるようにします。既存の REA 登録は初期選択されますが、検出されただけのクライアントは自動選択されず、未設定のクライアントも選べます。具体的なパスと変更内容を確認してから承認してください。選択したエージェントには通常 REA のワークフローをインストールします。Hopper は別の任意操作で、個別の承認が必要です。既存の Ghidra のパスも登録できます。
### 始める前に
変更は事前に表示され、既存の設定はバックアップされます。要件と追加オプションは[インストールガイド](docs/installation.md)を参照してください。
- macOS 12 以降
- Ubuntu 24.04+、Fedora 41+、または 64 ビット Arch Linux
- Node.js 22.19+ または 24.11+ と、Node に付属する npm
### エージェントで使う
`rea setup` は変更計画を表示し、確認後に適用します。Homebrew、Node.js、npm をインストールまたは更新しません。[Hopper](https://www.hopperapp.com/) がない場合は公式パッケージを提案します。Hopper は別製品で、ライセンスは別途必要です。
設定後にエージェントを再起動し、調べたいアプリや機能を説明します。REA は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code、VS Code に対応します。既存の REA 登録は初期選択され、それ以外の検出済みクライアントは選択が必要です。その他のエージェントは下記の MCP 設定を使えます。
64 ビット Linux に Ghidra 12.1.4 PUBLIC と完全な 64 ビット JDK 21 がすでにある場合、Setup は承認後に `GHIDRA_INSTALL_DIR` と任意の `JAVA_HOME` も登録できます。REA は Ghidra や Java をダウンロード、インストール、変更しません。Ghidra プロバイダーは隔離された読み取り専用 headless セッションで、インベントリ、逆コンパイル、アセンブリ、呼び出し関係、型付き参照、xref、CFG、関数 dossier を提供します。GUI 状態と変更操作は引き続き利用できません。
Hopper はデモモードで使えます。初回起動の画面ではデモを選択するか、既存のライセンスを入力してください。
#### Linux のインストールとトラブルシューティング
### ターミナルで使う
Ubuntu 24.04+、Fedora 41+、64 ビット Arch Linux では、REA が公式の DEB、RPM、または Arch パッケージを取得し、公開されたサイズとチェックサムを検証してから `apt-get`、`dnf`、または `pacman` で依存関係を解決します。root 以外では `pkexec` がシステム承認を表示します。REA は `sudo` を呼び出しません。
既定のランチャーは `/opt/hopper/bin/Hopper` です。別の場所では `HOPPER_LAUNCHER_PATH` を設定してください。Doctor が解析エンジン不足を報告した場合は `ldd /opt/hopper/bin/Hopper | grep 'not found'` を実行し、不足ライブラリを入れて `rea setup` を再実行します。REA は対応する Hopper デモ版を専用 Xvfb ディスプレイで起動し、解析セッションごとに Hopper が提示するデモモードを選択します。デスクトップの `DISPLAY` や有料ライセンスは不要ですが、デモ版にはベンダー所定の制限があります。`~/.local/bin` も `PATH` に追加してください。
```bash
# 1. REA をセットアップ
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx --yes rea-agents@latest setup
```
macOS やインストーラーから確認を求められた場合は、その操作を完了してから同じコマンドをもう一度実行してください。
### 2. コーディングエージェントを再起動
Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin を検出します。検出した最初の 6 クライアントは自動設定しますが、Devin はローカル MCP 設定境界が文書化されていないため、検出結果を報告するだけで変更しません。設定されたクライアントを再起動して REA を読み込んでください。
### 3. エージェントに依頼
アプリは名前で指定できます。コーディングエージェントがアプリを探し、REA に必要なプログラムファイルを渡します。
```text
REA でメモアプリをリバースエンジニアリングしてください。検索機能の仕組みと
根拠を示し、SQLite を使って私のプロジェクト向けに同様の機能を実装してください。
```
問題がある場合は次を実行します:
設定後に実行します:
```bash
npx -y rea-agents@latest doctor
rea uninstall
rea uninstall --purge-data
npx -y rea-agents@latest analyze /Applications/Notes.app
```
### rea コマンドをインストール
コマンドラインツールをインストールします:
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
```
Node.js と npm を先にインストールしてください。ターミナルで実行すると、インストーラーは `rea` を追加してセットアップを開始します。
npm でインストールし、セットアップを実行することもできます:
```bash
npm install --global rea-agents
rea setup
```
### 要件
- macOS 12 以降
- Ubuntu 24.04+、Fedora 41+、または 64 ビット Arch Linux
- Node.js 22.x (>=22.19)、24.x (>=24.11)、または 26+ と npm
ネイティブバイナリ解析には Hopper または Ghidra が必要です。Hopper は別製品です。デモにはベンダー所定の制限がありますが、有料ライセンスは必須ではありません。
Ghidra は Linux x64 と macOS x64/arm64 に対応します。Ghidra 12.1.4 と完全な 64 ビット JDK 21 を別途インストールし、REA で使うように設定してください。macOS ではホストのアーキテクチャに合うネイティブデコンパイラーも必要です。
Setup はインストールを確認し、パスを保存できます。Ghidra、Java、Node.js、npm、Homebrew のインストールや更新は行いません。
Windows の Ghidra 解析は利用できません。プロセスの所有権、プライベートディレクトリの権限、安全なパス検証が未実装のため、Ghidra と Java を正しくインストールしても有効になりません。[Windows Ghidra P0](docs/windows-ghidra-p0.md)を参照してください。
### トラブルシューティング
`npx -y rea-agents@latest doctor` はホスト、依存関係、解析ツール、エージェント設定を変更せずに確認します。構造化された診断には `--json` を追加してください。
Linux の既定ランチャーは `/opt/hopper/bin/Hopper` です。別の場所には `HOPPER_LAUNCHER_PATH` を設定します。ファイルがあるのに解析エンジンが見つからない場合は、`ldd /opt/hopper/bin/Hopper | grep 'not found'` で不足ライブラリを確認してください。詳しくは [Hopper ガイド](docs/installation.md#hopper)を参照してください。
### 更新とアンインストール
- `rea update` は現在の REA インストールを更新します。
- `rea uninstall` は REA が管理するエージェント登録とワークフローファイルを削除します。Hopper は残ります。
- `rea uninstall --purge-data` は REA のキャッシュと状態も削除します。それらを削除したい場合にだけ使ってください。
## 現在の対応状況
REA は CLI と MCP からネイティブバイナリ、JavaScript/Electron アプリ、.NET アセンブリ、Web サイトを解析できます。使える操作はホスト、対象、選択した解析ツールによって異なります。
- Hopper はネイティブ解析と注釈操作に対応します。GUI の動作はプラットフォームによって異なります。
- Ghidra は Linux x64 と macOS x64/arm64 で、一覧、検索、逆コンパイル、アセンブリ、呼び出し、参照、命令、型の検査など 22 の読み取り専用操作を提供します。GUI 操作や変更操作はありません。
- ブラウザー、Electron、プロセスの実行時ワークフローには、それぞれ設定、承認、終了処理の要件があります。詳細は [English README](README.md#current-status) を参照してください。
- Windows の Ghidra 解析は未対応です。進捗は [#527](https://github.com/morluto/rea/issues/527) で確認できます。
## ひとつのプロンプトで調査を完結
```text
@@ -149,22 +191,26 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
## 調査ツールカタログ
| ツール群 | 数 | 例 |
| ---------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| バイナリ調査 | 36 | プロシージャ、疑似コード、アセンブリ、文字列、名前、セグメント、callers、callees、xrefs、注釈 |
| 合成解析 | 14 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift / ObjC 検出 |
| macOS ネイティブ | 5 | Mach-O メタデータ、署名、plist、アーキテクチャ、Swift デマングル。Hopper 起動不要 |
| アーティファクトグラフ | 3 | ディレクトリと対応パッケージの完全なインライン調査、および明示選択されたトランザクション抽出 |
| Managed PE/CLI | 8 | PE/CLI 識別、メタデータメンバー、CIL ハッシュ、P/Invoke/ネイティブ境界宣言と検証、アプリケーショングラフ投影、逆コンパイル再構成インポート、構造的 token 再マッピング、ランタイム相関計画、バージョン比較 |
| ブラウザ観察 | 9 | origin 限定 CDP 取得、bundle/source map 解析、WebMCP 検出、セッション、capture diff、視覚証拠 |
| Electron 解析 | 5 | canonical ファイルルート内の受動観察、有界な静的アプリマッピング、Evidence に基づく静的/実行時の照合 |
| JavaScript ランタイム | 2 | 承認済みのアタッチ専用 Node/Electron Inspector ターゲット検出と、評価や計装を行わない有界なスクリプト/実行コンテキスト観察 |
| アプリケーション | 10 | 有界なクロスレイヤー追跡、一意限定のバージョン比較、静的なエクスポート戻り値形状比較、承認済み Linux 隔離 extracted-module replay、マネージドランタイム特性評価、再構築カバレッジ閉包、決定論的な義務台帳 |
| バイナリセッション | 22 | `open_binary`、`binary_session`、証拠バンドル、有限リプレイ状態機械、プロセス/成果物/関数比較、残余未知レジストリ |
| ツール分類 | 数 | 用途 |
| -------------------- | --: | --------------------------------------------------------------------------- |
| バイナリ検査 | 41 | 関数、疑似コード、アセンブリ、文字列、シンボル、参照、注釈 |
| 組み合わせた解析 | 14 | 概要、関数解析、一括逆コンパイル、呼び出しグラフ、Swift と ObjC |
| macOS ネイティブ | 7 | Mach-O メタデータ、署名、plist、アーキテクチャ、Swift 名の復元 |
| ファイルとパッケージ | 5 | ディレクトリとパッケージ、Interface Builder、Apple リソース、抽出 |
| .NET PE/CLI | 7 | アセンブリ識別、メタデータ、CIL、ネイティブ呼び出し、比較、再構築の取り込み |
| ブラウザー観察 | 9 | ページ、スクリプト、ソースマップ、WebMCP、スクリーンショット、比較 |
| Electron 解析 | 5 | ページ観察、アプリ構造、静的・実行時結果の対応付け |
| JavaScript 実行時 | 2 | 既存の Node/Electron Inspector に接続し、スクリプトと実行コンテキストを観察 |
| アプリのワークフロー | 7 | 機能追跡、バージョン比較、戻り値構造の比較、再実装の検証 |
| バイナリセッション | 21 | 対象切り替え、根拠の保存、プロセス・関数比較、未解決事項の記録 |
## ロードマップ
次の作業はネイティブ対象の検証拡大、JavaScript と .NET のバージョン比較、実行時の観察と再実装の検証です。Setup はエージェント連携と Hopper のインストールを選択できます。他の解析ツールのインストールは今後の作業です。[インストールのロードマップ](docs/roadmap.md)と[解析ツールの評価](docs/provider-evaluation.md)を参照してください。
## 他のコーディングエージェントで使う
Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin を検出し、最初の 6 クライアントを自動設定します。Devin は検出結果を報告するだけで変更しません。ローカル MCP サーバーに対応するコーディングエージェントなら、次の設定で REA を利用できます。
Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code、VS Code に対応します。既存の REA 登録は初期選択され、それ以外の検出済みクライアントは選択が必要です。ローカル MCP サーバーに対応するエージェントは、次の設定でも接続できます。
<!-- x-release-please-start-version -->
@@ -173,7 +219,7 @@ Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@3.2.1", "mcp"]
"args": ["-y", "rea-agents@4.0.1", "mcp"]
}
}
}
@@ -193,7 +239,7 @@ flowchart LR
Ghidra --> App
```
CLI と MCP サーバーは同じ解析エンジンを使います。ターミナルのコマンドは終了時にアプリを閉じ、エージェントのセッションは調査中アプリを開いたままにします。
CLI と MCP サーバーは同じ解析処理を使います。ターミナルコマンドは完了後に自分のブリッジセッションを解放し、エージェントセッションは調査中の接続を維持できます。REA セッションを閉じても、ユーザーが利用中の Hopper は終了しません。
## CLI
@@ -229,7 +275,7 @@ REA は必要なときに Hopper を起動します。Hopper のランチャー
<details><summary><strong>Hopper を先に起動する必要がありますか?</strong></summary>
いいえ。REA が必要時に起動します。すでに起動している Hopper にも対応します。
いいえ。REA が必要時に起動します。Hopper が起動済みでも使えますが、既存の GUI ドキュメントには接続せず、新しい解析ドキュメントを開きます。
</details>
+122 -75
View File
@@ -4,7 +4,7 @@
# REA: 무엇이든 리버스 엔지니어링
### 코딩 에이전트가 무엇이든 리버스 엔지니어링할 수 있게 하는 하나의 CLI 및 MCP 서버
### 에이전트로 앱의 동작부터 네이티브 바이너리까지 조사하세요.
**마음에 드는 기능을 찾고, 작동 방식을 이해하고, 원하는 방식으로 구현하세요.**
@@ -13,35 +13,52 @@
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#조사-도구-카탈로그)
[![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
[![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM)
[빠른 시작](#빠른-시작) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [도구 카탈로그](#조사-도구-카탈로그) · [작동 방식](#작동-방식) · [FAQ](#faq)
<a href="https://trendshift.io/repositories/82054?utm_source=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>
[빠른 시작](#빠른-시작) · [현재 지원 범위](#현재-지원-범위) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [조사 도구 카탈로그](#조사-도구-카탈로그) · [로드맵](#로드맵) · [작동 방식](#작동-방식)
<table aria-label="REA community">
<tr>
<td align="center" width="360">
<a href="https://discord.gg/GkcryMnJDM">
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
<strong>리버스 엔지니어링 커뮤니티에 참여하세요</strong>
</a><br />
<sub>Discord · 질문과 답변 · 작업 공유</sub>
</td>
</tr>
</table>
<br />
<code>curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash</code>
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="Hopper에서 분석 브리지를 시작하고 네이티브 바이너리를 조사하는 REA" width="1200" />
</div>
---
어떤 앱에서 마음에 드는 기능을 자신의 제품에도 넣고 싶나요? 소스 코드가 없어도 앱을 코딩 에이전트에 전달할 수 있습니다. REA를 사용하면 에이전트가 기능을 조사하고 작동 방식을 이해한 뒤, 사용자의 기술 스택, 디자인, 요구 사항에 맞는 버전을 구현할 수 있습니다.
앱에서 마음에 드는 기능을 찾았다면 에이전트에게 REA로 조사해 달라고 요청하세요. 소스 코드가 없어도 앱을 분석하고, 기능의 동작과 근거를 설명한 뒤, 프로젝트에 맞는 기능을 구현할 수 있습니다.
REA는 이 과정을 하나의 CLI 및 MCP 서버로 제공합니다. 에이전트는 컴파일된 앱을 조사하고 기능의 작동 방식을 추적한 뒤 알아낸 내용을 일반 코딩 작업에 활용할 수 있습니다. 복잡한 리버스 엔지니어링 도구는 REA가 하나의 인터페이스 뒤에서 처리합니다.
REA는 네이티브 바이너리, JavaScript/Electron 앱, .NET 어셈블리, 웹사이트 분석 도구를 제공합니다. 에이전트와 터미널에서 같은 도구를 사용할 수 있습니다. 분석은 로컬에서 실행되며 결과에는 근거와 제한 사항이 포함됩니다.
Setup은 에이전트를 설정하고 기존 Hopper 또는 Ghidra 설치에 연결합니다. 승인 후 Hopper를 설치할 수도 있습니다.
## 에이전트에게 바로 요청하기
```bash
npx rea-agents setup
```
그런 다음 요청하세요.
[설정](#빠른-시작)을 마친 뒤 에이전트를 다시 시작하고 요청하세요.
```text
REA를 설정하고 메모 앱을 리버스 엔지니어링해 주세요. 검색 기능의 작동 방식과
그렇게 판단한 이유를 보여 주고, 제 프로젝트에 비슷한 기능을 구현해 주세요.
메모 앱의 검색 기능이 어떻게 동작하는지 조사하고 근거를 보여 주세요.
그다음 제 프로젝트에 비슷한 기능을 구현해 주세요.
```
메모 앱은 예시일 뿐입니다. 이해하고 싶은 앱을 지정하거나 먼저 개요부터 살펴보라고 요청할 수 있습니다.
메모 앱 대신 조사할 앱을 지정하거나 먼저 개요를 요청할 수 있습니다.
## 바이너리에서 동작까지
@@ -53,70 +70,96 @@ REA는 분석을 바이너리 증거에 근거하게 합니다. 원본 소스
## REA를 사용하는 이유
| | |
| ------------------ | ---------------------------------------------------------------------------------- |
| **에이전트용** | 컴파일된 앱에 관해 질문하고 추측 대신 증거를 수집하게 합니다. |
| **CLI와 MCP** | 터미널과 코딩 에이전트에서 동일한 리버스 엔지니어링 기능을 사용합니다. |
| **복잡성 처리** | 도구 설정, 앱 열기, 조사 유지, 작업 후 정리를 REA가 처리합니다. |
| **전체 조사 과정** | 첫 개요에서 의사 코드, 호출 관계, 타입, 구현 단서까지 이어서 조사합니다. |
| **로컬 분석** | 분석은 Mac에서 실행되며 REA는 바이너리를 호스팅 분석 서비스에 업로드하지 않습니다. |
| **컨텍스트 유지** | 질문마다 분석을 처음부터 시작하지 않고 여러 바이너리를 연속으로 조사합니다. |
| | |
| ------------------ | --------------------------------------------------------------------------------------------------- |
| **에이전트용** | 컴파일된 앱에 관해 질문하고 추측 대신 증거를 수집하게 합니다. |
| **CLI와 MCP** | 터미널과 코딩 에이전트에서 동일한 리버스 엔지니어링 기능을 사용합니다. |
| **복잡성 처리** | 도구 설정, 앱 열기, 조사 유지, 작업 후 정리를 REA가 처리합니다. |
| **전체 조사 과정** | 첫 개요에서 의사 코드, 호출 관계, 타입, 구현 단서까지 이어서 조사합니다. |
| **로컬 분석** | 분석은 지원되는 로컬 호스트에서 실행되며 REA는 바이너리를 호스팅 분석 서비스에 업로드하지 않습니다. |
| **컨텍스트 유지** | 질문마다 분석을 처음부터 시작하지 않고 여러 바이너리를 연속으로 조사합니다. |
## 빠른 시작
### 코딩 에이전트에서 시작하기(권장)
### 설정 실행하기(권장)
REA를 에이전트에 연결하세요.
```bash
npx --yes rea-agents@latest setup
npx rea-agents setup
```
에이전트에게 REA 설정을 요청하세요. Mac을 확인하고 필요한 설치 내용을 설명한 뒤 승인을 요청하며 시스템 프롬프트를 안내합니다. 전체 도구를 불러오기 위해 재시작하라고 하면 설정 후 에이전트를 재시작하세요.
Setup은 먼저 연결할 에이전트를 여러 개 선택하도록 안내합니다. 기존 REA 등록은 기본 선택되며, 감지만 된 클라이언트는 자동 선택되지 않습니다. 아직 설정되지 않은 클라이언트도 직접 선택할 수 있습니다. 정확한 경로와 변경 사항을 검토한 뒤 승인하세요. 선택한 에이전트에는 기본적으로 REA 워크플로가 설치됩니다. Hopper는 별도의 선택 사항이며 따로 승인해야 합니다. 기존 Ghidra 경로도 등록할 수 있습니다.
### 시작하기 전에
변경 전에 계획을 표시하고 기존 설정을 백업합니다. 요구 사항과 추가 옵션은 [설치 안내](docs/installation.md)를 참고하세요.
- macOS 12 이상
- Ubuntu 24.04+, Fedora 41+ 또는 64비트 Arch Linux
- Node.js 22.19+ 또는 24.11+ 및 Node와 함께 제공되는 npm
### 에이전트에서 사용하기
`rea setup`은 전체 변경 계획을 표시하고 확인 후 적용합니다. Homebrew, Node.js 또는 npm을 설치하거나 업데이트하지 않습니다. [Hopper](https://www.hopperapp.com/)가 없으면 공식 패키지 설치를 제안합니다. Hopper는 별도 라이선스가 필요한 독립 소프트웨어입니다.
설정 후 에이전트를 다시 시작하고 조사할 앱이나 기능을 설명하세요. REA는 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, Command Code, VS Code를 지원합니다. 기존 REA 등록은 기본 선택되고, 그 밖의 감지된 클라이언트는 직접 선택해야 합니다. 다른 에이전트는 아래 MCP 설정을 사용할 수 있습니다.
64비트 Linux에 Ghidra 12.1.4 PUBLIC과 완전한 64비트 JDK 21이 이미 설치되어 있다면 Setup은 승인 후 `GHIDRA_INSTALL_DIR`과 선택 사항인 `JAVA_HOME`도 등록할 수 있습니다. REA는 Ghidra나 Java를 다운로드, 설치 또는 수정하지 않습니다. Ghidra 공급자는 격리된 읽기 전용 headless 세션에서 인벤토리, 디컴파일, 어셈블리, 호출 관계, 형식이 지정된 참조, xref, CFG 및 함수 dossier를 제공합니다. GUI 상태와 변경 작업은 계속 사용할 수 없습니다.
Hopper는 데모 모드로 사용할 수 있습니다. 첫 실행 안내가 나오면 데모를 선택하거나 기존 라이선스를 입력하세요.
#### Linux 설치 및 문제 해결
### 터미널에서 실행하기
Ubuntu 24.04+, Fedora 41+, 64비트 Arch Linux에서 REA는 공식 DEB, RPM 또는 Arch 패키지를 내려받아 게시된 크기와 체크섬을 검증한 뒤 `apt-get`, `dnf` 또는 `pacman`으로 의존성을 설치합니다. root가 아니면 `pkexec`가 시스템 승인 창을 표시합니다. REA는 `sudo`를 호출하지 않습니다.
기본 실행 파일은 `/opt/hopper/bin/Hopper`입니다. 다른 위치에 설치했다면 `HOPPER_LAUNCHER_PATH`를 설정하세요. Doctor가 분석 엔진 누락을 보고하면 `ldd /opt/hopper/bin/Hopper | grep 'not found'`를 실행하고 누락된 라이브러리를 설치한 다음 `rea setup`을 다시 실행하세요. REA는 지원되는 Hopper 데모 빌드를 전용 Xvfb 디스플레이에서 실행하고 분석 세션마다 Hopper가 제공하는 데모 모드를 선택합니다. 데스크톱 `DISPLAY`나 유료 라이선스는 필요하지 않지만 데모에는 공급업체가 정한 제한이 있습니다. `~/.local/bin`도 `PATH`에 추가해야 합니다.
```bash
# 1. REA 설정
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx --yes rea-agents@latest setup
```
macOS나 설치 프로그램이 확인을 요청하면 해당 절차를 완료하고 같은 명령을 다시 실행하세요.
### 2. 코딩 에이전트 재시작
Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin을 감지합니다. 감지된 앞의 여섯 클라이언트는 자동으로 구성하지만, Devin은 문서화된 로컬 MCP 구성 경계가 없어 감지만 보고하고 변경하지 않습니다. 구성된 클라이언트를 다시 시작해 REA를 불러오세요.
### 3. 에이전트에게 요청
앱 이름만 말해도 됩니다. 코딩 에이전트가 앱을 찾아 REA에 필요한 프로그램 파일을 전달합니다.
```text
REA로 메모 앱을 리버스 엔지니어링해 주세요. 검색 기능의 작동 방식과 증거를 보여 주고,
SQLite를 사용해 제 프로젝트에 비슷한 기능을 구현해 주세요.
```
문제가 있으면 다음을 실행하세요.
설정 후 실행하세요.
```bash
npx -y rea-agents@latest doctor
rea uninstall
rea uninstall --purge-data
npx -y rea-agents@latest analyze /Applications/Notes.app
```
### rea 명령 설치하기
명령줄 도구를 설치하세요.
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
```
Node.js와 npm이 먼저 설치되어 있어야 합니다. 터미널에서 실행하면 설치 프로그램이 `rea`를 설치하고 설정을 시작합니다.
npm으로 설치한 뒤 설정을 실행할 수도 있습니다.
```bash
npm install --global rea-agents
rea setup
```
### 요구 사항
- macOS 12 이상
- Ubuntu 24.04+, Fedora 41+ 또는 64비트 Arch Linux
- Node.js 22.x (>=22.19), 24.x (>=24.11) 또는 26+와 npm
네이티브 바이너리 분석에는 Hopper 또는 Ghidra가 필요합니다. Hopper는 별도 소프트웨어입니다. 데모에는 공급업체의 제한이 있지만 유료 라이선스가 필수는 아닙니다.
Ghidra는 Linux x64와 macOS x64/arm64를 지원합니다. Ghidra 12.1.4와 완전한 64비트 JDK 21을 별도로 설치한 뒤 REA가 사용하도록 설정하세요. macOS에서는 호스트 아키텍처에 맞는 네이티브 디컴파일러도 필요합니다.
Setup은 설치를 확인하고 경로를 저장할 수 있습니다. Ghidra, Java, Node.js, npm, Homebrew를 설치하거나 업데이트하지 않습니다.
Windows Ghidra 분석은 사용할 수 없습니다. 프로세스 소유권, 비공개 디렉터리 권한, 안전한 경로 검사가 구현되지 않았으므로 Ghidra와 Java를 올바르게 설치해도 분석을 활성화할 수 없습니다. [Windows Ghidra P0](docs/windows-ghidra-p0.md)를 참고하세요.
### 문제 해결
`npx -y rea-agents@latest doctor`는 호스트, 의존성, 분석 도구, 에이전트 설정을 변경 없이 확인합니다. 구조화된 진단에는 `--json`을 추가하세요.
Linux의 기본 Hopper 실행 파일은 `/opt/hopper/bin/Hopper`입니다. 다른 위치에는 `HOPPER_LAUNCHER_PATH`를 설정하세요. 파일이 있는데도 분석 엔진이 없다고 하면 `ldd /opt/hopper/bin/Hopper | grep 'not found'`로 누락된 라이브러리를 확인하세요. 자세한 내용은 [Hopper 안내](docs/installation.md#hopper)를 참고하세요.
### 업데이트와 제거
- `rea update`는 현재 REA 설치를 업데이트합니다.
- `rea uninstall`은 REA가 관리하는 에이전트 등록과 워크플로 파일을 제거합니다. Hopper는 유지합니다.
- `rea uninstall --purge-data`는 REA 캐시와 상태도 삭제합니다. 해당 데이터를 지우려는 경우에만 사용하세요.
## 현재 지원 범위
REA는 CLI와 MCP로 네이티브 바이너리, JavaScript/Electron 앱, .NET 어셈블리, 웹사이트를 분석합니다. 사용할 수 있는 작업은 호스트, 대상, 선택한 분석 도구에 따라 다릅니다.
- Hopper는 네이티브 분석과 주석 작업을 지원합니다. GUI 동작은 플랫폼에 따라 다릅니다.
- Ghidra는 Linux x64와 macOS x64/arm64에서 목록, 검색, 디컴파일, 어셈블리, 호출, 참조, 명령어, 타입 검사 등 읽기 전용 작업 22개를 제공합니다. GUI나 변경 작업은 제공하지 않습니다.
- 브라우저, Electron, 프로세스 런타임 워크플로에는 각각 설정, 승인, 종료 처리 요구 사항이 있습니다. 자세한 내용은 [English README](README.md#current-status)를 참고하세요.
- Windows Ghidra 분석은 아직 사용할 수 없습니다. 진행 상황은 [#527](https://github.com/morluto/rea/issues/527)에서 확인하세요.
## 하나의 프롬프트로 전체 조사
```text
@@ -149,22 +192,26 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
## 조사 도구 카탈로그
| 도구 그룹 | 수 | 예시 |
| ------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 바이너리 검사 | 36 | 프로시저, 의사 코드, 어셈블리, 문자열, 이름, 세그먼트, callers, callees, xrefs, 주석 |
| 합성 분석 | 14 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift 및 ObjC 탐색 |
| macOS 네이티브 도구 | 5 | Mach-O 메타데이터, 코드 서명, plist, 아키텍처, Swift 디맹글링. Hopper 실행 불필요 |
| 아티팩트 그래프 | 3 | 디렉터리와 지원 패키지의 전체 인라인 검사 및 명시적으로 선택한 트랜잭션 추출 |
| Managed PE/CLI | 8 | PE/CLI ID, 메타데이터 멤버, CIL 해시, P/Invoke/네이티브 경계 선언 및 검증, 애플리케이션 그래프 투영, 디컴파일 재구성 가져오기, 구조적 token 재매핑, 런타임 상관 계획, 버전 비교 |
| 브라우저 관찰 | 9 | origin 제한 CDP 캡처, bundle/source map 분석, WebMCP 검색, 세션 타임라인, capture diff 및 시각 증거 |
| Electron 분석 | 5 | canonical 파일 루트 내 수동 관찰, 제한된 정적 앱 매핑, Evidence 기반 정적/런타임 조정 |
| JavaScript 런타임 | 2 | 승인된 연결 전용 Node/Electron Inspector 대상 탐색과 평가·계측 없는 제한된 스크립트 및 실행 컨텍스트 관찰 |
| 애플리케이션 | 10 | 제한된 교차 계층 추적, 고유 일치 버전 비교, 정적 export 반환 형태 비교, 승인된 Linux 격리 extracted-module replay, 관리형 런타임 특성화, 재구성 커버리지 폐쇄, 결정론적 의무 원장 |
| 바이너리 세션 | 22 | `open_binary`, `binary_session`, 증거 번들, 유한 replay 상태 머신, 프로세스·아티팩트·함수 비교, 잔여 미지수 레지스트리 |
| 도구 분류 | 수 | 용도 |
| ----------------- | --: | -------------------------------------------------------------------- |
| 바이너리 검사 | 41 | 함수, 의사 코드, 어셈블리, 문자열, 심볼, 참조, 주석 |
| 결합 분석 | 14 | 개요, 함수 분석, 일괄 디컴파일, 호출 그래프, Swift와 ObjC 검사 |
| macOS 네이티브 | 7 | Mach-O 메타데이터, 서명, plist, 아키텍처, Swift 이름 복원 |
| 파일과 패키지 | 5 | 디렉터리와 패키지 검사, Interface Builder, Apple 리소스, 추출 |
| .NET PE/CLI | 7 | 어셈블리 식별, 메타데이터, CIL, 네이티브 호출, 비교, 재구성 가져오기 |
| 브라우저 관찰 | 9 | 페이지, 스크립트, 소스 맵, WebMCP, 스크린샷, 캡처 비교 |
| Electron 분석 | 5 | 페이지 관찰, 앱 구조, 정적·런타임 결과 연결 |
| JavaScript 런타임 | 2 | 기존 Node/Electron Inspector에 연결해 스크립트와 실행 컨텍스트 관찰 |
| 앱 워크플로 | 7 | 기능 추적, 버전 비교, 반환 구조 비교, 재구현 검증 |
| 바이너리 세션 | 21 | 대상 전환, 근거 저장, 프로세스·함수 비교, 미해결 항목 기록 |
## 로드맵
다음 작업은 네이티브 대상 검증 확대, JavaScript와 .NET의 버전 비교, 런타임 관찰과 재구현 검증입니다. Setup은 에이전트 연결과 Hopper 설치 선택을 이미 지원합니다. 다른 분석 도구 설치는 향후 작업입니다. [설치 로드맵](docs/roadmap.md)과 [분석 도구 평가](docs/provider-evaluation.md)를 참고하세요.
## 다른 코딩 에이전트에서 사용하기
Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin을 감지하고 앞의 여섯 클라이언트를 자동으로 구성합니다. Devin은 감지만 보고하고 변경하지 않습니다. 로컬 MCP 서버를 지원하는 코딩 에이전트라면 다음 설정으로 REA를 사용할 수 있습니다.
Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, Command Code, VS Code를 지원합니다. 기존 REA 등록은 기본 선택되고, 그 밖의 감지된 클라이언트는 직접 선택해야 합니다. 로컬 MCP 서버를 지원하는 에이전트는 다음 설정으로 연결할 수 있습니다.
<!-- x-release-please-start-version -->
@@ -173,7 +220,7 @@ Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@3.2.1", "mcp"]
"args": ["-y", "rea-agents@4.0.1", "mcp"]
}
}
}
@@ -193,7 +240,7 @@ flowchart LR
Ghidra --> App
```
CLI와 MCP 서버는 같은 분석 엔진을 사용합니다. 터미널 명령은 끝날 때 앱을 닫고, 에이전트 세션은 조사하는 동안 앱을 열어 둡니다.
CLI와 MCP 서버는 같은 분석 워크플로를 사용합니다. 터미널 명령은 완료 후 자신의 브리지 세션을 해제하고, 에이전트 세션은 조사 중 연결을 유지할 수 있습니다. REA 세션을 닫아도 사용자가 이용 중인 Hopper 앱은 종료하지 않습니다.
## CLI
@@ -229,7 +276,7 @@ REA는 필요할 때 Hopper를 시작합니다. Hopper 런처는 내부적으로
<details><summary><strong>Hopper를 미리 실행해야 하나요?</strong></summary>
아니요. REA가 필요할 때 시작하며 이미 실행 중인 Hopper도 지원합니다.
아니요. REA가 필요할 때 시작합니다. Hopper가 실행 중이어도 사용할 수 있지만 기존 GUI 문서에 연결하는 대신 새 분석 문서를 엽니다.
</details>
+122 -76
View File
@@ -4,7 +4,7 @@
# REA:逆向工程一切
### 一个 CLI 与 MCP 服务器,让编程智能体逆向工程任何程序
### 使用智能体逆向工程任何程序,从应用行为到原生二进制文件。
**看到喜欢的功能。理解它的原理。按照你的方式实现。**
@@ -13,35 +13,51 @@
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#调查工具目录)
[![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
[![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM)
[快速开始](#快速开始) · [从二进制到行为](#从二进制到行为) · [工具目录](#调查工具目录) · [工作原理](#工作原理) · [常见问题](#常见问题)
<a href="https://trendshift.io/repositories/82054?utm_source=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>
[快速开始](#快速开始) · [当前状态](#当前状态) · [从二进制到行为](#从二进制到行为) · [调查工具目录](#调查工具目录) · [路线图](#路线图) · [工作原理](#工作原理)
<table aria-label="REA community">
<tr>
<td align="center" width="360">
<a href="https://discord.gg/GkcryMnJDM">
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
<strong>加入逆向工程社区</strong>
</a><br />
<sub>Discord · 问答 · 成果分享</sub>
</td>
</tr>
</table>
<br />
<code>curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash</code>
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="REA 在 Hopper 中启动分析桥并检查原生二进制文件" width="1200" />
</div>
---
看到某个应用中想加入自己产品的功能?即使没有源代码,也可以把应用交给编程智能体。借助 REA,智能体能够调查该功能、理解其工作原理,并按照你的技术栈、设计和需求构建适合你产品的版本。
看到某个应用中想加入自己产品的功能?让智能体用 REA 调查它。即使没有源代码,智能体也可以检查应用、解释功能的工作方式、展示证据,并为你的项目实现类似功能。
REA 通过一个 CLI 与 MCP 服务器实现这套流程。智能体可以检查编译后的应用、追踪功能的工作方式,并把了解到的内容用于日常编码。REA 在一个接口背后处理复杂的逆向工程工具。
REA 提供原生二进制、JavaScript 与 Electron 应用、.NET 程序集和网站的分析工具。你可以从智能体或终端使用它们。分析在本机运行,结果会说明证据和限制。
Setup 可以配置智能体、连接已安装的 Hopper 或 Ghidra,也可以在你批准后安装 Hopper。
## 直接询问智能体
```bash
npx rea-agents setup
```
然后说:
完成[设置](#快速开始)后,重启智能体并提出问题:
```text
逆向工程“备忘录”应用。解释搜索功能的工作方式,
说明你是如何得出结论的,然后为我的项目构建类似功能。
解释“备忘录”应用的搜索功能,展示证据,然后为我的项目实现类似功能。
```
“备忘录”只是示例。你可以说出任何想了解的应用,也可以让智能体先从概览开始。
把“备忘录”替换成你想了解的应用,也可以先请求概览。
## 从二进制到行为
@@ -53,70 +69,96 @@ REA 让调查始终以二进制证据为依据。它不会声称能恢复原始
## 为什么选择 REA
| | |
| ---------------- | -------------------------------------------------------------- |
| **为智能体设计** | 直接询问编译后应用的行为,让智能体搜集证据,而不是猜测。 |
| **CLI 与 MCP** | 在终端或编程智能体中使用同一套逆向工程能力。 |
| **处理复杂流程** | REA 负责工具设置、打开应用、维持调查过程,并在完成后清理资源。 |
| **完整工作流** | 从初步概览推进到伪代码、调用关系、类型和实现线索。 |
| **本地运行** | 分析在你的 Mac 上运行;REA 不会把二进制上传到托管式分析服务。 |
| **保留上下文** | 连续调查多个二进制文件,无需为每个问题重新开始整个分析过程。 |
| | |
| ---------------- | -------------------------------------------------------------------- |
| **为智能体设计** | 直接询问编译后应用的行为,让智能体搜集证据,而不是猜测。 |
| **CLI 与 MCP** | 在终端或编程智能体中使用同一套逆向工程能力。 |
| **处理复杂流程** | REA 负责工具设置、打开应用、维持调查过程,并在完成后清理资源。 |
| **完整工作流** | 从初步概览推进到伪代码、调用关系、类型和实现线索。 |
| **本地运行** | 分析在受支持的本机系统上运行;REA 不会把二进制上传到托管式分析服务。 |
| **保留上下文** | 连续调查多个二进制文件,无需为每个问题重新开始整个分析过程。 |
## 快速开始
### 使用编程智能体(推荐)
### 运行设置(推荐)
配置 REA 与智能体的连接:
```bash
npx --yes rea-agents@latest setup
npx rea-agents setup
```
让智能体设置 REA。它会检查你的 Mac,说明需要安装的内容,先征得同意,并引导你处理系统提示。如果智能体要求重启以加载完整工具,请在设置后重启。
Setup 首先让你多选要连接的智能体。已有 REA 注册默认选中;仅被检测到的客户端不会自动选中,也可以手动选择尚未配置的客户端。查看具体路径和变更后再批准。所选智能体默认安装 REA 工作流;Hopper 是单独的可选操作,需要单独批准。Setup 也可以记录现有 Ghidra 的路径。
### 开始之前
Setup 会先展示变更并备份已有配置。要求与更多选项见[安装与设置](docs/installation.md)。
- macOS 12 或更高版本
- Ubuntu 24.04+、Fedora 41+ 或 64 位 Arch Linux
- Node.js 22.19+ 或 24.11+,以及随 Node 安装的 npm
### 使用智能体
`rea setup` 会先显示完整变更计划并请求确认。它不会安装或更新 Homebrew、Node.js 或 npm。缺少 [Hopper](https://www.hopperapp.com/) 时,Setup 会提议安装官方软件包;Hopper 是需要单独许可证的独立软件。
设置完成后,重启智能体并描述你想了解的应用或功能。REA 支持 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code 和 VS Code。已有 REA 注册默认选中;其他检测到的客户端需手动选择。其他智能体可以使用下方的 MCP 配置。
如果你已在 64 位 Linux 上安装 Ghidra 12.1.4 PUBLIC 和完整的 64 位 JDK 21,Setup 也可以在批准后登记 `GHIDRA_INSTALL_DIR` 和可选的 `JAVA_HOME`。REA 不会下载、安装或修改 Ghidra 或 Java。Ghidra provider 在隔离的只读 headless 会话中提供清单、反编译、汇编、调用关系、带类型的引用、xref、CFG 与函数 dossier;GUI 状态和修改操作仍不可用。
Hopper 支持演示模式。如果首次启动时出现提示,选择演示模式或输入已有许可证。
#### Linux 安装与故障排除
### 从终端运行
在 Ubuntu 24.04+、Fedora 41+ 和 64 位 Arch Linux 上,REA 会下载 Hopper 官方的 DEB、RPM 或 Arch 软件包,验证发布方提供的大小和校验和,然后通过 `apt-get`、`dnf` 或 `pacman` 安装依赖。非 root 运行时,`pkexec` 会显示系统授权提示;REA 不调用 `sudo`。
默认启动器是 `/opt/hopper/bin/Hopper`。自定义安装可设置 `HOPPER_LAUNCHER_PATH`。如果 Doctor 报告缺少分析引擎,请运行 `ldd /opt/hopper/bin/Hopper | grep 'not found'`,安装缺少的系统库后重新运行 `rea setup`。REA 会在私有 Xvfb 显示器上运行受支持的 Hopper 演示版,并为每个分析会话选择 Hopper 提供的演示模式;无需桌面 `DISPLAY` 或付费许可证,但演示版受厂商限制。Linux 上还应确保 `~/.local/bin` 位于 `PATH` 中。
```bash
# 1. 安装和配置 REA
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx --yes rea-agents@latest setup
```
如果 macOS 或安装程序要求确认,请完成提示,然后再次运行同一命令。
### 2. 重启编程智能体
Setup 会检测 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf 和 Devin。它会自动配置检测到的前六种客户端;由于目前没有明确的本地 MCP 配置边界,检测到 Devin 时只会报告而不会修改。重启已配置的客户端,让它加载 REA。
### 3. 直接询问智能体
你可以直接说出应用名称。编程智能体会找到应用,并把 REA 所需的程序文件交给它。
```text
使用 REA 逆向工程“备忘录”应用。解释搜索功能的工作方式,展示证据,
然后使用 SQLite 为我的项目构建一个类似功能。
```
如果遇到问题,请运行:
设置完成后:
```bash
npx -y rea-agents@latest doctor
rea uninstall
rea uninstall --purge-data
npx -y rea-agents@latest analyze /Applications/Notes.app
```
### 安装 rea 命令
安装命令行工具:
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
```
需要预先安装 Node.js 和 npm。在终端中运行时,安装程序会安装 `rea` 并启动设置。
也可以用 npm 安装,再运行设置:
```bash
npm install --global rea-agents
rea setup
```
### 运行要求
- macOS 12 或更高版本
- Ubuntu 24.04+、Fedora 41+ 或 64 位 Arch Linux
- Node.js 22.x (>=22.19)、24.x (>=24.11) 或 26+,以及 npm
原生二进制分析需要 Hopper 或 Ghidra。Hopper 是独立软件;演示模式有厂商规定的限制,不要求购买许可证。
Ghidra 支持 Linux x64 和 macOS x64/arm64。单独安装 Ghidra 12.1.4 和完整的 64 位 JDK 21,然后配置 REA 使用它们。macOS 还需要对应架构的原生反编译器。
Setup 可以验证安装并保存路径,不会安装或升级 Ghidra、Java、Node.js、npm 或 Homebrew。
Windows Ghidra 分析目前不可用。进程归属、私有目录权限和安全路径检查尚未实现,正确安装 Ghidra 和 Java 也不能启用分析。详见 [Windows Ghidra P0](docs/windows-ghidra-p0.md)。
### 故障排查
运行 `npx -y rea-agents@latest doctor`,检查主机、依赖、分析工具和智能体配置。该命令不会修改文件。添加 `--json` 可获取结构化诊断。
Linux 上的默认 Hopper 启动器为 `/opt/hopper/bin/Hopper`。其他路径可通过 `HOPPER_LAUNCHER_PATH` 指定。如果文件存在但仍报告缺少分析引擎,运行 `ldd /opt/hopper/bin/Hopper | grep 'not found'` 检查缺少的系统库。安装详情见 [Hopper 指南](docs/installation.md#hopper)。
### 更新与卸载
- `rea update` 更新当前 REA 安装。
- `rea uninstall` 移除 REA 管理的智能体配置项和工作流文件,保留 Hopper。
- `rea uninstall --purge-data` 还会删除 REA 的缓存和状态。仅在需要移除这些数据时使用。
## 当前状态
REA 可以通过 CLI 和 MCP 分析原生二进制、JavaScript/Electron 应用、.NET 程序集和网站。具体操作取决于主机、目标和所选分析工具。
- Hopper 支持原生分析和注释操作;GUI 行为取决于平台。
- Ghidra 在 Linux x64 和 macOS x64/arm64 上提供 22 项只读操作,包括清单、搜索、反编译、汇编、调用关系、引用、指令与类型检查。它不提供 GUI 或修改操作。
- 浏览器、Electron 和进程运行时工作流有独立的配置、批准和生命周期要求。详见 [English README](README.md#current-status) 中的完整状态说明。
- Windows Ghidra 分析尚未启用,跟踪进展见 [#527](https://github.com/morluto/rea/issues/527)。
## 一个提示词,完成一次完整调查
```text
@@ -149,22 +191,26 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
## 调查工具目录
| 工具类别 | 数量 | 示例 |
| ----------------- | ---: | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| 二进制检查 | 36 | 过程、伪代码、汇编、字符串、名称、段、调用者、被调用者、交叉引用、注释 |
| 组合分析 | 14 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`、Swift 与 ObjC 发现 |
| macOS 原生工具 | 5 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原,无需启动 Hopper |
| 制品图谱 | 3 | 目录及支持的软件包的完整内联检查,以及显式选择的事务式提取 |
| Managed PE/CLI | 8 | PE/CLI 身份、元数据成员、CIL 哈希、P/Invoke/原生边界声明与验证、应用图谱投影、反编译重建导入、结构化 token 重映射、运行时关联计划与跨版本比较 |
| 浏览器观察 | 9 | 精确来源 CDP 抓取、bundle/source map 分析、WebMCP 发现、会话时间线、capture diff 与视觉证据 |
| Electron 分析 | 5 | 限定 canonical 文件根的被动观察、有界静态应用映射,以及基于 Evidence 的静态/运行时对账 |
| JavaScript 运行时 | 2 | 经批准的只附加 Node/Electron Inspector 目标发现,以及不执行求值或插桩的有界脚本和执行上下文观察 |
| 应用工作流 | 10 | 有界跨层追踪、唯一匹配跨版本图比较、静态导出返回形状比较、经批准的 Linux 隔离 extracted-module replay、托管运行时表征、重建覆盖闭合和确定性义务账本 |
| 二进制会话 | 22 | `open_binary`、`binary_session`、证据包、有限重放状态机、进程、制品与函数比较、残余未知项注册表 |
| 工具类别 | 数量 | 用途 |
| ----------------- | ---: | --------------------------------------------------------- |
| 二进制检查 | 41 | 函数、伪代码、汇编、字符串、符号、段、引用与注释 |
| 组合分析 | 14 | 概览、函数分析、批量反编译、调用图、Swift 与 ObjC 检查 |
| macOS 原生工具 | 7 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原 |
| 制品检查 | 5 | 目录与软件包检查、Interface Builder、Apple 资源目录与提取 |
| .NET PE/CLI | 7 | 程序集身份、元数据、CIL、原生调用声明、成员比较与重建导入 |
| 浏览器观察 | 9 | 页面、脚本、来源映射、WebMCP 工具、截图与捕获比较 |
| Electron 分析 | 5 | 页面观察、应用结构与静态/运行时结果关联 |
| JavaScript 运行时 | 2 | 连接现有 Node/Electron Inspector,观察脚本与执行上下文 |
| 应用工作流 | 7 | 功能追踪、版本比较、返回结构比较与重建验证 |
| 二进制会话 | 21 | 目标切换、证据包、进程与函数比较、待解决问题记录 |
## 路线图
接下来的工作包括更多原生目标验证、JavaScript 与 .NET 跨版本比较,以及运行时观察和重建验证。当前设置已支持选择智能体集成和 Hopper 安装;安装其他分析工具仍是后续工作。详见[安装路线图](docs/roadmap.md)和[分析工具评估](docs/provider-evaluation.md)。
## 与其他编程智能体一起使用
Setup 会检测 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf 和 Devin,并自动配置检测到的前六种客户端;检测到 Devin 时只会报告而不会修改。任何支持本地 MCP 服务器的编程智能体都可以使用以下配置连接 REA。
Setup 支持 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code 和 VS Code。已有 REA 注册默认选中;其他检测到的客户端需手动选择。任何支持本地 MCP 服务器的智能体都可以使用以下配置连接 REA。
<!-- x-release-please-start-version -->
@@ -173,7 +219,7 @@ Setup 会检测 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Win
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@3.2.1", "mcp"]
"args": ["-y", "rea-agents@4.0.1", "mcp"]
}
}
}
@@ -193,7 +239,7 @@ flowchart LR
Ghidra --> App
```
CLI 与 MCP 服务器使用相同的分析引擎。终端命令完成后会关闭应用;智能体会话则会在调查期间保持应用打开。
CLI 与 MCP 服务器使用相同的分析流程。终端命令完成后释放自己创建的桥接会话;智能体会话可以在调查期间保持连接。关闭 REA 会话不会退出用户正在使用的 Hopper 应用。
## CLI
@@ -223,7 +269,7 @@ REA 会推导明确的格式和架构参数,以避免常见的 FAT 与 ARM 选
## 安全模型
每个桥会话都使用随机能力令牌和仅限当前用户的 Unix 套接字。协议消息有大小限制,面向调用者的错误不会暴露启动器 stderr 或内部异常原因。Ghidra 会话还使用隔离的临时项目,不会打开或修改用户自己的 Ghidra 项目。
每个桥会话都使用随机能力令牌和仅限当前用户的 Unix 套接字。诊断保留本机路径、摘要和错误位置等排查信息,同时移除凭据和认证令牌。Ghidra 会话还使用隔离的临时项目,不会打开或修改用户自己的 Ghidra 项目。
这不是沙箱,也无法防御以同一操作系统用户身份运行的恶意进程。打开不可信二进制文件会让所选本地提供商以当前用户权限进行解析和分析。请按照 [SECURITY.md](SECURITY.md) 中的私密流程报告漏洞。
@@ -231,7 +277,7 @@ REA 会推导明确的格式和架构参数,以避免常见的 FAT 与 ARM 选
<details><summary><strong>Hopper 是否需要提前运行?</strong></summary>
不需要。REA 会在操作需要时启动 Hopper,也支持已经运行的 Hopper。
不需要。REA 会在操作需要时启动 Hopper。Hopper 已经运行时也可以使用,但 REA 会打开新的分析文档,不会连接并复用现有 GUI 文档。
</details>
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import java.math.BigInteger;
/** Seed the declared DOS COM analysis context before default auto-analysis. */
public final class ReaGhidraPrepareCom extends GhidraScript {
@Override
public void run() throws Exception {
if (!currentProgram.getLanguageID().getIdAsString().equals("x86:LE:16:Real Mode") ||
!currentProgram.getExecutableFormat().equals("Raw Binary")) {
throw new IllegalArgumentException("DOS COM preparation requires raw x86 real-mode import");
}
Address entry = currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress("1000:0100");
Address start = currentProgram.getMinAddress();
Address end = currentProgram.getMaxAddress();
if (!entry.equals(start) || end.subtract(start) + 1 > 0xff00) {
throw new IllegalArgumentException("DOS COM import must map 1..65280 bytes at 1000:0100");
}
for (String name : new String[] { "CS", "DS", "ES", "SS" }) {
monitor.checkCancelled();
currentProgram.getProgramContext().setValue(
currentProgram.getRegister(name), start, end, BigInteger.valueOf(0x1000)
);
}
addEntryPoint(entry);
if (!disassemble(entry) || createFunction(entry, "entry") == null) {
throw new IllegalStateException("DOS COM entry cannot be decoded as a function");
}
currentProgram.getOptions(Program.PROGRAM_INFO).setBoolean("REA DOS COM prepared", true);
}
}
+70 -27
View File
@@ -128,6 +128,10 @@ class CapabilityUnavailableError(Exception):
"""The active Hopper build lacks one admitted public API operation."""
class InvalidRequestError(Exception):
"""Caller input failed bridge request or operation validation."""
EXHAUSTIVE_ANALYSIS_METHODS = frozenset(
(
"analyze_function",
@@ -150,6 +154,11 @@ EXHAUSTIVE_ANALYSIS_METHODS = frozenset(
def _hex(value):
if not isinstance(value, int) or isinstance(value, bool):
try:
value = int(str(value), 16)
except ValueError as error:
raise ValueError("Hopper returned a non-hexadecimal address") from error
return "0x%x" % value
@@ -177,8 +186,8 @@ def _document(name=None):
current = api.current_document()
if current is not None and current.getDocumentName() == name:
return current
raise ValueError("Hopper document name is ambiguous")
raise ValueError("Unknown Hopper document")
raise InvalidRequestError("Hopper document name is ambiguous")
raise InvalidRequestError("Unknown Hopper document")
if _selected_document is not None:
for candidate in documents:
if candidate.getDocumentName() == _selected_document:
@@ -188,7 +197,7 @@ def _document(name=None):
return session_document
current = api.current_document()
if current is None:
raise ValueError("No Hopper document is loaded")
raise CapabilityUnavailableError("No Hopper document is loaded")
return current
@@ -197,20 +206,20 @@ def _address(document, value=None):
if value is None:
return document.getCurrentAddress()
if not isinstance(value, str):
raise ValueError("Address must be a string")
raise InvalidRequestError("Address must be a string")
try:
return int(value, 16)
except ValueError:
result = document.getAddressForName(value)
if result in BAD_ADDRESSES:
raise ValueError("Unknown Hopper address or name")
raise InvalidRequestError("Unknown Hopper address or name")
return result
def _segment(document, address):
result = document.getSegmentAtAddress(address)
if result is None:
raise ValueError("Address is outside every segment")
raise InvalidRequestError("Address is outside every segment")
return result
@@ -218,7 +227,7 @@ def _procedure(document, value=None):
address = document.getCurrentAddress() if value is None else _address(document, value)
result = _segment(document, address).getProcedureAtAddress(address)
if result is None:
raise ValueError("No procedure exists at the requested address")
raise InvalidRequestError("No procedure exists at the requested address")
return result
@@ -232,6 +241,10 @@ def _procedure_identity(procedure):
"address": _hex(procedure.getEntryPoint()),
"name": _procedure_name(procedure),
"classification": None,
"body": {
"available": False,
"reason": "Hopper's public Python API does not expose complete function body ranges",
},
}
@@ -278,7 +291,7 @@ def _procedure_references(document, params):
procedure = _procedure(document, params.get("procedure"))
direction = params.get("direction", "outgoing")
if direction not in ("incoming", "outgoing"):
raise ValueError("direction must be incoming or outgoing")
raise InvalidRequestError("direction must be incoming or outgoing")
addresses = _instruction_addresses(procedure)
edges = set()
for address in addresses:
@@ -300,6 +313,12 @@ def _procedure_references(document, params):
})
return {
"procedure": _procedure_identity(procedure), "direction": direction,
"reference_kinds_available": False,
# The public Hopper API returns observed references but does not expose
# enough flow metadata to enumerate calls with no resolved target.
# Keep the required list field explicit; the provider records this as
# unknown coverage rather than claiming the list is exhaustive.
"unresolved_calls": [],
"references": items,
}
@@ -366,13 +385,13 @@ def _search_inventory(document, kind):
def _search_results(document, kind, params):
pattern = params.get("pattern")
if not isinstance(pattern, str) or not pattern:
raise ValueError("pattern must be a non-empty string")
raise InvalidRequestError("pattern must be a non-empty string")
mode = params.get("mode", "literal")
if mode not in ("literal", "regex"):
raise ValueError("mode must be literal or regex")
raise InvalidRequestError("mode must be literal or regex")
case_sensitive = params.get("case_sensitive", False)
if not isinstance(case_sensitive, bool):
raise ValueError("case_sensitive must be a boolean")
raise InvalidRequestError("case_sensitive must be a boolean")
if mode == "literal":
needle = pattern if case_sensitive else pattern.casefold()
@@ -381,7 +400,7 @@ def _search_results(document, kind, params):
try:
expression = re.compile(pattern, 0 if case_sensitive else re.IGNORECASE)
except (re.error, OverflowError) as error:
raise ValueError("Invalid regex pattern") from error
raise InvalidRequestError("Invalid regex pattern") from error
matches = lambda value: expression.search(value) is not None
selected = []
@@ -544,7 +563,11 @@ def _analyze_function(document, params):
referenced_strings.sort(key=lambda item: (int(item["address"], 16), int(item["source_address"], 16)))
referenced_names.sort(key=lambda item: (int(item["address"], 16), int(item["source_address"], 16)))
return {
"procedure": {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure), "classification": None, "signature": procedure.signatureString(), "locals": _procedure_locals(procedure)},
"procedure": {
**_procedure_identity(procedure),
"signature": procedure.signatureString(),
"locals": _procedure_locals(procedure),
},
"pseudocode": pseudo,
"assembly": assembly_lines,
"comments": comments,
@@ -618,9 +641,9 @@ def _dispatch(method, params):
if method == "read_bytes":
length = params.get("length", 256)
if isinstance(length, bool) or not isinstance(length, int):
raise ValueError("Byte-read length must be an integer")
raise InvalidRequestError("Byte-read length must be an integer")
if length < 1:
raise ValueError("Byte-read length must be at least 1")
raise InvalidRequestError("Byte-read length must be at least 1")
reader = getattr(document, "readBytes", None)
if not callable(reader):
raise CapabilityUnavailableError(
@@ -654,7 +677,7 @@ def _dispatch(method, params):
or offset in BAD_ADDRESSES
or offset < 0
):
raise ValueError("Address has no authoritative file-offset mapping")
raise InvalidRequestError("Address has no authoritative file-offset mapping")
return {"address": _hex(address), "file_offset": offset}
if method == "resolve_containing_procedure":
address = _address(document, params.get("address"))
@@ -690,7 +713,7 @@ def _dispatch(method, params):
else:
result = document.getInstructionStart(max(0, target - 1))
if result in BAD_ADDRESSES:
raise ValueError("No adjacent address")
raise InvalidRequestError("No adjacent address")
return _hex(result)
if method == "list_segments":
result = []
@@ -729,14 +752,14 @@ def _dispatch(method, params):
if requested is not None:
key = _hex(_address(document, requested))
values = {key: values[key]} if key in values else {}
return [{"address": _hex(address), "value": value} for address, value in sorted(values.items())]
return [{"address": address, "value": value} for address, value in values.items()]
if method == "list_names":
result = dict(_search_inventory(document, "name"))
requested = params.get("address")
if requested is not None:
key = _hex(_address(document, requested))
result = {key: result[key]} if key in result else {}
return [{"address": _hex(address), "value": value} for address, value in sorted(result.items(), key=lambda item: int(item[0]))]
return [{"address": address, "value": value} for address, value in result.items()]
if method in ("search_procedures", "search_strings"):
kind = "procedure" if method == "search_procedures" else "string"
return _search_results(document, kind, params)
@@ -755,7 +778,16 @@ def _dispatch(method, params):
if method == "procedure_info":
blocks = list(procedure.basicBlockIterator())
length = sum(max(0, block.getEndingAddress() - block.getStartingAddress()) for block in blocks)
return {"name": _procedure_name(procedure), "entrypoint": _hex(procedure.getEntryPoint()), "basicblock_count": procedure.getBasicBlockCount(), "length": length, "signature": procedure.signatureString(), "locals": _procedure_locals(procedure), "classification": None}
return {
"name": _procedure_name(procedure),
"entrypoint": _hex(procedure.getEntryPoint()),
"basicblock_count": procedure.getBasicBlockCount(),
"length": length,
"signature": procedure.signatureString(),
"locals": _procedure_locals(procedure),
"classification": None,
"body": _procedure_identity(procedure)["body"],
}
if method == "set_address_name":
address = _address(document, params.get("address"))
try:
@@ -789,7 +821,7 @@ def _dispatch(method, params):
address = _address(document, params.get("address"))
document.removeBookmarkAtAddress(address)
return not document.hasBookmarkAtAddress(address)
raise ValueError("Unknown bridge method")
raise InvalidRequestError("Unknown bridge method")
def _serve_connection(connection):
@@ -803,13 +835,24 @@ def _serve_connection(connection):
authenticated = False
should_stop = False
try:
request = json.loads(line.decode("utf-8"))
if set(request) != {"id", "token", "method", "params"}:
raise ValueError("Invalid bridge request shape")
try:
request = json.loads(line.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as error:
raise InvalidRequestError("Invalid bridge request JSON") from error
if not isinstance(request, dict) or set(request) != {"id", "token", "method", "params"}:
raise InvalidRequestError("Invalid bridge request shape")
request_id = request["id"]
if isinstance(request_id, bool) or not isinstance(request_id, int) or request_id < 0:
raise ValueError("Invalid bridge request id")
if not isinstance(request["token"], str) or not hmac.compare_digest(request["token"], REA_TOKEN):
raise InvalidRequestError("Invalid bridge request id")
if (
not isinstance(request["method"], str) or not request["method"]
or not isinstance(request["params"], dict)
):
raise InvalidRequestError("Invalid bridge method or parameters")
if (
not isinstance(request["token"], str) or not request["token"].isascii()
or not hmac.compare_digest(request["token"], REA_TOKEN)
):
raise PermissionError("Invalid bridge capability")
authenticated = True
_write_message(file, {"id": request_id, "event": {
@@ -864,7 +907,7 @@ def _diagnostic_type(error):
return "capability_unavailable"
if isinstance(error, PermissionError):
return "authorization"
if isinstance(error, (ValueError, TypeError, KeyError)):
if isinstance(error, InvalidRequestError):
return "invalid_request"
return "bridge_exception"
+157
View File
@@ -0,0 +1,157 @@
import AppKit
import ApplicationServices
import CryptoKit
import ScreenCaptureKit
import Darwin
struct Request: Decodable {
var pid: Int32
var window_id: UInt32
var executable: String
var sha256: String
var launch_time: Double?
var screenshot: Bool
var accessibility: Bool
var max_nodes: Int
var action: Action?
}
struct Action: Decodable { var kind: String; var path: [Int]?; var direction: String?; var text: String? }
struct BoundaryFailure: Error { var code: String; var message: String }
func fail(_ code: String, _ message: String) throws -> Never { throw BoundaryFailure(code: code, message: message) }
func attribute(_ element: AXUIElement, _ key: String) -> CFTypeRef? {
var value: CFTypeRef?
return AXUIElementCopyAttributeValue(element, key as CFString, &value) == .success ? value : nil
}
func childCount(_ element: AXUIElement) -> Int {
var count = 0
return AXUIElementGetAttributeValueCount(element, kAXChildrenAttribute as CFString, &count) == .success ? count : 0
}
func children(_ element: AXUIElement, limit: Int) -> [AXUIElement] {
var value: CFArray?
let count = min(limit, childCount(element))
if count == 0 { return [] }
return AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, 0, count, &value) == .success ? value as? [AXUIElement] ?? [] : []
}
func text(_ element: AXUIElement, _ key: String) -> Any { (attribute(element, key) as? String) ?? NSNull() as Any }
func windowBounds(_ element: AXUIElement) -> CGRect? {
guard let p = attribute(element, kAXPositionAttribute), let s = attribute(element, kAXSizeAttribute),
CFGetTypeID(p) == AXValueGetTypeID(), CFGetTypeID(s) == AXValueGetTypeID() else { return nil }
var point = CGPoint.zero; var size = CGSize.zero
guard AXValueGetValue(p as! AXValue, .cgPoint, &point), AXValueGetValue(s as! AXValue, .cgSize, &size) else { return nil }
return CGRect(origin: point, size: size)
}
func observe(_ request: Request) async throws -> [String: Any] {
guard let app = NSRunningApplication(processIdentifier: request.pid), let url = app.executableURL else {
try fail("target-mismatch", "Selected PID does not expose an application executable URL")
}
guard let canonicalPointer = realpath(url.path, nil) else { try fail("target-mismatch", "Running executable path cannot be resolved") }
defer { free(canonicalPointer) }
let canonicalExecutable = String(cString: canonicalPointer)
guard canonicalExecutable == request.executable else {
try fail("target-mismatch", "Selected PID executable \(canonicalExecutable) differs from active target \(request.executable)")
}
guard let launch = app.launchDate?.timeIntervalSince1970 else {
try fail("process-identity-unavailable", "Selected application does not expose a launch time; stable PID reuse checks are unavailable")
}
if let expected = request.launch_time, expected != launch { try fail("process-replaced", "Selected process was replaced") }
let executableSize = (try FileManager.default.attributesOfItem(atPath: url.path)[.size] as? NSNumber)?.int64Value ?? -1
guard executableSize >= 0 && executableSize <= 256 * 1024 * 1024 else { try fail("target-limit", "Running executable exceeds the 256 MiB identity-check budget") }
let executableBytes = try Data(contentsOf: url, options: .mappedIfSafe)
guard SHA256.hash(data: executableBytes).map({ String(format: "%02x", $0) }).joined() == request.sha256 else {
try fail("target-mismatch", "Running executable bytes differ from the active target digest")
}
guard let windows = CGWindowListCopyWindowInfo([.optionIncludingWindow], request.window_id) as? [[String: Any]],
windows.count == 1, let window = windows.first,
(window[kCGWindowOwnerPID as String] as? NSNumber)?.int32Value == request.pid,
(window[kCGWindowLayer as String] as? NSNumber)?.intValue == 0,
let boundsDictionary = window[kCGWindowBounds as String] as? [String: Any],
let bounds = CGRect(dictionaryRepresentation: boundsDictionary as CFDictionary) else {
try fail("window-mismatch", "Selected window is absent or belongs to another process")
}
if request.screenshot && !CGPreflightScreenCaptureAccess() {
try fail("screen-recording-denied", "Grant Screen Recording to the REA host in System Settings > Privacy & Security, then retry; no broad capture fallback is used")
}
var selected: AXUIElement?
if request.accessibility || request.action != nil {
guard AXIsProcessTrusted() else { try fail("accessibility-denied", "Grant Accessibility to the REA host in System Settings > Privacy & Security, then retry") }
let application = AXUIElementCreateApplication(request.pid)
AXUIElementSetMessagingTimeout(application, 2)
guard let axWindows = attribute(application, kAXWindowsAttribute) as? [AXUIElement] else {
try fail("accessibility-unavailable", "Selected application does not expose accessibility windows")
}
let matches = axWindows.filter { windowBounds($0) == bounds }
guard matches.count == 1 else { try fail("ambiguous-window", "Accessibility window geometry does not identify exactly one selected window; selected CG bounds \(bounds), AX candidates \(axWindows.map { String(describing: windowBounds($0)) })") }
selected = matches[0]
}
if let action = request.action, let root = selected {
var element = root
for index in action.path ?? [] {
var selectedChild: CFArray?
guard index >= 0 && index < childCount(element),
AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, index, 1, &selectedChild) == .success,
let item = (selectedChild as? [AXUIElement])?.first else { try fail("element-missing", "Selected accessibility path no longer exists") }
element = item
}
var owner: pid_t = 0
guard AXUIElementGetPid(element, &owner) == .success && owner == request.pid else { try fail("element-owner-mismatch", "Selected element belongs to another process") }
let outcome: AXError
switch action.kind {
case "click": outcome = AXUIElementPerformAction(element, kAXPressAction as CFString)
case "scroll": outcome = AXUIElementPerformAction(element, (action.direction == "increment" ? kAXIncrementAction : kAXDecrementAction) as CFString)
case "key-entry": outcome = AXUIElementSetAttributeValue(element, kAXValueAttribute as CFString, (action.text ?? "") as CFString)
default: try fail("unsupported-action", "Only selected-element press, increment/decrement and text-value entry are admitted")
}
guard outcome == .success else { try fail("action-failed", "Accessibility action failed with AXError \(outcome.rawValue)") }
}
var nodes: [[String: Any]] = []; var truncated = false
if request.accessibility, let root = selected {
var pending: [(AXUIElement, [Int])] = [(root, [])]
while let (element, path) = pending.popLast() {
if nodes.count >= request.max_nodes { truncated = true; break }
let totalChildren = childCount(element)
var actions: CFArray?
AXUIElementCopyActionNames(element, &actions)
nodes.append(["path": path, "role": text(element, kAXRoleAttribute), "title": text(element, kAXTitleAttribute), "value": text(element, kAXValueAttribute), "actions": actions as? [String] ?? [], "children_count": totalChildren])
if path.count >= 32 { if totalChildren > 0 { truncated = true }; continue }
let available = max(0, request.max_nodes - nodes.count - pending.count)
let count = min(available, totalChildren)
let items = children(element, limit: count)
if totalChildren > count { truncated = true }
for index in (0..<count).reversed() { pending.append((items[index], path + [index])) }
}
}
var screenshot: Any = NSNull()
if request.screenshot {
guard #available(macOS 14, *) else { try fail("unsupported-host", "Selected-window screenshots require macOS 14 or later") }
let content = try await SCShareableContent.excludingDesktopWindows(true, onScreenWindowsOnly: false)
guard let scWindow = content.windows.first(where: { $0.windowID == request.window_id && $0.owningApplication?.processID == request.pid }) else {
try fail("capture-window-missing", "Selected window disappeared before capture")
}
guard bounds.width > 0 && bounds.height > 0 && bounds.width <= 100000 && bounds.height <= 100000 else { try fail("window-bounds", "Selected window has invalid or oversized geometry") }
let filter = SCContentFilter(desktopIndependentWindow: scWindow)
let configuration = SCStreamConfiguration()
let scale = min(1, 2048 / max(bounds.width, bounds.height))
configuration.width = max(1, Int(bounds.width * scale)); configuration.height = max(1, Int(bounds.height * scale))
configuration.showsCursor = false
let image = try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: configuration)
guard let png = NSBitmapImageRep(cgImage: image).representation(using: .png, properties: [:]) else {
try fail("capture-failed", "Selected-window PNG encoding failed")
}
screenshot = ["mime_type": "image/png", "base64": png.base64EncodedString(), "sha256": SHA256.hash(data: png).map({ String(format: "%02x", $0) }).joined(), "width": image.width, "height": image.height]
}
return ["window": ["pid": request.pid, "window_id": request.window_id, "executable": request.executable, "launch_time": launch, "title": window[kCGWindowName as String] as? String ?? ""], "nodes": nodes, "truncated": truncated, "screenshot": screenshot, "gaps": request.accessibility ? [] : ["Accessibility capture was disabled"]]
}
Task { @MainActor in
do {
let request = try JSONDecoder().decode(Request.self, from: Data(CommandLine.arguments[1].utf8))
let result = try await observe(request)
let output = try JSONSerialization.data(withJSONObject: ["ok": true, "result": result], options: [.sortedKeys])
FileHandle.standardOutput.write(output)
} catch {
let failure = error as? BoundaryFailure
let output: [String: Any] = ["ok": false, "code": failure?.code ?? "capture-failed", "message": failure?.message ?? error.localizedDescription]
if let data = try? JSONSerialization.data(withJSONObject: output, options: [.sortedKeys]) { FileHandle.standardOutput.write(data) }
}
exit(0)
}
RunLoop.main.run()
@@ -6,7 +6,7 @@
target binding, analysis-profile commitment, and snapshot/Evidence migration
are implemented. Ghidra discovery, target/profile resolution, doctor checks,
the private headless-session foundation, ten read-only inventory capabilities,
and nine function-analysis capabilities are implemented with real
and twelve function-analysis capabilities are implemented with real
cross-format conformance.
## Context
@@ -569,5 +569,7 @@ The first six implementation stages are shipped:
Future stages may deepen format and indirect-flow coverage, but must continue
to compare normalized semantics rather than provider-specific pseudocode or
assembly text. Ghidra continues to omit GUI and mutation operations, so
unsupported requests cannot route to an unverified implementation.
assembly text. Linux and macOS now admit atomic function-name and entry-comment
edits in the ephemeral database, with readback, rollback, and cache invalidation.
Executable bytes remain unchanged. Windows P0 retains its read-only boundary;
GUI and other unimplemented mutations remain unavailable.
@@ -1,10 +1,14 @@
# ADR-0002: Controlled JavaScript replay authority and sandbox policy
- Status: Accepted
- Status: Superseded; the controlled JavaScript replay tool was removed
- Date: 2026-07-16
- Implementation status: Implemented by `run_controlled_replay` with a
Linux-x86_64 Bubblewrap, seccomp, and delegated systemd user-cgroup backend.
Hosts that cannot establish every required boundary fail closed.
- Implementation status: The implementation was removed. The design below is
retained as historical context and does not describe a supported REA tool.
> Historical design note: The tool, sandbox backend, and plan/execute workflow
> described here are no longer part of REA. For runtime behavior, run probes
> directly against the application and use the available browser, Electron, or
> process capture workflows.
## Context
@@ -1,13 +1,17 @@
# ADR-0003: Managed-code evidence and provider boundary
- Status: Accepted
> Historical design record: The non-executing runtime-correlation planning
> tool described in earlier revisions was removed. This ADR's current
> implementation status below covers only the static managed tools that ship.
- Status: Superseded for runtime planning; static evidence boundary remains in force
- Date: 2026-07-16
- Implementation status: Read-only PE/CLI triage and exact identity are shipped
through `inspect_managed_artifact` / `rea inspect-managed-artifact`. Bounded
metadata, signatures, method bodies, raw CIL hashes, limited
decoded-instruction-tuple v1 hashes, exception regions, call edges, and
field-access anchors are shipped through `inspect_managed_members`; `rea
inspect-managed-members` provides CLI parity. Static member comparison and build-local
inspect-managed-members` provides CLI parity. Static member comparison and build-local
token remapping are shipped through `compare_managed_members` /
`rea compare-managed-members`. Declaration-only managed/native boundary
inventory for ModuleRef, ImplMap/PInvoke, ReadyToRun indicators, and non-IL
@@ -19,11 +23,9 @@ inspect-managed-members` provides CLI parity. Static member comparison and build
addresses. Decompiler reconstruction import is
shipped through `import_managed_reconstruction` /
`rea import-managed-reconstruction`; it records user-supplied C#/IL/pseudocode
as analyst inference only after exact static member locks match. Default-disabled
runtime-correlation admission planning is shipped through
`plan_managed_runtime_correlation` / `rea plan-managed-runtime-correlation`;
it does not attach, load, debug, reflect, instrument, invoke, or execute
target code. Managed static graph projection is shipped through
as analyst inference only after exact static member locks match. Managed
inspection does not execute assemblies or observe CLR internals. Managed static graph
projection is shipped through
`project_managed_application_graph` /
`rea project-managed-application-graph`; it projects authenticated managed
Evidence into the application graph without changing managed tokens into
@@ -50,7 +52,7 @@ makes names especially weak while tokens remain coordinates in only one build.
REA already separates one selected deep native provider from disjoint auxiliary
providers under [ADR-0001](0001-provider-selection-and-analysis-profiles.md).
It also separates static evidence, passive observation, process capture, and
controlled replay authorities. Managed analysis must fit those boundaries
interactive observation authorities. Managed analysis must fit those boundaries
without requiring a CLR to inspect an artifact, loading target code into the
REA process, or treating a managed token as a native address.
@@ -257,10 +259,13 @@ exception/local commitments, and an explicit algorithm version require a new
normalized-CIL schema.
Metadata tokens are reported because they are precise coordinates inside one
module. Cross-version identity never consists of a token alone. A structural
match cites both build commitments, both local tokens, the v1 tuple inputs, and
all competing candidates. V1 cannot cite a
separate matching-algorithm version because none is exposed.
module. Cross-version identity never consists of a token alone. Matching tries
exact CIL/signature identity, then an exact declared-type/name/signature tuple,
then structural shape; a name by itself never selects a pair. Partial or
malformed bodies leave body facets unknown after a signature tuple pairs the
methods. Matches cite both build commitments, both local tokens, and all
competing candidates. V1 cannot cite a separate matching-algorithm version
because none is exposed.
### 5. Separate five evidence layers
@@ -356,24 +361,13 @@ emit full IL, decompiled source, runtime logs, user data, credentials, or
service/account material. No proprietary binary, hash-locked application
manifest, or derived dump is committed to REA.
### 8. Keep runtime correlation a different, default-disabled authority
### 8. Keep runtime behavior separate from static managed analysis
Static managed support grants no permission to attach, load, debug, reflect,
instrument, invoke, or execute. The shipped runtime-correlation planning
capability distinguishes those effects, remains disabled by default, requires
explicit administrator and per-call approval, and binds to the exact artifact
SHA-256, MVID, method signature, and CIL/body shape observed by the static
path. It records an admission plan only; a future executor must pass a separate
threat model before any effect can occur.
Runtime admission must additionally validate host OS, CLR family, architecture,
and supported build/tool versions; bound time, threads, outputs, UI and network
effects; avoid real services and accounts; and own all created processes and
artifacts through complete cleanup. Reflection-only loading is still a CLR load
and is not a substitute for the static byte parser.
Runtime execution is intentionally outside the current managed-code track and
requires its own threat model and admission decision.
No managed runtime planning or execution tool ships. Managed inspection does
not execute assemblies or establish which methods ran. Where supported, direct
process capture can record target-level inputs and outputs, but it does not
prove CLR method execution or resolution. Internal managed behavior remains
unknown until independently observed with suitable runtime evidence.
## Public contract consequences
@@ -440,9 +434,7 @@ or development-only oracles.
5. Add source-built, pinned real-tool, package, CLI, and MCP conformance.
Source-owned conformance is shipped; pinned external real-tool checks remain
planned.
6. Add separately authorized runtime-correlation admission planning. Shipped;
runtime execution remains planned.
7. Project authenticated managed static Evidence into the application graph.
6. Project authenticated managed static Evidence into the application graph.
Shipped.
Every gate must keep unsupported ReadyToRun, NativeAOT, IL2CPP, mixed-mode, or
+5 -5
View File
@@ -4,8 +4,8 @@ Accepted decisions describe the architecture that implementation PRs must
follow. Acceptance does not by itself mean the behavior is shipped; each record
states its implementation status separately.
| ADR | Status | Implementation |
| ------------------------------------------------------------------------------------------------------------------ | -------- | ----------------------------------------------- |
| [0001: Provider selection and analysis profiles](0001-provider-selection-and-analysis-profiles.md) | Accepted | Foundation shipped; capability work continues |
| [0002: Controlled JavaScript replay authority and sandbox policy](0002-controlled-replay-authority-and-sandbox.md) | Accepted | Linux x86_64 extracted-module replay shipped |
| [0003: Managed-code evidence and provider boundary](0003-managed-code-evidence-and-provider-boundary.md) | Accepted | PE/CLI triage shipped; deeper contracts planned |
| ADR | Status | Implementation |
| ------------------------------------------------------------------------------------------------------------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| [0001: Provider selection and analysis profiles](0001-provider-selection-and-analysis-profiles.md) | Accepted | Provider selection and 22 Ghidra operations shipped on Linux/macOS; experimental Windows x64 P0 with bundled native controls |
| [0002: Controlled JavaScript replay authority and sandbox policy](0002-controlled-replay-authority-and-sandbox.md) | Superseded | Historical design; the controlled JavaScript replay tool was removed |
| [0003: Managed-code evidence and provider boundary](0003-managed-code-evidence-and-provider-boundary.md) | Partially superseded | Seven static managed tools remain; runtime planning removed |
+20 -5
View File
@@ -24,15 +24,19 @@ graph TD
GHIDRA["src/ghidra/<br/>Ghidra discovery, profile & function analysis<br/>snapshot, Unix/Windows transport, bounded lifecycle"]
AUX["src/artifacts/ + src/native/<br/>disjoint artifact and native capabilities"]
BROWSER["src/browser/<br/>Loopback CDP transport<br/>exact-origin passive observation"]
REPLAY["src/replay/<br/>Disposable JavaScript worker<br/>Bubblewrap, seccomp, cgroup limits"]
DOTNET["src/dotnet/<br/>Managed static metadata provider<br/>PE/CLI metadata inventory, members, IL"]
REFERENCE["src/reference/<br/>Filesystem reference-source reader<br/>bounded traversal, validation, errors"]
end
subgraph "Provider Process Foundation"
PROCESS["src/process/<br/>owned process groups, private roots<br/>deadlines, bounded file reads and diagnostics, cleanup"]
WINDOWS["src/windows/<br/>Windows host boundary adapters<br/>package loader, opaque handles, owned jobs and private runtimes"]
WINDOWS_NATIVE["native/windows/src/<br/>Node-API implementation<br/>NTFS admission, protected DACLs and atomic Job Object assignment"]
end
subgraph "Contracts Layer"
CONTRACTS["src/contracts/<br/>Caller-visible schemas for analysis, managed, browser, Electron, application, and session tools"]
ERRORS["src/contracts/errorSchemas.ts<br/>Shared analysis error schemas<br/>CLI diagnostics, generated error catalog"]
end
subgraph "Domain Layer"
@@ -40,19 +44,24 @@ graph TD
end
subgraph "Configuration"
CONFIG["src/config.ts<br/>Zod-validated env parsing<br/>provider settings, origins, and approved roots"]
CONFIG["src/config.ts<br/>Zod-validated env parsing<br/>provider settings and origins"]
end
subgraph "Tests"
TESTS["tests/<br/>Vitest suite<br/>fixtures/ holds fake launcher, bridge, and CDP seams"]
end
subgraph "Evaluation"
EVALUATION["src/evaluation/<br/>Agent release evaluation<br/>session transcript scoring"]
end
DISPATCH --> CLI
DISPATCH --> MAIN
MAIN --> CONFIG
MAIN --> SERVER
MAIN --> APP
CLI --> APP
CLI --> ERRORS
APP --> SESSION
SESSION --> REGISTRY
REGISTRY --> HOPPER
@@ -63,25 +72,31 @@ graph TD
SERVER --> CONTRACTS
SERVER --> APP
APP --> BROWSER
APP --> REPLAY
APP --> DOTNET
DOTNET --> DOMAIN
HOPPER --> HOPPER_BRIDGE
GHIDRA --> GHIDRA_BRIDGE
HOPPER --> PROCESS
GHIDRA --> PROCESS
GHIDRA --> WINDOWS
PROCESS --> WINDOWS
WINDOWS --> WINDOWS_NATIVE
APP --> PROCESS
APP --> DOMAIN
JS_RECON --> AUX
JS_RECON --> DOMAIN
APP --> REFERENCE
REFERENCE --> DOMAIN
CONTRACTS --> DOMAIN
HOPPER --> DOMAIN
GHIDRA --> DOMAIN
AUX --> DOMAIN
BROWSER --> DOMAIN
REPLAY --> DOMAIN
PROCESS --> DOMAIN
TESTS -.-> MAIN
TESTS -.-> HOPPER
TESTS -.-> GHIDRA
TESTS -.-> PROCESS
TESTS -.-> BROWSER
TESTS -.-> REPLAY
TESTS -.-> EVALUATION
TESTS -.-> ERRORS
+4
View File
@@ -0,0 +1,4 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Discord</title>
<path fill="#5865F2" d="M20.317 4.3698a19.7913 19.7913 0 0 0-4.8851-1.5152.0741.0741 0 0 0-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 0 0-.0785-.037 19.7363 19.7363 0 0 0-4.8852 1.515.0699.0699 0 0 0-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 0 0 .0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 0 0 .0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 0 0-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 0 1-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 0 1 .0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 0 1 .0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 0 1-.0066.1276 12.2986 12.2986 0 0 1-1.873.8914.0766.0766 0 0 0-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 0 0 .0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 0 0 .0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 0 0-.0312-.0286ZM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189Zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+28 -44
View File
@@ -4,20 +4,20 @@ REA can attach to a user-owned Chrome-family browser through the Chrome DevTools
## Shipped surfaces
- `list_browser_targets` / `rea list-browser-targets` discovers page targets whose current URL matches an approved exact origin.
- `inspect_web_page` / `rea inspect-web-page` captures DOM structure, accessibility nodes, scripts, resources, safe response/DOM metadata, attach-window network and console metadata, WebSocket frame sizes, workers, quota, and optionally storage key names, redacted storage-content fingerprints, or script sources.
- `analyze_web_bundle` / `rea analyze-web-bundle` parses approved script artifacts without execution and derives chunk edges, route and endpoint candidates, vendor fingerprints, static WebMCP declarations, and optionally approved source-map/original-source evidence.
- `list_browser_targets` / `rea list-browser-targets` discovers page targets at the selected target's current origin by default, with an optional exact-origin filter.
- `inspect_web_page` / `rea inspect-web-page` captures DOM structure, accessibility nodes, scripts, resources, safe response/DOM metadata, attach-window network and console metadata, WebSocket frame sizes, workers, quota, and optionally selected storage key names, redacted storage-content fingerprints, or script sources.
- `analyze_web_bundle` / `rea analyze-web-bundle` parses selected script artifacts without execution and derives chunk edges, route and endpoint candidates, vendor fingerprints, static WebMCP declarations, and optionally source-map/original-source evidence.
- `observe_web_session` / `rea observe-web-session` arms an observation window of the requested duration for an external user action and records ordered reload, SPA navigation, redirect, failure, lifecycle, and target-termination metadata.
- `discover_webmcp_tools` / `rea discover-webmcp-tools` uses the experimental CDP WebMCP domain to return every in-scope registration and its complete structural input-schema summary inline. REA never exposes `WebMCP.invokeTool`; malformed protocol payloads fail explicitly instead of returning incomplete tool metadata.
- `compare_web_captures` / `rea compare-web-captures` compares passive captures or exact-step-aligned browser scenarios. Scenario results commit reproducible literal normalization and expose alignment failures plus artifact-level action, screenshot, DOM, accessibility, URL, history, storage, and event differences. Missing or truncated evidence is never treated as equivalence.
- `capture_web_screenshot` / `rea capture-web-screenshot` returns an explicitly approved, content-addressed visible-viewport PNG.
- `capture_web_screenshot` / `rea capture-web-screenshot` returns a content-addressed visible-viewport PNG.
- `compare_web_screenshots` / `rea compare-web-screenshots` performs bounded local PNG pixel comparison without OCR or external services.
- Every surface has equivalent CLI and MCP contracts and returns Evidence provenance.
- MCP tools return complete results inline and include the Evidence ID. Session evidence can be exported with `export_evidence_bundle`.
Target discovery returns the complete approved target array in one result.
Target discovery returns the complete in-scope target array in one result.
Electron `file://` pages use a separate provider, permission capability, and root model; see [electron-observation.md](electron-observation.md).
Electron `file://` pages use a separate provider and target boundary; see [electron-observation.md](electron-observation.md).
Existing static application Evidence and passive web/Electron captures can be
combined later through
[JavaScript static/runtime reconciliation](javascript-runtime-reconciliation.md).
@@ -35,35 +35,20 @@ google-chrome \
REA does not launch, own, or terminate this browser. Use a dedicated profile and stop it yourself when the investigation is complete. Do not expose the debugging port on a non-loopback interface.
## Configure authority
## Request boundary
Browser observation is fail-closed by default. Configure the administrator ceiling before starting REA:
```bash
export REA_BROWSER_OBSERVE_ENABLED=true
export REA_BROWSER_CDP_ENDPOINTS_JSON='["http://127.0.0.1:9222"]'
export REA_BROWSER_ALLOWED_ORIGINS_JSON='["http://127.0.0.1:3000"]'
```
`REA_BROWSER_CDP_ENDPOINTS_JSON` accepts at most 16 literal `http://127.0.0.1:PORT` or `http://[::1]:PORT` endpoints. `localhost`, private-LAN addresses, HTTPS endpoints, credentials, paths, queries, fragments, and implicit ports are rejected.
`REA_BROWSER_ALLOWED_ORIGINS_JSON` accepts at most 32 exact HTTP(S) origins. Paths, credentials, queries, fragments, and wildcards are rejected. An input must stay within both this administrator ceiling and any active project/session/once grant.
Configuration is reloadable through the existing `SIGHUP` permission-policy path. Each request is checked against the configured endpoint, origin, and permission grants.
Inspect the exact effective scope without contacting the browser:
```bash
rea policy explain browser_observe \
--origins http://127.0.0.1:9222 \
--origins http://127.0.0.1:3000 \
--network loopback \
--json
```
Each request supplies a literal loopback CDP endpoint and target. Target
discovery uses the selected target's current origin by default; an optional
exact HTTP(S) origin filter can narrow the result. `localhost`, private-LAN
addresses, HTTPS CDP endpoints, credentials, paths, queries, fragments, and
implicit ports are not valid CDP endpoints. Origin filters contain only scheme,
host, and port; paths, credentials, queries, fragments, and wildcards are not
valid origins.
## CLI workflow
Configured origins are used by default. `--allowed-origins` can request a narrower set that still fits the configured ceiling.
Pass the endpoint and selected target for each call. An origin filter is
optional and narrows the target set.
```bash
rea list-browser-targets http://127.0.0.1:9222 --json
@@ -96,7 +81,7 @@ rea inspect-web-page http://127.0.0.1:9222 TARGET_ID \
--json
```
Accessibility text and script content may contain application secrets and become part of the returned Evidence. Credential-shaped console substrings are redacted. JSON and WebSocket captures retain complete paths, types, and counts, never values or examples. Storage values remain redacted even when key-name capture is requested. `--include-storage-fingerprints` requests SHA-256 identity/value fingerprints, which support comparison only when cookies, DOM storage, IndexedDB, and Cache Storage were all captured completely.
Accessibility text, script content, query values, and explicitly requested primitive console text may contain application secrets and become part of the returned Evidence. URL username/password credentials are removed; ordinary query values, fragments, and parameter order remain intact. JSON and WebSocket captures retain complete paths, types, and counts, never values or examples. Storage values remain redacted even when key-name capture is requested. `--include-storage-fingerprints` requests SHA-256 identity/value fingerprints, which support comparison only when cookies, DOM storage, IndexedDB, and Cache Storage were all captured completely.
Screenshot capture returns the complete visible viewport as an inline image artifact:
@@ -110,7 +95,6 @@ rea capture-web-screenshot http://127.0.0.1:9222 TARGET_ID \
```json
{
"cdp_endpoint": "http://127.0.0.1:9222",
"allowed_origins": ["http://127.0.0.1:3000"],
"target_id": "TARGET_ID_FROM_LIST_BROWSER_TARGETS"
}
```
@@ -121,25 +105,25 @@ Collection counts are not capped. Per-value validation, the selected observation
window, and CDP protocol limits still apply; any incomplete or truncated values
are reported in the result.
Call `list_browser_targets` first because target IDs are browser-instance-specific. REA rechecks the selected target's current type and origin immediately before attaching.
Call `list_browser_targets` first because target IDs are browser-instance-specific. REA rechecks the selected target's current type and origin immediately before attaching. Without an origin filter, discovery lists all eligible pages; an individual capture defaults to the selected page's current origin.
REA establishes an authorized main-frame boundary before enabling Runtime, Debugger, or Network observation, rechecks it before document capture, and validates it again afterward. If the main frame navigates during final capture, REA discards the mixed result and returns `target_changed`.
REA establishes a main-frame origin boundary before enabling Runtime, Debugger, or Network observation, rechecks it before document capture, and validates it again afterward. If the main frame navigates during final capture, REA discards the mixed result and returns `target_changed`.
## Data minimization
REA removes sensitive values before normalized event data is retained:
REA retains local observation data and removes only URL userinfo credentials, explicitly declared scenario secrets, and values from structured credential/storage fields:
- URLs retain a bounded path and at most 256 bounded query parameter names, replace every retained query value with `[REDACTED]`, and remove credentials and fragments.
- URL observations preserve the complete local URL, including query values, fragments, duplicate parameters, and their order. Only username/password userinfo is removed; malformed original URL strings remain available with `origin: null`.
- DOM snapshots retain node types, node names, value lengths, and attribute names, but not text or attribute values.
- Accessibility structure and roles are retained by default, while names and descriptions require `include_accessibility_text: true`.
- Network observations retain method, status, MIME type, size, type, initiator stack location, and redacted URLs. Headers are discarded after an allowlisted projection of length/encoding, structured CSP/Link/policy fields, and untrusted agent hints. Cookies and authorization headers are never retained.
- Network observations retain method, status, MIME type, size, type, initiator stack location, and complete URLs with userinfo credentials removed. Headers are discarded after an allowlisted projection of length/encoding, structured CSP/Link/policy fields, and untrusted agent hints. Cookies and authorization headers are never retained.
- Request/response bodies are not requested or parsed by default. When selected, only allowed-origin JSON media types are read, converted immediately to complete value-free property paths/types, and discarded. `Network.getResponseBody` is never sent unless JSON body shape capture is selected.
- Console observations with a stack source retain call type, argument types, timestamp, and redacted source location. When console text capture is selected, already-delivered primitive values are retained after credential redaction; objects, getters, and remote properties are never expanded.
- Console observations with a stack source retain call type, argument types, timestamp, and source location. When console text capture is selected, already-delivered primitive values are retained verbatim; objects, getters, and remote properties are never expanded.
- WebSocket observations retain direction, opcode, and payload byte length. When shape capture is selected, text frames are classified as text or complete value-free JSON shape; binary bytes, hashes, prefixes, and raw frames are never retained.
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` selection. Cache bodies above 64 KiB and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
- Script metadata is included only when CDP supplies a URL on an allowed origin. Stable keys exclude transient CDP script IDs, and exact transient raw URLs are used only during script/resource reconciliation. URL-less scripts are excluded because their origin cannot be established. When CDP supplies an execution-context association, the accepted script retains its authorized frame ID for later attribution. Source content is omitted unless explicitly requested and is returned inline as a self-verifying artifact with its SHA-256 digest.
Cross-origin frames, resources, scripts, events, and workers are excluded unless their exact origins are also approved. Excluded target details are counted without being exposed.
Frames, resources, scripts, events, and workers outside the selected origin scope are excluded. An explicit `allowed_origins` list can include additional origins; excluded target details are counted without being exposed.
Retained workers include validated opener-target and parent-frame IDs when CDP
provides them; these relationships do not expand the exact-origin boundary or
prove static module ownership.
@@ -156,12 +140,12 @@ coverage. It reports excluded items and any values omitted or truncated by
validation; it does not drop records to meet an internal collection quota.
Disallowed-origin entries are filtered before retention.
CDP discovery, WebSocket connection, and each command have a 5-second timeout. Version discovery is capped at 64 KiB, target discovery at 2 MiB, each WebSocket message at 16 MiB, and correlated pending commands at 128. Target and navigation-event inventories have no separate count cap. These transport bounds protect protocol framing and request correlation; they do not silently trim returned findings. Malformed unsolicited protocol events poison the connection so later commands fail closed.
CDP discovery, WebSocket connection, and commands continue until the peer responds or the caller cancels the operation; REA has no internal byte, message-count, or pending-command quota for these exchanges. Without caller cancellation, a stalled peer can leave an operation waiting. On cancellation, REA closes only its own WebSocket, waiting up to one second before terminating it. Malformed unsolicited protocol events poison the connection so later commands fail closed.
Network and console coverage starts only after REA attaches and enables the relevant CDP domains. `prior_activity_available` is always `false`; absence from these arrays is not evidence that an event never occurred. Source maps are not fetched by inspection. Bundle analysis can fetch them only after separate approval, with exact-origin redirect checks and no credentials, cookies, or referrer. Every requested map, mapping, original source, AST node, and finding from parsed content is returned inline; failed maps remain explicitly unavailable and make bundle completeness partial.
Network and console coverage starts only after REA attaches and enables the relevant CDP domains. `prior_activity_available` is always `false`; absence from these arrays is not evidence that an event never occurred. Source maps are not fetched by inspection. Bundle analysis fetches them only when `fetch_source_maps` is selected, with exact-origin redirect checks and no credentials, cookies, or referrer. Every requested map, mapping, original source, AST node, and finding from parsed content is returned inline; failed maps remain explicitly unavailable and make bundle completeness partial.
## Non-goals and threat model
The browser provider deliberately does not expose generic CDP commands, `Runtime.evaluate`, WebMCP invocation, navigation, input, downloads, page closure, or browser closure. Screenshot capture is the sole pixel surface and requires separate literal approval. REA disables the domains it enabled, detaches the target session, and closes only its own WebSocket.
The browser provider deliberately does not expose generic CDP commands, `Runtime.evaluate`, WebMCP invocation, navigation, input, downloads, page closure, or browser closure. Screenshot capture is the sole pixel surface and returns an image only when requested. REA disables the domains it enabled, detaches the target session, and closes only its own WebSocket.
This feature is not a browser sandbox or network containment mechanism. The attached page and browser continue running with their existing privileges and may make external requests independently of REA. CDP gives deep access to the selected browser profile, so use a dedicated profile, approve only origins you intend to inspect, and treat other same-user processes as outside this boundary.
This feature is not a browser sandbox or network containment mechanism. The attached page and browser continue running with their existing privileges and may make external requests independently of REA. CDP gives deep access to the selected browser profile, so use a dedicated profile, include only origins you intend to inspect, and treat other same-user processes as outside this boundary.
+53 -37
View File
@@ -3,42 +3,49 @@
`capture_browser_scenario` accepts the provider-neutral
`browserScenarioSchema` through both MCP and the
`capture-browser-scenario INPUT_JSON` CLI command. The request is declarative:
it admits a fixed action vocabulary, exact HTTP(S) origins, deterministic
browser settings, explicit storage and request replay, and provider-owned
it admits a fixed action vocabulary, explicit HTTP(S) targets, deterministic
browser settings, explicit storage seeds, and provider-owned
liveness deadlines. `INPUT_JSON` may be inline JSON or a JSON file path.
The minimal request contains only browser launch/connect authority, `start_url`,
`allowed_origins`, and at least one `actions` entry. Environment settings,
empty storage, disabled request replay, the required redaction policy, and a
final sanitized URL capture are supplied by default. Add secret declarations,
storage seeds, replay routes, query redaction names, and additional artifact or
event capture only when the investigation needs them. Action, secret, storage,
and replay-route counts are not capped. Duration, action, and navigation
The minimal request contains only browser launch/connect selection, `start_url`,
and at least one `actions` entry. Launch `headless` defaults to `true` and can
be set to `false` where the host supports a visible browser. Navigations follow
the destinations named by the scenario and the page's ordinary browser requests;
there is no separate origin allowlist or request-interception layer. Environment
settings (including service workers blocked by default), empty storage, and a
final sanitized URL capture are supplied by default. Set
`environment.service_workers` to `allow` when the application requires them.
Add secret declarations, storage seeds, and additional artifact or event
capture only when needed. Action, secret, and storage counts are not capped.
Duration, action, and navigation
timeouts remain fixed provider-owned liveness limits; the request does not
accept a caller-controlled `limits` object.
The browser boundary is part of the contract. Launch mode requires a
caller-selected absolute executable, a provider-owned temporary profile,
headless operation, and close-plus-delete cleanup. Connect mode accepts only an
explicit-port loopback CDP endpoint, declares the browser externally owned, and
permits disconnect-only cleanup. The Playwright driver preserves this
caller-selected executable and always uses a provider-owned temporary profile
that is closed and deleted during cleanup. Connect mode accepts only an
explicit-port loopback CDP endpoint. The Playwright driver preserves this
distinction: it closes and deletes provider-owned profiles, but only disconnects
from an external CDP browser. Real-browser verification checks both outcomes and
confirms that an attached external browser remains alive.
URLs contain a query-free HTTP(S) base plus ordered query entries. Every query,
form, storage, cookie, and replay value is either an explicitly public literal
or a reference to a declared environment-backed secret. Raw credentials in
URLs and literal credential headers are rejected. Secret query names must also
appear in the redaction policy, and the required credential headers cannot be
removed from that policy. Durable results must replace resolved secret values
HTTP(S) URLs may include ordinary query values and fragments directly. Use
ordered structured query entries when a value needs to reference a declared
environment-backed secret. Form, storage, and cookie values are either literal
strings or declared secret references. Raw URL userinfo credentials are rejected.
Captured credential-header
values are not retained, and declared secret values are redacted automatically.
A secret may be declared solely to
redact matching observed content; every secret reference in an action, URL,
storage value still needs a declaration. Ordinary query values
and fragments remain intact. Durable results replace resolved secret values
with their secret references.
Every start, navigation, storage, replay, and redirect origin must be listed in
`allowed_origins`. Unsupported action tags, unknown fields, duplicate step or
route IDs, undeclared or unused secrets, and provider-owned replay headers fail
validation. HTTP and WebSocket routing enforces the same declared scope. Exact
replay may abort unmatched requests or pass through only approved origins.
The exact `start_url`, navigation destinations, and storage-seed origins define
the inputs to a scenario; ordinary page requests are handled by the browser.
Storage cookies are scoped to the page URL at each capture. Unsupported action
tags, unknown fields, duplicate step IDs, and undeclared secret references fail
validation.
The result is Evidence with an initial state followed by one record per
declared action. Each step reports action status, elapsed time, sanitized URLs,
@@ -53,21 +60,30 @@ inline without application-defined size ceilings. Other missing sections
remain explicit and make the capture ineligible for equality claims.
Attach-mode captures also declare the unavoidable pre-attach event gap.
Browser automation is disabled by default. The administrator ceiling is
configured with:
Selected event families are observed on the root page and discovered descendant
popups, including nested popups. Popup lifecycle records are emitted only when
`popups` is selected. Launch-mode network observation starts at the owned
context before navigation, so popup document requests, redirects, and responses
are included once. Connected contexts can contain unrelated tabs: only network
events with a frame belonging to the root or a discovered popup are retained.
Initial popup requests without a frame cannot be attributed in shared contexts
and are omitted; attach coverage remains incomplete.
- `REA_BROWSER_SCENARIO_ENABLED`
- `REA_BROWSER_SCENARIO_EXECUTABLE_ROOTS_JSON`
- `REA_BROWSER_SCENARIO_CDP_ENDPOINTS_JSON`
- `REA_BROWSER_SCENARIO_ALLOWED_ORIGINS_JSON`
- `REA_BROWSER_SCENARIO_ALLOWED_ENV_JSON`
Playwright reports new popup Pages after their first navigation has committed.
It buffers early console messages and page errors, but the pinned API cannot
recover every earlier frame, worker, WebSocket, or download event. If one of
those families is selected and a popup is discovered, REA reports the family gap
in `limitations`, marks `events` missing, and disables equality eligibility.
Later events remain available; no initial navigation is synthesized from the
popup's current URL.
Unlike passive observation, browser automation is not granted automatically.
Use an explicit project/session grant, or opt into the administrator grant with
`REA_BROWSER_SCENARIO_AUTO_GRANT=true` for a trusted unattended environment.
Authorization commits the exact executable or CDP endpoint, origins, environment
variable names, network class, and scenario digest before any browser side
effect.
Each request names the exact launch executable or loopback CDP endpoint, target,
actions, and capture behavior. Origin filters and environment-variable names
are included when the scenario needs them.
Launch owns a temporary profile; connect mode disconnects from the selected
external browser without closing it. The request itself defines the operation;
there is no additional REA grant step. The browser and host still enforce their
own access and process rules.
## Scenario comparison
-101
View File
@@ -1,101 +0,0 @@
# Controlled JavaScript replay
`run_controlled_replay` executes only operator-selected extracted modules. It
does not launch or drive the original application. Static inference, passive
browser/Electron observation, isolated replay, and real application behavior
remain different authorities.
## Enable the ceiling
Replay is disabled by default. Configure exact source roots before REA starts:
```sh
export REA_JAVASCRIPT_REPLAY_ENABLED=true
export REA_JAVASCRIPT_REPLAY_ROOTS_JSON='["/absolute/extracted/modules"]'
```
The default trusted executables are the current Node.js runtime,
`/usr/bin/bwrap`, `/usr/bin/systemd-run`, `/usr/bin/systemctl`, and
`/usr/bin/bash`. Override them only with the corresponding
`REA_JAVASCRIPT_REPLAY_*_PATH` variable. Configuration creates the
administrator ceiling; it does not replace per-call approval. `rea doctor`
reports disabled, available, and exact sandbox-probe failures without running
module code. REA setup never installs these host components.
## Plan, review, execute
Call the same CLI command or MCP tool twice. The first request uses
`mode: "plan"`. Its response commits module paths and digests, closed dependency
aliases, cases, deterministic providers, runtime/backend identities, resource
limits, no-network policy, private filesystems, and a `plan_digest`.
Runtime commitments include the exact worker, seccomp filter, Node executable,
ELF loader, and shared-library source/destination paths and SHA-256 digests.
The execute request must repeat the same manifest and include:
```json
{
"mode": "execute",
"approved": true,
"plan_digest": "<exact digest returned by plan>"
}
```
REA rebuilds the plan. Any changed module, stub, case, runtime, backend, limit,
or export commitment returns `plan_stale` before worker admission.
Modules use `esm` or `commonjs-factory`. The latter accepts extracted Rspack
factory syntax such as `123(module, exports, require) { ... }` and implements
the bounded `require.d`, `require.r`, `require.n`, and `require.nmd` helpers.
Every import or require must map to a declared alias. There is no ambient
package resolution.
Explicit cases can be combined with deterministic `parser-boundaries`,
`sanitizer-boundaries`, or `clipboard-boundaries` generation. Supplying a
`right` manifest runs both sides with fresh realms per case and reports
`equal`, `changed`, or `unknown` comparisons.
## Boundary and Evidence
The worker receives module bytes over bounded stdin and runs under a fresh
Bubblewrap user/PID/network/IPC/UTS namespace, an architecture-checked seccomp
filter, an empty mount root with a descriptor-backed read-only Node closure,
private tmpfs, and a transient systemd user cgroup. Host and external network,
host writes, process spawning, workers, native addons, inspector access, and
ambient environment are unavailable. Wall time, memory, swap, tasks, CPU,
case input, worker-protocol input, output, stderr, depth, and node counts are
independently bounded. The parent also requires an exact worker response shape
and authenticates every returned case ID, order, and input digest. Values are
projected through own data descriptors only; Proxy values and accessors are
rejected without invoking their traps or getters.
Returns, exceptions, serialization failures, denials, timeouts, OOMs, crashes,
protocol failures, and cleanup state are observations with provider
`rea-javascript-replay` and authority `controlled-replay`. An observation does
not prove that the original renderer, preload, main process, browser, or remote
service behaved identically.
Each left/right run is retained as its own `observed` source Evidence. A
differential envelope is `derived` and links those source Evidence IDs, so the
comparison never erases the underlying observations.
Optional `reproducer_export` is committed by the plan and writes to its
caller-supplied path after the sandbox has stopped.
The owner-only manifest is written only after complete sandbox cleanup. Source
bytes are excluded unless `include_sources: true` was explicitly approved.
An export failure is retained in the result and does not erase a completed
replay observation.
## Local real-artifact verification
`npm run verify:replay` uses source-owned parser and hostile fixtures. An
operator can additionally supply a local ESM or Rspack/CommonJS-factory
manifest without copying module source into the repository:
```sh
REA_REPLAY_INPUT_PATH=/absolute/replay-manifest.json npm run verify:replay
```
The verifier canonicalizes the manifest's module paths and prints only plan,
module, and Evidence digests, case/comparison summaries, and cleanup state.
This is the intended seam for local Notion-like parser/sanitizer benchmarks.
+24 -30
View File
@@ -1,24 +1,23 @@
# Electron file-page observation
REA can attach to a user-owned Electron/Chromium CDP endpoint and inspect existing `file://` renderer pages without evaluating JavaScript or invoking Electron APIs. The caller supplies the endpoint directly; REA accepts only literal-port loopback HTTP endpoints. Selecting an endpoint exposes every Electron target it serves, including local file paths and page metadata.
REA can attach to an Electron/Chromium CDP endpoint and inspect existing `file://` renderer pages without evaluating JavaScript or invoking Electron APIs. The request supplies the endpoint directly; REA accepts only literal-port loopback HTTP endpoints. Selecting an endpoint exposes every Electron target it serves, including local file paths and page metadata.
This passive runtime surface is distinct from the target-free static
[`analyze_javascript_application`](javascript-artifact-reconstruction.md)
workflow. Static analysis reads an approved ASAR or extracted directory under
observations and static inferences are never silently treated as the same fact.
workflow. Static analysis reads a local ASAR or extracted directory without
executing application code. Passive observation inspects an existing Electron
page. The results keep static inferences and runtime observations distinct.
Use the separate
[`reconcile_javascript_runtime`](javascript-runtime-reconciliation.md) workflow
when both Evidence sets already exist.
## Configure authority
## Target boundary
The capability is disabled by default:
```bash
export REA_ELECTRON_OBSERVE_ENABLED=true
```
The capability is disabled by default. Its permission ceiling is loopback-only. REA accepts local hostless `file://` URLs that resolve to regular files; remote hosts, encoded path separators, and nonexistent paths are rejected. There is no separate filesystem-root configuration.
The request supplies a literal-port loopback endpoint and, for inspection, a
target ID. REA accepts local hostless `file://` URLs that resolve to regular
files; remote hosts, encoded path separators, and nonexistent paths are
rejected. The endpoint exposes every eligible target and its local path
metadata.
## Workflow
@@ -32,7 +31,8 @@ rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID \
--observation-ms 100 --json
```
Script content is excluded by default. Requesting it includes every authorized script source:
Script content is excluded by default. Setting `include_script_sources` in the
request returns the selected target's script sources:
```bash
rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID \
@@ -42,9 +42,9 @@ rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID \
The normalized result contains canonical local paths, frame and DOM
structure, resource metadata, stable script/resource identities, explicit
completeness, and content-addressed approved source artifacts. Script metadata
completeness, and content-addressed script artifacts. Script metadata
retains its execution-context frame ID when CDP supplies one. The capture also
inventories authorized worker, service-worker, and shared-worker targets with
inventories worker, service-worker, and shared-worker targets with
validated opener-target and parent-frame IDs. Worker discovery uses passive
target metadata; REA does not attach to or execute code in those targets.
Collection counts and aggregate script-source bytes are not capped. Like every
@@ -58,31 +58,25 @@ invoke Electron IPC, close a target, or terminate the application.
## Active Electron scenarios
Active Electron authority is separate and disabled by default. When enabled,
REA launches an operator-approved Electron executable through the official
Playwright Electron API, owns its lifetime, accepts bounded click/wait actions,
The scenario request supplies the Electron executable and application entry
point, plus any arguments and actions. The application root defaults to the
entry point's parent directory and can be supplied when the app uses a different
root. REA launches that executable through
the official Playwright Electron API and owns its lifetime. It accepts click/wait actions,
window-targeted renderer reload/crash actions, and synthetic `open-url` or
`second-instance` deep-link delivery. It records capture-scoped window and
WebContents identities, process metrics, and IPC channel and value-shape metadata.
Payload values are not retained.
Configure exact roots and run the real fixture verifier with an operator-owned
Electron runtime:
Run the real fixture verifier with an operator-provided Electron runtime:
```bash
export REA_ELECTRON_AUTOMATE_ENABLED=true
export REA_ELECTRON_AUTOMATE_AUTO_GRANT=false
export REA_ELECTRON_AUTOMATE_EXECUTABLE_ROOTS_JSON='["/absolute/path/to/runtime"]'
export REA_ELECTRON_AUTOMATE_APPLICATION_ROOTS_JSON='["/absolute/path/to/app"]'
REA_ELECTRON_EXECUTABLE=/absolute/path/to/electron npm run verify:electron
```
This capability actively launches and interacts with the target. It is not a
passive CDP observation and must be granted separately as `electron_automate`.
The default is fail-closed (`AUTO_GRANT=false`); an operator can instead issue
a project/session/one-shot grant through the normal permission workflow. The
owned process keeps normal host filesystem and network privileges, so this is
an authority boundary and lifecycle boundary, not a sandbox.
This operation actively launches and interacts with the selected target. It is
not a passive CDP observation. The owned process keeps normal host filesystem
and network privileges, so lifecycle ownership does not make it a sandbox.
The CLI and MCP surfaces accept the same schema. For a JSON request file:
@@ -105,7 +99,7 @@ caller-selected capture limits.
Active Electron Evidence can also be supplied to
[`reconcile_javascript_runtime`](javascript-runtime-reconciliation.md). That
projection is intentionally target-only and partial: it binds the approved
projection is intentionally target-only and partial: it binds the selected
application path and capture outcome to the static graph, while frames, scripts,
workers, and execution claims remain unavailable until a separate passive runtime
capture provides them.
+32 -282
View File
@@ -9,7 +9,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -23,12 +22,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -48,7 +43,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -62,12 +56,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -87,7 +77,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -101,12 +90,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -126,7 +111,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -140,12 +124,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -165,7 +145,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -179,12 +158,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -204,7 +179,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -218,12 +192,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -243,7 +213,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -257,12 +226,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -304,7 +269,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -318,12 +282,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -343,7 +303,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -357,12 +316,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -382,7 +337,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -396,12 +350,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -413,137 +363,6 @@
"required": ["code", "category", "message", "retryable", "remediation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"code": { "type": "string", "const": "permission_required" },
"category": {
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"unavailable",
"execution_failure"
]
},
"message": { "type": "string", "minLength": 1 },
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"additionalProperties": false
},
"details": {
"type": "object",
"properties": {
"capability": { "type": "string" },
"requested": {
"type": "object",
"properties": {
"capability": { "type": "string" },
"roots": { "type": "array", "items": { "type": "string" } },
"executables": {
"type": "array",
"items": { "type": "string" }
},
"environment_names": {
"type": "array",
"items": { "type": "string" }
},
"origins": { "type": "array", "items": { "type": "string" } },
"network": {
"type": "string",
"enum": ["none", "loopback", "external"]
},
"mount": { "type": "boolean" }
},
"required": [
"capability",
"roots",
"executables",
"environment_names",
"network",
"mount"
],
"additionalProperties": false
},
"missing": {
"type": "object",
"properties": {
"roots": { "type": "array", "items": { "type": "string" } },
"executables": {
"type": "array",
"items": { "type": "string" }
},
"environment_names": {
"type": "array",
"items": { "type": "string" }
},
"origins": { "type": "array", "items": { "type": "string" } },
"network": {
"type": "string",
"enum": ["none", "loopback", "external"]
},
"mount": { "type": "boolean", "const": true }
},
"additionalProperties": false
},
"ceiling": {
"anyOf": [
{
"type": "object",
"properties": {
"capability": { "type": "string" },
"roots": { "type": "array", "items": { "type": "string" } },
"executables": {
"type": "array",
"items": { "type": "string" }
},
"environment_names": {
"type": "array",
"items": { "type": "string" }
},
"origins": {
"type": "array",
"items": { "type": "string" }
},
"network": {
"type": "string",
"enum": ["none", "loopback", "external"]
},
"mount": { "type": "boolean" }
},
"required": [
"capability",
"roots",
"executables",
"environment_names",
"network",
"mount"
],
"additionalProperties": false
},
{ "type": "null" }
]
}
},
"required": ["capability", "requested", "missing", "ceiling"],
"additionalProperties": false
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
@@ -552,7 +371,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -566,12 +384,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -591,7 +405,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -605,12 +418,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -630,7 +439,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -644,12 +452,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -669,7 +473,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -683,12 +486,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -708,7 +507,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -722,12 +520,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
@@ -747,7 +541,6 @@
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
@@ -761,51 +554,8 @@
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"additionalProperties": false
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"code": { "type": "string", "const": "plan_stale" },
"category": {
"type": "string",
"enum": [
"invalid_input",
"permission_required",
"unsupported_provider",
"integrity_mismatch",
"truncated",
"cancelled",
"timeout",
"unavailable",
"execution_failure"
]
},
"message": { "type": "string", "minLength": 1 },
"retryable": { "type": "boolean" },
"remediation": {
"type": "object",
"properties": {
"action": { "type": "string", "minLength": 1 },
"restart_required": { "type": "boolean" },
"elicitation_supported": { "type": "boolean" }
},
"required": ["action", "restart_required"],
"properties": { "action": { "type": "string", "minLength": 1 } },
"required": ["action"],
"additionalProperties": false
},
"details": {
+188
View File
@@ -0,0 +1,188 @@
# DOS MZ and COM analysis with Ghidra
REA can analyze DOS MZ executables and explicitly selected COM images through the bring-your-own Ghidra adapter.
The Linux x64 verification lane exercises actual 16-bit disassembly and
decompilation through the CLI and stdio MCP. It requires the same Ghidra
12.1.4 and 64-bit JDK 21 installation as other Ghidra sessions; no DOS emulator
or cross-compiler is required.
## Open and inspect
```bash
rea inspect /absolute/path/to/legacy.exe --provider ghidra --json
rea search /absolute/path/to/legacy.exe entry --kind procedures --provider ghidra --json
rea instructions /absolute/path/to/legacy.exe entry --provider ghidra --json
rea function /absolute/path/to/legacy.exe entry --provider ghidra --json
rea inspect-native-load-image /absolute/path/to/legacy.exe --provider ghidra --json
rea read-bytes /absolute/path/to/legacy.exe 0x10000 --length 16 --provider ghidra --json
rea address-to-file-offset /absolute/path/to/legacy.exe 0x10000 --provider ghidra --json
```
Use an observed procedure name or address from discovery when the target does
not name its entry function `entry`. MCP uses `open_binary`, `list_procedures`,
`procedure_info`, `read_function_instructions`, and `analyze_function` with the
same admitted target and provider semantics.
The classifier validates MZ page counts, initialized module length, header and
relocation-table bounds, relocation destinations, and the initialized entry
point. It distinguishes DOS load-module and relocation bytes from the Windows
new-header field. PE remains PE; NE, LE, and LX declarations are explicitly
unsupported. A damaged declared Windows header does not silently become DOS.
Inventory classification returns `unknown` when its bounded prefix cannot
establish the format; this is distinct from the complete target admission check.
Ghidra imports a private target snapshot with `MzLoader`, language
`x86:LE:16:Real Mode`, and compiler specification `default`. These settings,
the fixed Ghidra load segment `0x1000`, and the linear address convention are
committed in the analysis profile. The handshake rejects language or compiler
drift. Closing the session deletes the temporary project and snapshot.
The profile also commits `function_body_evidence: complete-inclusive-ranges-v1`,
so snapshots from older length-only Ghidra results do not satisfy the current
profile.
## Headerless COM images
COM has no identifying header. Select its interpretation explicitly; neither the
`.com` suffix nor arbitrary unrecognized bytes are enough to admit it.
```bash
rea inspect /absolute/path/to/legacy.com --target-format dos-com --provider ghidra --json
rea function /absolute/path/to/legacy.com 0x10100 --target-format dos-com --provider ghidra --json
rea inspect-native-load-image /absolute/path/to/legacy.com --target-format dos-com --provider ghidra --json
```
```json
{
"name": "open_binary",
"arguments": {
"path": "/absolute/path/to/legacy.com",
"format": "dos-com",
"provider_id": "ghidra"
}
}
```
The caller's interpretation applies to the complete file, even if its filename
suggests another format. Files must contain 1..65280 bytes: the 64 KiB COM segment
minus the 256-byte PSP prefix. Ghidra uses `BinaryLoader` with real-mode language
and maps file offset zero at `1000:0100` (linear `0x10100`). A packaged pre-analysis
script seeds the entry function and CS/DS/ES/SS context `0x1000`. These settings
and the preparation policy are committed in the analysis profile; a failed
preparation prevents the resident bridge from serving results.
Load-image verification checks all original and modified file bytes, complete
source mapping/memory digests, absence of relocations, entry and measured register
context. The context is imposed for analysis, not observed from execution. PSP
memory, initial stack contents/SP, DOS interrupts and PC-98 devices are not
modeled. COM has no MZ header, relocation table or appended-overlay model; all
selected bytes belong to its analysis module. The original file stays unchanged.
## Addresses and function extent
Returned default-space addresses are linear byte coordinates. A Ghidra
segment:offset address has linear value `segment * 16 + offset`; multiple
segment aliases can identify the same byte. REA accepts the returned linear
address for subsequent lookups and leaves instruction decoding context with
Ghidra. These coordinates are analysis load addresses, not file offsets or
observations of a running DOS system.
`procedure_info` and function dossiers include `body` evidence:
```json
{
"available": true,
"provenance": "ghidra-function-body-address-set",
"ranges": [
{ "start": "0x10000", "end": "0x1000d" },
{ "start": "0x10060", "end": "0x10065" }
],
"total_bytes": 20,
"span_bytes": 102,
"non_contiguous": true,
"contains_entry": true
}
```
Each end is **inclusive**. `total_bytes` counts the complete observed
AddressSet; `span_bytes` measures its enclosing span and is null across address
spaces. Gaps and shared tails must not be treated as owned bytes. This is
Ghidra's database observation, not a proof of original source ownership or
exhaustive code discovery. Providers that do not report complete ranges return
`available: false` and an explicit reason.
## Loaded-image evidence
`inspect_native_load_image` independently checks the immutable session snapshot
against the loaded Ghidra Program. It measures complete original and modified
FileBytes digests, every source mapping and its memory digest, relocation records
(including multiplicity), and external entry points. It verifies MZ header and
module partitions, relocation fixups and linear coordinates rather than relying
on the imported executable SHA alone.
The result is `verified`, `mismatch`, or `unsupported`. Every check retains its
expected and observed values. A memory-digest mismatch identifies the source
range's file offset and analysis address; it does not locate the first differing
byte inside that range. Mapping ends are **inclusive**, unlike the exclusive ends
from `list_segments`. Appended overlays remain in source-file identity and are
excluded from initialized module coverage. Uninitialized allocations are reported;
their contents and DOS runtime semantics are not verified. Formats other than MZ and explicit COM return
`unsupported` with measured observations, not a successful MZ verification.
`read_bytes` returns initialized provider memory, including loader fixups; an
unmapped or uninitialized byte ends the read with an explicit returned length and
`complete: false`. `address_to_file_offset` uses the provider's source mapping;
unmapped, uninitialized and ambiguous mappings fail explicitly. A relocated word
can differ from the original file word even when its source offset is known.
## Coverage boundaries
Static decompilation does not execute the target or emulate BIOS, DOS
interrupts, device ports, or self-modifying code. PC-98 hardware behavior and
recovered calling conventions can remain uncertain; provider pseudocode and
diagnostics are retained for analyst review.
For a packed executable, importing the original can analyze its unpacking
stub. Analyze a separately prepared unpacked copy to inspect the original
program's recovered code. Retain both artifact digests and the transformation
evidence; REA never silently replaces the admitted target. Appended overlays
are separate from the initialized MZ module. Raw COM files and DOS extenders
without an admitted MZ real-mode entry are outside this import boundary.
DOS MZ is available through Ghidra, with no Hopper support claim. It is outside
the Windows native PE P0 boundary. macOS DOS verification remains unverified.
## Real verification
```bash
npm run verify:ghidra:dos
```
The lane generates a small public fixture from source at runtime. It verifies
16-bit instruction bytes, a near call, a segment-relocated far call, actual
decompilation, disjoint function ranges, stable CLI/MCP observations, unchanged
input bytes, and session/process cleanup. It does not require external
executable fixtures, an emulator, or an existing analysis database.
Provider p-code representations can contain process-specific address-space selector
tokens. The lane preserves those reported values and compares stable dossier
observations rather than asserting cross-process identity of raw p-code.
## Function annotations
`annotate_native_function` changes the name and/or comments at one exact local function entry, verifies readback, and returns a refreshed dossier. This is useful when recording recovered DOS function roles; it does not claim original-source names. Name writes preserve the existing namespace; readback returns the fully qualified name used by function inventories. Regular comments map to Ghidra PRE comments and inline comments to EOL comments. Empty text clears a comment; omitted fields preserve it. The whole edit rolls back if a setter, readback or analysis fails.
```json
{
"procedure": "0x10100",
"name": "entry",
"comment": "Recovered startup role",
"inline_comment": "Inspect segment setup"
}
```
```bash
rea annotate-native-function /local/program.com 0x10100 --target-format dos-com --provider ghidra --name entry --comment 'Recovered startup role' --json
```
The CLI returns the updated analysis before closing its isolated session. MCP edits remain visible to later reads until `close_binary`. Original bytes remain unchanged, database edits are discarded on close, and immutable analysis snapshots are invalidated after edits. This operation does not save a Ghidra project, change ABI/types/body ranges, patch instructions, or emulate a DOS runtime. Windows P0 does not admit it.
+141 -92
View File
@@ -4,22 +4,27 @@ REA separates installing its CLI from configuring external software and agents.
## Start setup
The recommended setup entrypoint is:
Start setup with:
```bash
npx --yes rea-agents@latest setup
npx rea-agents setup
```
If npm asks to download and run the package, that approval applies only to the
current package-runner invocation. REA still prints its own setup plan and asks
for separate approval before changing agent configuration or installing a
product-owned component.
If npm asks to download and run REA, that approval applies only to downloading
the package. REA shows its own plan and asks before changing agent configuration
or installing Hopper.
The explicit `@latest` request prevents a project dependency or stale npx cache
entry from choosing the setup version.
REA runs the version npm selected and never silently replaces an intentional
version request after launch. To repair registrations written by an older
release, rerun the command above and review the resulting setup plan.
The short command can use a REA version installed in the current project. To
request the latest release explicitly, use:
```bash
npx rea-agents@latest setup
```
REA runs the version npm selects. To update older agent registrations, run the
latest-version command and review its setup plan. For unattended package
downloads, add `--yes` before the package name; this does not approve REA's
setup changes.
For an intentional rollback, make the package request explicit:
@@ -31,11 +36,11 @@ Setup continues to pin persistent MCP registrations to the exact version that
performed setup. Running current setup later migrates unversioned or older
managed registrations through the normal reviewed setup transaction.
REA supports Node.js 22.19+ and 24.11+ (including newer releases). It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew.
REA supports Node.js 22.x (>=22.19), 24.x (>=24.11), and 26+. Node.js 23, 25, and prereleases are unsupported. It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew.
Running `npm install rea-agents` without `--global` installs the executable only
in the current project's `node_modules/.bin`; it does not make `rea` available
on the shell `PATH`. Use the explicit setup command above for the guided setup
on the shell `PATH`. Use the setup command above for the guided setup
journey, `npx -y rea-agents@latest` for unattended one-off commands, or install globally
with `npm install --global rea-agents` for a shell-visible `rea` command.
@@ -56,60 +61,65 @@ curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh |
Supported options are `--version <semver>`, `--dry-run`, `--no-setup`, `--no-prompt`, and `--verbose`. Neither `--no-prompt` nor a non-interactive shell grants permission to install external dependencies.
## Supported agents
Setup can configure these clients for REA's local MCP server:
| Client | `--client` value |
| ------------------ | ---------------- |
| Claude Code | `claude_code` |
| Claude Desktop | `claude_desktop` |
| Codex | `codex` |
| Cursor | `cursor` |
| Gemini CLI | `gemini_cli` |
| Windsurf | `windsurf` |
| Devin | `devin` |
| OpenCode | `opencode` |
| Antigravity | `antigravity` |
| GitHub Copilot CLI | `copilot_cli` |
| Command Code | `commandcode` |
| VS Code | `vscode` |
## Review setup changes
`rea setup` uses an inline, scroll-preserving journey inspired by the clarity of
PostHog's CLI wizard. It begins with the outcome instead of the installer
mechanics:
`rea setup` first offers the supported agents in a multi-select. Existing REA
registrations are selected by default. Newly detected clients remain available
but unselected: detection gives setup context, not permission to add a new
registration. Clients without a detected configuration can still be selected.
Explicit `--client` flags skip this question.
- investigate local applications from a supported agent;
- recover evidence through an available deep-analysis provider;
- use the bundled skill for a repeatable investigation workflow.
Setup adds MCP access for selected clients. It installs REA's bundled workflow
with those integrations by default; use `--skill=false` to omit it. If no agent
is selected, setup offers the workflow separately for CLI use, with No as the
default. Hopper is a separate optional choice: setup shows its proposed
installation or connection and requires its own explicit approval. It can also
save verified paths for an existing Ghidra installation.
REA then summarizes the detected clients and asks which capabilities to set up.
The capability picker pre-selects `Agent integration` (the MCP registration and
matching guided workflow together) when detected agents exist, and presents the
optional Hopper provider alongside it. Selecting agent integration opens a second
checklist for the exact detected agents that should receive a registration; all
detected agents are pre-selected. Detection provides context and pre-selection;
it does not authorize a configuration write until the user confirms.
After selection, review the plan's exact paths and changes and approve before
REA writes files or installs Hopper. You can cancel at any prompt.
Because the user has explicitly invoked `rea setup`, the wizard biases toward the
happy path: capabilities and agents are pre-selected, and the final approval
defaults to **Yes**. The user can still deselect any item or cancel at any prompt.
Choosing no capabilities exits without changes. Selecting agent integration
always includes the bundled skill, even if all agents are deselected. The picker
keeps its navigation, selection, confirmation, and cancellation keys visible
instead of relying on a transient hint.
Before applying changes, REA checks your current configuration. The plan lists:
Every selected path converges on the same exact preflight. REA validates the
current state, prints the proposed effects, and asks for final approval with
**Yes** as the default. Selection alone never authorizes a mutation. The plan
identifies:
- an existing Hopper installation, a validated bring-your-own Ghidra environment, or the official Hopper package it proposes to install;
- an existing Hopper installation, a verified existing Ghidra installation, or the official Hopper package it proposes to install;
- each detected agent configuration path;
- the REA skill destination;
- external software, network origins, integrity evidence, and package-manager
commands.
Malformed or unsafe existing configuration blocks the whole transaction before
Hopper installation or any file write. Declining, pressing Ctrl-C, or selecting
nothing makes no changes. Agent configuration writes preserve unrelated
Hopper installation or any file write. Declining or pressing Ctrl-C makes no
changes. Agent configuration writes preserve unrelated
entries, create backups, use atomic replacement, and verify their result.
Progress remains append-only so completed and failed operations stay visible in
terminal history. After a successful run, the completion message names the
verified capabilities now available—for example configured MCP clients, the
selected analysis provider, and the installed skill—and gives the corresponding
next action. When an agent must restart to load its registration, REA says so;
otherwise it suggests beginning an investigation. It does not advertise a
capability that the final diagnostic check did not verify.
After setup, REA reports which agents, analysis tools, and workflow files passed
its final checks. Restart any agent named in the completion message, then begin
your investigation. Failed steps and diagnostics remain in terminal history.
Select exact clients in scripts with repeatable `--client` flags, or retain
automatic discovery explicitly with `--all-detected`. Use `--skill=false` to
override the normal bundled-skill installation and `--dry-run` for a read-only
plan:
Select exact clients in scripts with repeatable `--client` flags. Each explicit
client skips interactive selection. Use `--all-detected` only when you intend
to configure every detected supported client. `--skill=false` omits the
workflow, and `--dry-run` returns a read-only plan with status `planned` and
exit code 0:
```bash
rea setup --client codex --client cursor --skill=false --dry-run
@@ -117,13 +127,20 @@ rea setup --client codex --client cursor --skill=false --dry-run
Prompt UI and progress are written to stderr so stdout remains available for
structured results and pipelines. `NO_COLOR=1` disables color. Use
`--accessible` for sequential, vertically rendered yes/no prompts.
`--accessible` for sequential, vertically rendered yes/no prompts. Implicit
interactive setup requires stdin, stdout, and stderr to all be terminals; when
any stream is redirected, setup stays non-interactive. Declining or cancelling
returns status `cancelled` with exit code 0.
For automation, `rea setup --json` reports the plan without applying it.
Prefer pairing `--yes` with explicit scope such as `--client codex`,
or `--all-detected`. Legacy unscoped `--yes` remains compatible for
this release but emits a deprecation warning. Installing missing Hopper
non-interactively additionally requires `--install-hopper`:
For automation, `rea setup --json` reports the plan and a compact `.doctor`
readiness projection without applying it. Use `rea doctor --json` for full
health diagnostics and canonical tool catalog details. Pair
`--yes` with explicit scope such as `--client codex`, or use
`--all-detected` when the broad scope is intended. Without a scope flag,
`--yes` is limited to existing REA-owned registrations; it does not select all
detected clients. An unapproved actual apply reports `needs_confirmation` and
exits 1. Installing missing Hopper non-interactively additionally requires
`--install-hopper`:
```bash
rea setup --yes --all-detected --install-hopper --json
@@ -131,11 +148,14 @@ rea setup --yes --all-detected --install-hopper --json
Setup pins package-runner MCP registrations to the exact installed REA version,
installs the matching skill and on-demand references in the same plan, and adds
`startup_timeout_sec = 30` for Codex. Interactive `rea upgrade` installs the new
executable and then opens the updated `rea setup --all-detected` plan for
separate approval. Structured or non-TTY upgrades defer that integration sync.
Use the same setup command to migrate floating, unversioned, or stale
registrations, then restart changed clients.
`startup_timeout_sec = 30` for Codex. `rea update` installs the exact resolved
release into the npm prefix that owns the running package, then checks the new
executable's version before reporting success. It does not reopen onboarding.
Release lookup and installation both use npm's configured registry.
Its maintenance plan selects only existing REA registrations and an already
installed REA skill. Run the returned scoped setup command to review and approve
those changes, then restart affected agents. The plan is returned in terminal,
non-TTY, and JSON modes without applying configuration changes.
## Hopper
@@ -193,15 +213,28 @@ copying license secrets into logs, or killing unrelated Hopper processes.
## Ghidra
REA's Ghidra provider is bring-your-own and supports Linux x64 with the exact
official Ghidra 12.1.4 release and a 64-bit full JDK 21. macOS is not an
admitted Ghidra host. An experimental
Windows x64 P0 supports approved native x86-64 PE applications. It supplies
discovery, analysis-profile commitment, an isolated read-only headless session,
ten inventory/name/search operations and nine function-analysis operations,
for 19 read-only operations total. Function analysis covers metadata,
decompilation, assembly, resolved calls, typed references, xrefs, CFG, and
dossiers. GUI state and analysis mutations remain unavailable through Ghidra.
REA connects to an existing Ghidra installation on Linux x64, macOS x64/arm64,
or experimental Windows x64 P0.
It requires Ghidra 12.1.4 and a 64-bit full JDK 21. On macOS, the installation
must include the native decompiler for the host architecture; REA does not
build it or change Gatekeeper quarantine settings.
The adapter exposes 25 read-only operations: thirteen inventory/name/search
operations and twelve function-analysis operations. These cover metadata,
decompilation, assembly, resolved calls, typed references, xrefs, function
dossiers, instructions, recovered data types, measured load mappings, loaded
memory bytes, and observed file offsets. Independent load-image attestation
supports DOS MZ and explicitly selected COM; PE returns its measurements with that limitation.
On Linux and macOS, `annotate_native_function` also edits a function name and/or
entry comments atomically and returns refreshed analysis. These session metadata
edits leave executable bytes unchanged and are discarded on close. GUI controls
require Hopper; Windows P0 remains read-only.
Windows P0 admits native x86-64 PE applications on fixed local NTFS volumes.
The npm package bundles native Job Object ownership, protected private runtime
DACLs, and handle-based path admission; no separate addon installation is needed.
See the [Windows Ghidra P0 guide](windows-ghidra-p0.md) for verified scope and
limitations.
Extract Ghidra and install the JDK outside REA, then export absolute paths:
@@ -212,7 +245,7 @@ rea doctor --json
rea setup
```
PowerShell configuration for Windows uses the same non-secret paths:
On Windows, configure the existing installation in PowerShell:
```powershell
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
@@ -221,10 +254,12 @@ rea doctor --json
rea providers --json
```
`rea setup` does not mutate Windows client configuration or install Hopper,
Ghidra, Java, Python, or another package. Register the built `rea mcp` command
manually in the intended client and preserve the two environment variables.
See [Windows Ghidra P0](windows-ghidra-p0.md) for a complete example.
`rea setup` can configure supported Windows agents and install the bundled
REA skill after approval. Direct registrations use Node to launch REA's entry
script; package-runner registrations use the pinned `npx` command. Hopper
installation remains unavailable on Windows. Setup never installs Ghidra,
Java, or Python. It preserves valid detected Ghidra/JDK settings in agent
registrations. See [Windows Ghidra P0](windows-ghidra-p0.md) for provider diagnostics.
Doctor validates the platform, architecture, application version,
`support/analyzeHeadless` or `support/analyzeHeadless.bat`, Java
@@ -237,12 +272,10 @@ installs, upgrades, or modifies Ghidra or Java.
Each verified session uses an ephemeral temporary project and isolated
home/cache/config/temp paths. REA passes `-readOnly`, `-deleteProject`, uses
Ghidra's default analysis and resource settings, and loads its packaged Java
bridge via `-scriptPath`; it never opens an existing user project. Linux uses a
current-user-only local bridge socket and descriptor. Windows P0 uses
token-authenticated IPv4 loopback, a token-free endpoint record, and bounded
process-tree termination. It does not yet prove private DACL,
reparse-point-safe, or Job Object semantics; use only approved non-sensitive
fixtures.
bridge via `-scriptPath`; it never opens an existing user project. Linux and
macOS use a current-user-only local bridge socket and descriptor. The
experimental Windows transport uses authenticated IPv4 loopback with a
private native-owned bearer descriptor and Job Object process ownership.
Operations begin only after default auto-analysis completes. The provider
startup deadline fails the open rather than exposing partial analysis. One
@@ -255,9 +288,18 @@ is no fixed per-operation or response-size ceiling. Unresolved computed calls
remain unknown, reference-kind provenance is preserved, and provider-specific
pseudocode is never treated as original source or Hopper-equivalent text.
Unexpected request failures retain their original internal cause. CLI and MCP
errors expose Error names, messages, and available codes under
`details.diagnostics.failure_cause`; other rejection values retain their
primitive value or an explicit type. Shutdown warnings include the failure
kind, message, and diagnostics, while process and temporary-project cleanup
continues. These messages redact known bridge authentication tokens and
preserve local paths and other analysis context.
Run `GHIDRA_INSTALL_DIR=... npm run verify:ghidra` from a source checkout to
compile and analyze debug and stripped x86-64 ELF fixtures on Linux x64. This lane needs only a host C compiler
in addition to Ghidra and its JDK.
compile and analyze debug and stripped host-native fixtures (ELF on Linux x64
or Mach-O on macOS), plus a native DWARF 4 type-layout object. This lane needs a
host C compiler in addition to Ghidra and its JDK.
Run `GHIDRA_INSTALL_DIR=... npm run verify:ghidra:cross-format` to add AArch64
ELF, x86-64 PE, and x86-64 Mach-O fixture coverage. This separate lane needs
@@ -267,12 +309,14 @@ host-native Ghidra acceptance lane.
On a controlled Windows x64 runner, use
`npm run verify:ghidra:windows`. The verifier generates a deterministic native
PE fixture from source bytes, exercises every admitted Ghidra operation, checks
target/snapshot/import digest identity, and requires complete runtime cleanup.
PE fixture from source bytes and requires the Windows native authority before
opening the provider. This lane remains blocked until those controls are
implemented; its intended checks include operation coverage, digest identity,
and complete runtime cleanup.
## Diagnose, update, and remove
`rea doctor --json` is strictly read-only. `rea upgrade` updates only the npm installation that owns the running CLI. `rea uninstall` removes only REA-owned agent registrations and skill files; `--purge-data` additionally removes REA cache and state paths.
`rea doctor --json` is strictly read-only. `rea update` updates only the npm installation that owns the running CLI. Source checkouts and package-runner copies must be updated through the mechanism that owns them; a fresh package-runner invocation can use `npx rea-agents@latest`. `rea uninstall` removes only REA-owned agent registrations and skill files; `--purge-data` additionally removes REA cache and state paths.
## MCP Registry
@@ -286,17 +330,22 @@ server through `npx` with the `mcp` command.
For a client that requires manual configuration, use:
<!-- x-release-please-start-version -->
```json
{
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@latest", "mcp"]
"args": ["-y", "rea-agents@4.0.1", "mcp"]
}
}
}
```
Use an exact `rea-agents@VERSION` in the arguments when a reproducible client
configuration is required. `rea setup` writes the same package-runner shape and
pins it to the exact version that performed setup.
<!-- x-release-please-end -->
Persistent registrations should use one exact package version. `rea setup`
writes the same package-runner shape and pins it to the exact version that
performed setup. Run current setup to refresh an older registration, then
restart the client.
+1 -1
View File
@@ -97,7 +97,7 @@ correspondence is a separately identified cross-layer inference.
Every observation and edge carries all of the following:
- an authority, such as artifact bytes, AST analysis, static relationship
inference, passive CDP runtime, cross-layer reconciliation, controlled replay,
inference, passive CDP runtime, and cross-layer reconciliation,
native analysis, historical reference, or user assertion;
- an epistemic state: `observed`, `inferred`, `unknown`, or `unavailable`;
- confidence independently bounded as exact, high, medium, low, or unknown;
+12 -24
View File
@@ -7,7 +7,7 @@ are `trace_application_feature`, `trace_javascript_semantics`,
`compare_javascript_export_shapes`; their CLI equivalents use the same names
with hyphens.
Both workflows consume Evidence produced by
These workflows consume Evidence produced by
`analyze_javascript_application` or `reconcile_javascript_runtime`. They do not
read an artifact, execute application code, attach to a process, or open a
native-analysis provider. Static artifact observations, passive runtime
@@ -132,8 +132,10 @@ unique occurrence on each side and pairing is reciprocal. Changes use JSON
Pointer paths with `added`, `removed`, `changed`, or `unknown` status. A missing
field is added or removed only when the relevant parent-property coverage is
complete on both shapes. The output includes exact selector candidates,
omissions, Evidence links, coverage, limitations, and a separate controlled
replay recommendation; it does not execute JavaScript.
omissions, Evidence links, coverage, and limitations; it does not execute
JavaScript. When runtime semantics matter, run behavioral probes directly
against the relevant application versions and capture them through the
available browser, Electron, or process workflows.
## CLI and verification
@@ -146,8 +148,7 @@ For two operator-provided directories or ASARs, run:
```bash
npm run verify:application-workflows -- \
--left /absolute/path/to/version-a \
--right /absolute/path/to/version-b \
--source-map-read-approved
--right /absolute/path/to/version-b
```
The verifier reconstructs both versions independently, compares them, runs one
@@ -163,23 +164,10 @@ Because source Evidence and derived Evidence are retained by the normal session
ledger, evidence bundles and analysis snapshots can carry these records without
another persistence format.
## Controlled replay boundary
## Runtime evidence
Static graph workflows never execute a graph node or recovered module.
`run_controlled_replay` is the separate extracted-module boundary: it requires
the `javascript_replay` authority, a plan call followed by exact content-bound
approval, and the mandatory Linux OS sandbox fixed by
[ADR-0002](adr/0002-controlled-replay-authority-and-sandbox.md). Browser,
Electron, Process Capture, artifact-read, and static-analysis approvals do not
authorize that execution.
Replay observations retain `controlled-replay` authority. They cannot
promote static inference into passive runtime observation or prove that the
original application, renderer, preload, main process, or remote service
behaved identically.
Module, explicit case, and preset-generated case lists have no fixed
item-count ceiling. Replay plans commit the caller's resource budgets, and the
worker protocol checks the complete request against that declared byte budget
before probing executables or reading modules. Aggregate module-source and
case-input bytes are likewise checked against the caller's declared budgets.
Static graph workflows do not execute recovered code. When runtime semantics
matter, run behavioral probes directly against the relevant application
versions and capture them through the available browser, Electron, or process
workflows. Those observations do not prove behavior in an unobserved app
version or environment.
+21 -3
View File
@@ -52,6 +52,23 @@ manifest and graph commitments, JavaScript Application Graph, static
Electron summary, reconstruction statistics, and explicit limitations. It does
not require a live Hopper, Ghidra, browser, or Electron process.
## Large results
Graph and Evidence identifiers hash canonical JSON incrementally, without
assembling a single string for the whole graph. The canonical bytes and existing
identifiers remain unchanged. The opt-in regression check is
`npm run verify:javascript:digests`;
it hashes a value larger than the running Node engine's single-string limit.
Output formatters and MCP transport serialization still assemble whole strings.
For a large CLI result, select the needed fields
before formatting, for example `--format json --filter-output
evidence_id,normalized_result.statistics`. Complete serialization of a result
beyond the engine's string limit is not established by this digest check.
Client framing limits also apply: the pinned Node MCP SDK's stdio transport
defaults to a 10 MiB buffer. Its caller-selected `maxBufferSize` must accommodate
the complete response, including text and structured Evidence projections.
## What is reconstructed
The projector reuses the content-addressed artifact inventory and safe artifact
@@ -125,9 +142,10 @@ For `.node` bindings, member names mean “requested by JavaScript syntax.” A
resolved add-on path does not convert them into verified native exports. Native
symbol verification remains a separate deep-analysis claim.
Endpoint observations preserve useful local diagnostics while removing URL
credentials, fragments, and query values. Artifact paths, digests, parse
locations, and analysis metadata remain actionable because REA is local-only.
Endpoint observations preserve useful local diagnostics, including query values
and fragments, while removing only URL username/password credentials. Artifact
paths, digests, parse locations, and analysis metadata remain actionable
because REA is local-only.
## Safety boundary
+39 -9
View File
@@ -7,13 +7,11 @@ have equivalent `rea list-javascript-runtime-targets` and
`rea observe-javascript-runtime` commands. Successful calls return
deterministic Evidence.
This authority is disabled by default and is separate from browser CDP,
Electron file-page inspection, Process Capture, and controlled JavaScript
replay:
Each request supplies the loopback Inspector endpoint and, for observation, the
target ID. This surface is separate from browser CDP, Electron file-page
inspection, and Process Capture:
```bash
export REA_V8_INSPECTOR_OBSERVE_ENABLED=true
rea list-javascript-runtime-targets http://127.0.0.1:9229 \
--json
rea observe-javascript-runtime http://127.0.0.1:9229 TARGET_ID \
@@ -27,6 +25,38 @@ array.
For the MCP follow-up call, pass the same literal-loopback endpoint and the selected target ID to `observe_javascript_runtime`. REA rediscovers targets and validates the ID against the live endpoint before observation.
### Discovery file locations
Node's `/json/list` reports a best-effort pathname prefixed with `file://`,
without URI encoding. On POSIX, REA interprets unambiguous paths literally,
including `#` and `%` in filenames. Node also replaces backslashes and double
quotes with underscores. That transformation cannot be reversed reliably:
an underscore may already be part of the original filename.
Windows paths, ambiguous underscore paths, and local discovery paths that
cannot be verified remain eligible by their endpoint and exact target ID.
Their target location preserves the reported value:
```json
{
"kind": "unresolved",
"reported_url": "file://C:_tools_entry.js",
"reason": "unverifiable-file-location"
}
```
This location does not claim that a file exists or authorize reading its
contents. Listing remains HTTP-only. Observation resolves loaded script
locations independently from `Debugger.scriptParsed` file URLs, preserving
the existing checks for local files and encoded separators. Unsupported
schemes, remote file hosts, and a bare `file://` remain excluded. These Node
discovery rules do not apply to page targets or other runtime products.
Reconciliation retains an unresolved `target_location` object and reports
`runtime-location-unresolved` with unknown confidence for that target. It
does not derive a static match from the reported pathname; verified loaded
script locations remain available for independent matching.
## Passive protocol boundary
The provider sends exactly two protocol commands:
@@ -66,14 +96,14 @@ Electron role, so Evidence records the role authority as
`caller-declared-unverified`.
Electron main, preload, and renderer behavior can therefore be observed only
as separate approved Inspector targets. The provider does not infer that two
as separate Inspector targets. The provider does not infer that two
targets belong to the same Electron application.
## Capture and determinism
Every observation records its time window and returns all valid script and
execution-context events received during it. Per-location protocol validation
remains in force. Scripts are authorized after capture, deduplicated by stable
remains in force. Scripts are validated after capture, deduplicated by stable
metadata, and canonically sorted. Wall-clock timestamps and protocol script IDs
are excluded from the durable result, so identical inputs and captured metadata
produce the same Evidence ID.
@@ -88,8 +118,8 @@ behavior does not occur.
Pass the resulting observation Evidence and an
`analyze_javascript_application` Evidence record to
`reconcile_javascript_runtime`. The reconciliation accepts this provider
alongside passive web and Electron page captures. Exact approved file or URL
mappings can correlate script presence with JavaScript Application Graph
alongside passive web and Electron page captures. Exact file or URL mappings
supplied with the request can correlate script presence with JavaScript Application Graph
assets. Because this provider never reads source bytes, matches normally use a
unique authorized location and remain weaker than captured-byte identity.
Builtin `node:` scripts remain runtime-only nodes unless a future explicit
+7 -9
View File
@@ -21,7 +21,7 @@ parameters, and normalized-result schemas are checked before reconciliation.
supplied as additional layers.
2. Run `inspect_web_page`, `inspect_electron_page`, or the attach-only
`observe_javascript_runtime`. Capturing script sources
is optional, but an approved source capture provides the strongest byte
is optional, but a source capture provides the strongest byte
identity.
3. Pass the resulting Evidence records to `reconcile_javascript_runtime`.
Browser URLs normally need an explicit URL-prefix mapping. Extracted
@@ -77,18 +77,16 @@ npm run verify:javascript-runtime -- \
--mapping '{"layer":1,"kind":"file-root","root":"/runtime/cache","artifact_prefix":""}'
```
The verifier analyzes the supplied static paths under a temporary in-process
permission policy, reads bounded Evidence JSON files, and prints graph IDs,
evidence IDs, paths, digests, counts, reasons, and coverage. It does not print
captured source text. `--source-map-read-approved` remains a distinct explicit
approval.
The verifier analyzes the supplied static paths, reads bounded Evidence JSON
files, and prints graph IDs, evidence IDs, paths, digests, counts, reasons, and
coverage. It does not print captured source text.
## Matching rules
Runtime targets, frames, scripts, and workers become capture-scoped graph
nodes. REA evaluates compatible static candidates conservatively:
1. An approved captured-source SHA-256 plus one matching mapped location yields
1. A captured-source SHA-256 plus one matching mapped location yields
a content-and-location match.
2. One unique exact content digest can match even when no location mapping is
available. Whole-asset digests and reconstructed module-factory source
@@ -144,8 +142,8 @@ EventEmitter activity, Electron IPC, or script-unload evidence. A unique
authorized file or URL location can still correlate script presence with a
static asset, but it does not prove module initialization or feature execution.
Source-map declarations and approved original-source reads stay under static
source-map authority. They are reported in `source_map_authority` but never used
Source-map declarations and original-source reads remain static evidence. They
are reported in `source_map_authority` but never used
as primary runtime byte matches. A source name or `sourcesContent` digest is not
silently equated with the generated bytes observed by CDP.
+65 -63
View File
@@ -1,26 +1,31 @@
# Managed-code analysis plan
This document turns
[ADR-0003](adr/0003-managed-code-evidence-and-provider-boundary.md) into an
implementation and verification plan. REA currently ships read-only PE/CLI
triage and exact identity through `inspect_managed_artifact` and
`rea inspect-managed-artifact`, plus file-backed metadata, signature,
method-body CIL, exception-region, call-edge, and field-access inspection
through `inspect_managed_members` and `rea inspect-managed-members`, and
declared ModuleRef/ImplMap/PInvoke and native implementation boundary inventory
through `inspect_managed_native_boundaries` and
`rea inspect-managed-native-boundaries`, plus obfuscation-resistant member
comparison through `compare_managed_members` and `rea compare-managed-members`,
managed/native export or function Evidence matching through
`verify_managed_native_boundaries` and `rea verify-managed-native-boundaries`,
decompiler reconstruction import through `import_managed_reconstruction` and
`rea import-managed-reconstruction`, and default-disabled runtime-correlation
admission planning through `plan_managed_runtime_correlation` and
`rea plan-managed-runtime-correlation`, plus static managed graph projection
through `project_managed_application_graph` and
`rea project-managed-application-graph`. Native-body bridge mapping and runtime
execution remain planned behavior. The current product inventory remains the
one in [`product-catalog.json`](product-catalog.json).
REA inspects .NET PE/CLI artifacts without loading or executing their code.
It can identify an assembly, inspect metadata and CIL, compare members across
builds, and connect declared native calls to supplied native-analysis evidence.
CIL is Common Intermediate Language, the instruction format used by managed
assemblies.
The seven shipped tools are:
| Task | MCP tool |
| --------------------------------------------------------------------------- | ----------------------------------- |
| Identify an artifact and its managed deployment form | `inspect_managed_artifact` |
| Inspect types, signatures, method bodies, calls, and field access | `inspect_managed_members` |
| List declared native calls and native implementation indicators | `inspect_managed_native_boundaries` |
| Compare members across builds and map build-local tokens | `compare_managed_members` |
| Check native call declarations against supplied export or function evidence | `verify_managed_native_boundaries` |
| Import decompiled code against verified static member identities | `import_managed_reconstruction` |
| Add managed findings to an application graph | `project_managed_application_graph` |
Each has a matching CLI command: replace underscores with hyphens and prefix
the name with `rea`, for example `rea inspect-managed-artifact`.
Native-body bridge mapping remains planned; managed runtime execution is not
part of the current tool set.
This guide describes the implementation and verification of
[ADR-0003](adr/0003-managed-code-evidence-and-provider-boundary.md). The canonical
tool inventory is [`product-catalog.json`](product-catalog.json).
## Analysis objective
@@ -32,8 +37,8 @@ collapsing them:
3. What source-like or behavioral structure can be reconstructed or inferred?
4. Which findings survive an exact-build check or a cross-build structural
comparison?
5. Which remaining questions require native analysis or a separately
authorized runtime experiment?
5. Which remaining questions require native analysis or runtime evidence that
static inspection cannot supply?
The ordinary workflow ends at question four. Static analysis never loads or
executes the target.
@@ -151,7 +156,7 @@ UTF-8 encoding, and lowercase hexadecimal output.
### Epistemic commitment
- authority: static bytes, reconstruction, structural inference, independent
validation, native provider, or separately authorized runtime;
validation, native provider, or future runtime observation;
- state: observed, inferred, unknown, or unavailable;
- confidence independent of authority;
- coverage and admitted/dropped counts;
@@ -216,31 +221,34 @@ The workflow produces separate observation, inference, validation, and unknown
tables. It may say that a method is a strong candidate for a role; it cannot
turn that role into the method's durable identity.
Cross-version matching is two-stage:
Cross-version matching first prefers exact CIL/signature identity. When that
does not match, it pairs an exact declared type, method name, and raw signature
before trying structural body shape. The exact-signature key uses names only
as part of that full tuple; names alone never select a pair. Duplicate tuples
remain ambiguous rather than being paired by token order.
1. Exact identity requires compatible artifact/module commitments and exact
raw CIL identity; no standalone complete-method-body digest is exposed.
2. Structural identity compares normalized signatures, the limited
decoded-CIL tuple fingerprint, constants, and bounded shape context. It
reports all candidates at the winning score and remains ambiguous when the
evidence does not distinguish them. The digest does not itself remap
metadata tokens.
For a matched method, an unavailable or partial body makes body-shape facets
unknown while preserving observed signature differences. Structural identity
compares normalized signatures, the limited decoded-CIL tuple fingerprint,
constants, and bounded shape context. It reports all candidates at the winning
score and remains ambiguous when the evidence does not distinguish them. The
digest does not itself remap metadata tokens.
Tokens are always remapped through observed structure. A caller cannot carry
`0x06001234` into a new MVID and assume it names the same method.
## Managed/native boundary rules
| Boundary | Managed observation | Native observation | Permitted link |
| ----------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| P/Invoke | Module, entry point, charset/calling-convention flags, declaring method | Import/export/symbol/function evidence from the selected deep provider | Exact declared-name/module link or qualified resolution inference |
| COM | Interop attributes, GUIDs, imported interfaces, method signatures | Native registration/vtable evidence when independently available | Identifier/signature inference with explicit environment limitations |
| C++/CLI | Managed declaration and implementation flags | Native body/function evidence | Only a provider-supported bridge observation; never token-as-address |
| ReadyToRun | Component/header and per-method CIL/native availability | Native section/function evidence | Authenticated image mapping with format/profile version |
| Unmanaged export | Export metadata/attribute when present | PE export and native thunk/function | Exact export identity plus provider-qualified address |
| Single-file host | Bundle entry/component identity | Host and native component evidence | Outer bundle plus component digest/extent commitment |
| Unity IL2CPP | Supported metadata entity with authenticated pairing | Generated native function/type evidence | Versioned IL2CPP mapping only; no invented CIL |
| Runtime-resolved native | API/constant/data-flow candidate | Loaded-module/symbol observation only under separate runtime execution authority | Static candidate remains inference until separately observed |
| Boundary | Managed observation | Native observation | Permitted link |
| ----------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| P/Invoke | Module, entry point, charset/calling-convention flags, declaring method | Import/export/symbol/function evidence from the selected deep provider | Exact declared-name/module link or qualified resolution inference |
| COM | Interop attributes, GUIDs, imported interfaces, method signatures | Native registration/vtable evidence when independently available | Identifier/signature inference with explicit environment limitations |
| C++/CLI | Managed declaration and implementation flags | Native body/function evidence | Only a provider-supported bridge observation; never token-as-address |
| ReadyToRun | Component/header and per-method CIL/native availability | Native section/function evidence | Authenticated image mapping with format/profile version |
| Unmanaged export | Export metadata/attribute when present | PE export and native thunk/function | Exact export identity plus provider-qualified address |
| Single-file host | Bundle entry/component identity | Host and native component evidence | Outer bundle plus component digest/extent commitment |
| Unity IL2CPP | Supported metadata entity with authenticated pairing | Generated native function/type evidence | Versioned IL2CPP mapping only; no invented CIL |
| Runtime-resolved native | API/constant/data-flow candidate | Loaded-module/symbol observation from an independently collected runtime source | Static candidate remains inference until separately observed |
Declared import inventory supports a bounded positive claim. Its absence does
not exclude dynamic resolution, generated code, protected code, native helpers,
@@ -397,25 +405,13 @@ The benchmark may prove that REA reproduces selected facts for that exact local
build. It cannot establish general support on its own; source-built conformance
remains the admission requirement.
## Runtime admission boundary
## Runtime behavior boundary
The later runtime track is not an extension flag on a static operation. Its
design must expose whether it attaches to an existing process, launches a new
process, loads an assembly, uses reflection, installs a debugger/profiler, or
instruments code. Each effect needs separate approval and exact-build checks.
At minimum, admission requires target SHA-256, MVID, normalized signature and
body/CIL commitment, CLR family, OS, architecture, tool version, scenario
limits, output policy, and cleanup ownership to match. Any mismatch fails before
the experiment. It must not contact real services/accounts or claim that an
instrumented path represents an ordinary launch unless that proposition is
independently tested.
The shipped `plan_managed_runtime_correlation` path admits only a
default-disabled, permission-gated experiment plan and records that no target
code was executed. Until a separate executor is designed and shipped, runtime
behavior questions remain explicit unknowns with suggested probes; static
support does not perform them.
Managed inspection does not execute assemblies or observe CLR internals.
Questions about actual behavior remain unknown until supported runtime
evidence is collected. A direct process capture can retain declared inputs,
outputs, filesystem and protocol activity for a target run; it does not prove
which managed methods executed or how the CLR resolved them.
## Delivery sequence
@@ -437,9 +433,15 @@ The managed-code track advances as reviewable pull requests:
PE/CLI corpus shipped through `npm run verify:managed`; optional BYO
`ilspycmd` real-tool oracle shipped through `REA_ILSPY_CMD_PATH`; dnSpy and
pinned Windows checks remain planned); and
8. separately authorized runtime-correlation admission planning (shipped; no
runtime execution);
9. managed static Evidence projection into the application graph (shipped).
8. managed static Evidence projection into the application graph (shipped).
Each implementation PR updates generated product facts only for behavior it
actually ships and states which real-tool checks were performed.
### Method metadata during build comparison
Managed member comparisons report a `metadata` dimension when an observed
MethodDef flags or implementation flags value differs, including accessibility
or synchronization changes. This is separate from the CIL/signature matching
tiers: identical instructions do not establish unchanged method metadata. The
reported difference is static metadata evidence, not proof of runtime behavior.
+51 -62
View File
@@ -8,16 +8,18 @@ registrations, reports their command vectors as aligned, stale, missing, or
invalid, and keeps live-server state `unknown` unless the active connection
supplies identity.
Canonical tool names remain stable, while `tools/list` advertises only the
operations callable for the current target, provider, policy, host, and
negotiated client capabilities. `binary_session.tool_availability` remains the
complete inventory: it explains advertised and hidden operations with stable
availability reasons and remediation. Each entry also reports required and
optional negotiated client features plus the currently missing features. Form elicitation is optional for
`capture_process_scenario`: an existing grant remains usable without it, while
a missing grant cannot be elicited by a client that does not advertise it.
Opening or closing a target, reloading policy, or observing a provider health
transition emits `notifications/tools/list_changed`.
`tools/list` returns the complete canonical tool inventory, including tools that
are currently unavailable. Opening or closing a target or observing a provider
health transition leaves that catalog unchanged and does not emit
`notifications/tools/list_changed`.
Call `binary_session` with `{}` and read `result.tool_availability` to choose a
callable operation for the current target, provider, host, and negotiated client
capabilities. The default result includes the complete inventory with each
tool's availability, reason, and remediation. Each entry also reports required
and optional negotiated client features plus the currently missing features.
The optional inputs `expected_package_version`, `expected_catalog_digest`, and
`expected_server_path` compare the live session with the caller's expectations.
`binary_session.analysis_provider_candidates` is authoritative for deep-engine
discovery. Target-free discovery is sorted by provider ID, reports host
@@ -57,8 +59,13 @@ shutdown rules still apply; REA never kills a process it cannot prove it owns.
## Tool results
Evidence-producing tools return their result and Evidence context inline. Read
that result directly. Use `get_evidence_bundle` when the task needs broader
Evidence-producing tools return `{ result, evidence_id, evidence }` in both
text and structured content. `evidence` is the complete canonical Evidence
record, including `normalized_result`, which equals `result`. The same record
is retained in the session bundle. Read `result` directly, or pass `evidence`
to a compatible comparison tool: `analyze_function` Evidence can be passed
directly to `compare_functions`, and `inspect_artifact` Evidence to
`compare_artifacts`. Use `get_evidence_bundle` when the task needs broader
retained session history or an explicit bundle for transfer.
## Aggregate native context
@@ -74,65 +81,47 @@ unsupported facets are local `unavailable` outcomes. Use `current_document`,
`current_address`, and `current_procedure` for direct single-field lookups; use
the aggregate tools when you need related context together.
## Permission policy
## Request scope and local effects
All local side effects use one scope vocabulary: capability, canonical roots,
executables, environment variable names, network mode, mount permission, exact
operation identity, and grant lifetime. Environment values and captured content
are never part of a grant or denial.
Each tool request names the target and lifecycle it will use. Browser calls
carry a loopback CDP endpoint and target ID, with optional origin filters;
process and Electron scenarios carry the executable, arguments, actions, and
cleanup behavior; artifact tools carry the input path and requested operation.
REA runs the declared request directly and does not infer a broader target or
action from it.
Existing environment settings map to administrator ceilings. They remain the
maximum authority:
REA does not require permission grants or per-call approval flags. Setup still
prints its plan and requires confirmation before changing configuration or
installing Hopper. MCP clients control their own confirmation UI.
- process roots, executables, environment names, and external networking;
- native mount enablement.
Tool annotations describe effects and are hints, not authorization controls
([MCP ToolAnnotations](https://modelcontextprotocol.io/specification/2025-06-18/schema#toolannotations)).
`readOnlyHint` includes session state: an analysis call that records additive
Evidence is marked non-read-only even when it leaves the target unchanged.
`destructiveHint` describes possible data loss, not ordinary Evidence recording.
Effect metadata covers possible behavior across supported inputs: DMG inventory
can launch `hdiutil` and create an owned temporary mount directory; extraction
creates a fresh output directory on every call. Comparing supplied web captures
or PNG artifacts uses local data without contacting the browser.
Evidence bundles and analysis snapshots use the path supplied to the CLI or MCP
tool; they do not require configured roots or separate read/write grants.
Host requirements remain in force. macOS may deny Accessibility,
Screen Recording, or native mounting; provider tools require their selected
analysis runtime. These failures are reported at the operation that needs
them. A configured provider or an endpoint alone does not establish that a
target is supported.
Artifact extraction takes no path or occurrence selectors. It materializes all
regular files into a fresh REA-chosen temporary directory. The write still
passes through the active permission evaluator; the tool caller does not choose
an output path.
Evidence bundles, snapshots, and extraction use the paths and output behavior
declared by their tools. Artifact extraction materializes the selected regular
files into a fresh REA-chosen temporary directory and reports its path.
`rea policy status`, `list`, `explain`, and `revoke` inspect the same evaluator
used by MCP. Optional project grants require both
`REA_PERMISSION_PROJECT_ROOT` and `REA_PERMISSION_PROJECT_STORE`. The store is
atomically written with mode `0600`, bound to the canonical project root, and is
never enabled by default. Send `SIGHUP` to the REA MCP process after a trusted
project-store change; project grants reload without restarting. Environment
settings belong to the process environment, so changing an administrator
ceiling in an MCP registration requires restarting that registered server or
its owning client.
Revocation affects future operations; an already-running operation retains the
decision made at its preflight boundary.
Once and session grants are connection-local overlays. They cannot authorize a
second MCP connection, be consumed by another connection, or be cleared when a
different connection closes. Live connections evaluate those overlays against
the current process-wide ceilings and persisted grants, including successful
SIGHUP reloads.
`rea policy revoke <grant-id>` displays the exact grant and requires interactive
confirmation; automation must pass `--yes` (or `-y`) explicitly.
Denials use the shared `permission_required` schema with requested scope, missing
scope, administrator ceiling, elicitation support, and exact restart status.
Client-provided roots are context only and never grants.
Elicitation can add a once or session grant only inside an existing
administrator ceiling. A request outside that ceiling reports
`elicitation_supported: false` and `restart_required: true`; interactive consent
cannot silently widen administrator policy.
`analyze_javascript_application` reads the supplied local directory or ASAR path directly and returns its result and Evidence inline; no approval flag or root configuration is needed.
`analyze_javascript_application` reads the supplied local directory or ASAR path
and returns its result and Evidence inline.
## Integrity record-and-continue
Artifact integrity remains fail-closed by default. Record-and-continue requires
all three conditions:
1. operator policy `REA_ARTIFACT_INTEGRITY_CONTINUE_ENABLED=true`;
2. `integrity_policy=record-and-continue`;
3. explicit per-call `integrity_continue_approved=true`.
Artifact integrity fails closed by default. A request can explicitly select
`integrity_policy=record-and-continue` when the investigation needs verified
siblings to continue after a mismatch.
Contradictory bytes are quarantined from nested expansion and recorded with
declared and observed hashes, trust, provenance, path, and unpacked state.
+4 -6
View File
@@ -13,7 +13,7 @@ invoke tools by themselves.
| `verify_reconstruction` | Evaluate a finite reconstruction specification against retained comparison Evidence. |
| `trace_crash` | Correlate a crash symptom with static paths and optional process capture evidence. |
| `audit_residual_unknowns` | Audit current residual-unknown heads and evidence-qualified resolution. |
| `prepare_bounded_process_capture` | Design an approval-gated controlled process experiment before execution. |
| `prepare_bounded_process_capture` | Design a bounded controlled process experiment before execution. |
Every rendered prompt provides optional starting points using current REA tool
names. Agents can call tools directly, skip irrelevant suggestions, and inspect
@@ -108,8 +108,6 @@ session are no longer suggested. Provider errors, unsupported operations, and
malformed provider output produce an empty completion list rather than an
unverified identifier.
Suggestions never grant permission. In particular, selecting an artifact
occurrence does not authorize extraction, selecting an unknown does not
authorize mutation, and selecting a prior capture does not authorize process
execution. The corresponding tools retain their existing approval, policy,
effect, and validation boundaries.
Suggestions are starting points, not tool calls. A tool acts on the target,
operation, and lifecycle fields in its own request; selecting a suggestion does
not execute it or broaden those fields.
+162 -67
View File
@@ -1,78 +1,173 @@
# Native UI and dispatch investigation
REA combines decoded application resources, provider metadata, and native code
analysis into evidence-linked investigation graphs. Each layer has a different
coverage boundary; a graph edge is not automatically proof that a runtime path
executes.
REA joins static resources, native metadata and code facts while keeping runtime
observations separate. Every result reports its evidence, target identity,
coverage and unknowns. The CLI and MCP use the same application workflows.
## Current workflow
## Static inspection
1. `decode_interface_builder` reads compiled Apple storyboard and nib resources
from an artifact graph and returns scenes, objects, outlets, actions,
connections, and available controller/class names. A source-owned AppKit
XIB is compiled by `ibtool` in `verify:interface-builder` and checked for
its hierarchy and target/action edge. Storyboard compilation still depends
on an installed iOS platform; unsupported archive objects remain explicit.
2. `inspect_native_dispatch_metadata` normalizes supported provider name
inventory into typed Objective-C and Swift records. Current extraction is
symbol-name based: symbolized Objective-C method implementations and
mangled Swift names can be reported as partial evidence. It does not yet
decode Objective-C runtime sections, ivar layouts, protocol conformance
records, Swift witness tables, vtables, relative pointers, or closure
thunks from binary metadata.
3. `trace_native_ui_action` accepts one selector or Interface Builder object
ID. REA decodes the active app and reads names from the bound native provider,
verifies that both observations refer to the same target digest, joins an
authored action to a symbolized Objective-C implementation, and follows
bounded direct callees. Ambiguous selectors and unresolved calls remain
explicit. This is a static candidate route, not proof of runtime reachability;
it does not resolve Swift witnesses, vtables, closures, or cross-function
value flow yet. The CLI uses the same query: `rea trace-native-ui-action
<app-or-binary> <selector-or-object-id>`.
`inspect_native_dispatch_metadata` and `rea inspect-native-dispatch-metadata`
expose the provider's typed symbol-derived inventory separately.
4. Ghidra `analyze_function` returns a bounded high-p-code def-use view for the
selected function. This can support manual state/value analysis, but REA
does not yet compose those views into an automatic cross-function slice.
- `inspect_asset_catalog` / `rea inspect-asset-catalog <app>` reads compiled
`Assets.car` metadata through macOS `assetutil --info`. Catalog digests, raw
rendition fields, pagination and exact UI resource-name matches are returned.
Image extraction and undocumented rendition interpretation are unsupported.
- `inspect_keyed_archive` / `rea inspect-keyed-archive <plist>` reads XML or
binary Foundation keyed archives without instantiating classes. For an active
app, provide its relative archive path as the second CLI argument or MCP
`path`. Results retain original `$objects` indices, named `$top` roots,
shared/cyclic references, class descriptors, raw serialized fields and
malformed/unresolved/nil references. Missing fields stay absent; unknown
classes are preserved. Select `root`, `offset` and `limit` explicitly.
UID 0 cannot distinguish conditional nil from ordinary nil. Limits are
64 MiB of input, 200,000 objects/references and 128 nesting levels.
- `decode_interface_builder` / `rea decode-interface-builder <app>` reads
compiled storyboard and nib resources into objects, outlets, actions,
connections and controller/class names. `verify:interface-builder` compiles
a source-owned AppKit XIB with `ibtool`. Storyboards require an installed
iOS platform; unsupported archive forms remain explicit.
- `inspect_native_dispatch_metadata` / `rea inspect-native-dispatch-metadata
<app-or-binary>` prefers a validated macOS Mach-O byte reader. It decodes
64-bit little-endian Objective-C class/metaclass records, superclass pointers,
absolute/relative method entries, ivar offsets/sizes/alignment and protocol
declarations. It also decodes simple Swift conformances, static synchronous
witness slots, signed-relative pointers and non-generic, non-resilient class
vtable descriptors. Records include exact virtual addresses/file offsets and
artifact evidence. Vtable indexes are metadata word offsets; witness indexes
start after the conformance header. Names unavailable in metadata remain null.
Chained fixups, external bindings, authenticated pointers, categories,
properties, generic/resilient/async/coroutine tables and inherited overrides
have explicit unsupported or partial coverage. Other providers retain the
existing symbol-based inventory with its narrower coverage.
## Ghidra p-code result boundary
## Native instruction, call and type primitives
The dossier retains up to 3,000 p-code operations, up to 64 inputs per
operation, and 12,000 def-use edges. Each operation carries its address,
sequence, opcode, input/output varnodes, and whether Ghidra marks it dead.
Memory reads/writes, conditional and indirect branches, and direct/indirect
calls are reported by opcode with operand indexes. Results expose truncation,
an omitted-operation lower bound, and known omitted-input and omitted-edge
counts.
Ghidra supplies three exact-object operations:
These are decompiler-derived static observations. Def-use edges stop at a
function boundary; memory aliasing, call side effects, unresolved dispatch,
runtime behavior, and application-level meaning require additional evidence.
A p-code `STORE` can describe stack memory and does not by itself identify
persistent application state.
```bash
rea inspect-native-instruction <binary> <address> --provider ghidra
rea resolve-native-call-targets <binary> <call-site> --provider ghidra
rea inspect-native-data-type <binary> --type /MyStruct --provider ghidra
rea inspect-native-data-type <binary> --address <typed-data-address> --provider ghidra
```
Recovered jump-table load sources and case-to-target mappings are reported as
separate facts. On AArch64, REA additionally decodes the verified byte-indexed
relative branch-table form when the selector's unsigned upper bound, `LDRB`
table load, `ADR` branch base, scaled `ADD`, indirect `BR`, and recovered
target set agree. It reads only the proven entry count and reports one direct
mapping per selector value. Other layouts retain unknown case values and
unknown table bounds when Ghidra does not provide aligned labels or load-table
metadata; REA does not extrapolate from neighboring targets.
Instruction facts include bytes, length, decoder text, ordered register/scalar/
address tokens, typed references and flow destinations. Mid-instruction, data,
outside-memory and undecodable addresses have separate outcomes. Effective
memory base/index/displacement roles and per-instruction context mode remain
unavailable; `mode` is the program language variant.
## Provider admission and remaining work
Call resolution reports direct, resolved indirect, ambiguous, unresolved and
non-call outcomes from static call references. It does not establish runtime
execution or classify Objective-C/Swift/vtable/closure mechanisms from names.
Ghidra is admitted on Linux x64; the experimental Windows x64 P0 is limited to
approved native PE applications. macOS is not an admitted Ghidra host. The
optimized AArch64 ELF byte-table fixture has a separate lane so its acceptance
does not depend on PE or Mach-O cross-linkers.
Type inspection selects one exact database pathname or typed data address.
Struct/union fields, enums, pointers, arrays, typedefs, size, alignment, packing
and bitfields retain database authority. Child types use exact IDs for further
inspection, including recursive layouts. Source/debug authority and flexible
array semantics are not inferred. The real lane uses a native DWARF 4 object
with a struct containing a union, enum and recursive pointer; a stripped linked
binary may have no corresponding recovered layout.
The native feature set is still partial. Objective-C/Swift metadata currently
uses symbol names rather than runtime sections, witness tables, vtables, or
relative pointers. UI action traces do not yet resolve Swift dispatch or
closure/thunk routes. Value dependencies remain intra-function, and the
AArch64 switch lane currently proves one optimized byte-indexed ELF layout;
sparse, stripped, Mach-O, and other table patterns remain unknown. The local
Xcode installation has no iOS simulator platform, so compiled storyboard
acceptance could not run here. These boundaries should stay visible until
matching provider-backed readers and fixtures exist.
## Bounded static traces
`trace_native_ui_action` / `rea trace-native-ui-action <target> <seed>` accepts
one authored selector/object ID, native symbol or exact function address. It
joins UI wiring to uniquely matched class/selector implementations and follows
bounded typed call references. Direct references, resolved indirect references,
ambiguous candidates, inferred untyped provider callees and targetless call
sites remain distinct. A static route does not establish runtime reachability.
Swift/closure dispatch requires resolved static references; unavailable ABI
forms remain unknown.
`trace_native_values` / `rea trace-native-values <binary> <procedure> --provider
ghidra` composes high-p-code def-use graphs across statically resolved calls.
Logical CALL arguments bind to recovered parameter ordinals; recovered RETURN
values bind to CALL outputs. These edges are decompiler-derived. Constants,
operators, LOAD/STORE and branch operands are included inline. Alias effects,
persistent state, RNG roles, missing/variadic bindings and ambiguous destinations
remain unknown. Budgets control depth, decompilations, call-site resolutions,
nodes and edges. Node payloads are bounded to 8 MiB and serialized results to
32 MiB. Pagination returns edges whose source is on the node page and preserves
stable IDs for endpoints outside that page.
The raw Java `is_dead` flag can remain set after Ghidra decodes a live block;
`block_membership` reports actual syntax-tree membership separately.
The provider dossier itself retains up to 3,000 p-code operations, 64 inputs per
operation and 12,000 def-use edges, with explicit omitted counts. A `STORE`
may describe stack memory and does not itself prove persistent state.
Jump-table evidence keeps numeric `mappings`, explicit `default_targets`, and
backing `data_sources` separate. A null case value denotes an unresolved
case, never a known default. Ghidra pairs typed case/default tokens with the
recovered block entry and its unique indirect dispatch predecessor; it does
not zip unequal label and destination arrays. Shared case bodies retain each
label. Negative numeric tokens are checked against their encoded magnitude;
nonliteral, ambiguous, or nonexact JSON integer labels remain unknown.
Unknown-label diagnostics retain the token text, encoded unsigned magnitude,
target and dispatch so callers can inspect the original observations.
Decompiler load-table metadata supplies observed entry sizes and counts
without assigning every backing table to each mapping. These observations
describe the decompiler's recovered switch, not guaranteed original source.
Legacy records without `default_targets` normalize to an empty array and
retain their existing unresolved mappings.
AArch64 jump-table recovery additionally verifies byte and halfword relative
forms from unsigned bounds, register definitions, table loads, branch bases,
scaled ADD/BR instructions and the recovered target set. It reads exactly the
proven count and preserves unknowns for other forms. Real ELF and host ARM64
Mach-O fixtures check each case against source-owned return values.
## Native desktop observation
`observe_native_ui` captures one explicitly selected existing PID/window ID.
Screenshots use a selected-window ScreenCaptureKit
filter on macOS 14+; accessibility reads stay within that window. Missing Screen
Recording/Accessibility permissions produce actionable errors without broad
capture or automatic permission prompts. Executable bytes and process launch
time guard against a different target or PID reuse. AX selection requires one
unique geometry match; ambiguity fails closed.
`capture_native_ui_scenario` takes AX child-index paths for press, increment/
decrement scrolling and text-value entry, or bounded waits. No global event
injection is used. Unsupported AX actions fail explicitly. The result preserves
ordered before/after captures and gaps; an action may have occurred before a
post-action capture fails. Application state is left as-is; REA does not attempt
to restore it.
Scenarios accept caller-selected action lists and accessibility node counts.
Individual waits cannot exceed the operation's 180-second deadline, and the
complete scenario result has a 64 MiB output budget. Screenshots are scaled to
at most 2,048 pixels and captured only for the selected window. REA compiles one
owned helper per scenario, removes its temporary compiler cache and stops its
helper on cancellation. It does not launch or own the selected application. UI
actions may change application data or trigger network activity.
## Provider and verification boundaries
Install Ghidra 12.1.4 and a full 64-bit JDK 21 separately, then configure REA to
use them. Ghidra analysis supports Linux x64 and macOS x64/arm64; macOS requires
the matching native decompiler. Experimental Windows x64 P0 admits native
x86-64 PE applications on local NTFS using bundled Job Object ownership,
protected runtime DACLs, and handle-based path admission. See [Windows Ghidra P0](windows-ghidra-p0.md) and
[issue #527](https://github.com/morluto/rea/issues/527).
On Linux and macOS, `annotate_native_function` atomically edits a function name
and entry comments in the ephemeral database, returning refreshed analysis
without changing executable bytes. Windows P0 remains read-only. Ghidra has no
GUI authority, and REA never falls back automatically to Hopper.
- `npm run verify:ghidra`: host-native debug/stripped targets, native type layout,
instruction/call facts, value dependencies and process/project cleanup.
- `npm run verify:ghidra:aarch64-jump-table`: optimized ELF and byte/halfword
relative tables, plus ARM64 Mach-O on an ARM64 macOS host.
- `npm run verify:apple-dispatch`: source-built Objective-C protocols/classes and
Swift conformances/vtables, repeated after stripping local symbols.
- `npm run verify:native-ui`: one source-owned fixture window, successful
selected-window capture and actions, changed-target rejection, and cleanup.
Missing OS permissions fail this lane.
- `npm run verify:native-ui:permissions`: permits a permission-denial result and
reports `positive_e2e: false` when capture is denied. That result verifies the
OS permission boundary, not successful UI capture or actions.
macOS ARM64 is the real host verified during this implementation. Admission of
macOS Intel does not claim an Intel verification run. Unsupported metadata and
unresolved runtime/value semantics remain visible in results.
+64 -243
View File
@@ -1,79 +1,34 @@
# Process capture
Process Capture records one approved command as deterministic Evidence. It is
intended for authority-versus-reconstruction checks where terminal behavior,
child lifetime, filesystem state, or dependency timing matters.
Process Capture runs one caller-selected command and records bounded evidence
about its terminal behavior, scheduled interactions, process lifetime, and
selected filesystem state. Use it to compare two direct runs of the same
scenario, such as an authority build and a reconstruction. It does not replay a
previous execution or emulate dependencies.
The harness records both raw PTY chunks and terminal states rendered by xterm.
It also records scripted input, resize and signal delivery, sampled process
metadata, named filesystem checkpoints, command-shim invocations, loopback
HTTP/WebSocket exchanges, and process exit ownership. Missing or bounded
observations remain explicit; a truncated capture is never treated as
equivalent to another capture.
The command runs with the current user's permissions and inherits the host
environment before applying the scenario's explicit overrides. This is not a
sandbox. Filesystem observation paths select what REA snapshots; they do not
restrict what the process can read or write. The inherited environment is not
recorded, so its influence may remain unknown.
Every capture includes a run manifest with canonical SHA-256 commitments for
the secret-safe full scenario projection, comparison contract, executable,
normalization rules, command-shim plan, and replay plan. The manifest also
records the REA/provider versions, platform, architecture, PTY backend, and UTC
start/completion timestamps. Capture creation, Evidence import, and comparison
recompute the self-contained commitments and reject invalid lifecycle data.
## Capture a command
## Enable the capability
Write a JSON scenario and pass its path to the CLI:
Process execution is disabled until the operator supplies policy roots and
approves host networking:
```bash
export REA_PROCESS_CAPTURE_ENABLED=true
export REA_PROCESS_EXECUTABLE_ROOTS_JSON='["/usr/bin","/absolute/project/bin"]'
export REA_PROCESS_WORKING_ROOTS_JSON='["/absolute/project"]'
export REA_PROCESS_ALLOWED_ENV_JSON='["PATH"]'
export REA_PROCESS_ALLOW_EXTERNAL_NETWORK=true
```
By default, enabling process capture also installs an administrator-lifetime
grant for the configured boundary. Set
`REA_PROCESS_CAPTURE_AUTO_GRANT=false` to install the same boundary only as an
administrator ceiling. Captures then fail with a structured permission-required
result until a narrower grant is established; this is the prerequisite mode for
connection-scoped MCP elicitation. It does not itself authorize a capture.
Each MCP connection owns its elicited once and session grants. A grant accepted
on one connection is not visible to another connection, and disconnect cleanup
removes only the grants owned by the connection that closed. Reloaded
administrator ceilings and persisted grants still apply immediately to every
live connection.
REA offers the once-or-session grant form only after the connection negotiates
an MCP revision with multi-round tool results and advertises form elicitation.
Older connections keep returning the structured
permission-required result and never wait for an unsupported elicitation
response. On the pinned `@modelcontextprotocol/server` 2.0.0 runtime, this is
the official `2026-07-28` `inputRequired` continuation: the SDK seals the
request state and validates the continuation envelope, while REA validates the
untrusted input response before creating a connection-owned grant. The older
push-style `elicitation/create` path is not used for this protocol revision.
Executables, working directories, filesystem roots, and requested environment
variables are checked against operator policy before launch. Process Capture is an observation tool,
not a security sandbox: the target runs with the current user's permissions.
## Capture a scenario
`rea capture-process` reads a JSON scenario and writes process capture
Evidence to stdout:
```bash
```sh
rea capture-process ./scenario.json > capture.json
```
A representative scenario is:
For example:
```json
{
"executable": "/absolute/path/to/node",
"executable": "node",
"arguments": ["./signup.mjs"],
"working_directory": "/absolute/project",
"filesystem_roots": ["/absolute/project/state"],
"working_directory": ".",
"environment": { "APP_MODE": "test" },
"filesystem_observation_paths": ["./state"],
"terminal": { "columns": 80, "rows": 24, "scrollback": 1000 },
"events": [
{
@@ -82,197 +37,63 @@ A representative scenario is:
"data": "user@example.test\r",
"sensitive": true
},
{ "type": "resize", "at_ms": 500, "columns": 100, "rows": 30 }
{ "type": "resize", "at_ms": 500, "columns": 100, "rows": 30 },
{ "type": "signal", "at_ms": 1000, "signal": "SIGINT" }
],
"checkpoints": [
{
"name": "signup_ready",
"trigger": { "type": "terminal_literal", "value": "All set!" }
},
{ "name": "root_exited", "trigger": { "type": "root_exit" } }
],
"command_shims": [
{
"name": "codex",
"routes": [
{
"arguments": ["--version"],
"outputs": [
{ "at_ms": 0, "stream": "stdout", "data": "codex 1.2.3\n" }
],
"termination": { "type": "exit", "code": 0 },
"max_calls": 1
}
]
}
]
"timeout_ms": 30000,
"idle_timeout_ms": 30000,
"settle_ms": 100
}
```
Input events marked `sensitive` are dispatched to the PTY but stored only as a
byte-count placeholder. Environment values listed in `secret_aliases` are also
redacted from recorded text. The command-shim directory is placed first in the
captured process `PATH`; any remaining `PATH` must be explicitly supplied or
inherited under operator policy.
`executable` is required. `arguments`, `working_directory`, `environment`,
`filesystem_observation_paths`, terminal settings, timed `events`, timeouts,
resource limits, and normalization settings have defaults; see
`processScenarioSchema` for the exact contract. Event times are milliseconds
from launch, must be ordered, and must fall within `timeout_ms`. Inputs marked
`sensitive` are sent to the process but persisted as a byte-count placeholder.
Environment overrides are recorded in the scenario commitment; inherited
values are not copied into Evidence.
Scenario arguments, environment names, timed interactions, shim routes and
output chunks, filesystem checkpoints, and static HTTP/WebSocket scripts are
accepted without fixed item-count ceilings. The run still obeys its timeout
and the configured output-byte, process, filesystem, protocol-event, body-byte,
and connection budgets; each filesystem checkpoint uses those same
bounded snapshot limits.
The capture contains raw PTY output chunks and rendered terminal states,
interaction dispatch outcomes, exit reason, sampled process-tree observations,
and settlement status. When filesystem paths are selected, REA records
`files_before` and `files_after`, then classifies observed entries as created,
deleted, modified, or unchanged. These are bounded snapshots, not a syscall
trace; short-lived changes between snapshots may be missed. With no selected
paths, filesystem effects remain unknown.
## Reactive process scenarios
Output, file count, file size, process sampling, filesystem depth, total
runtime, idle time, and post-exit settlement are bounded by the scenario's
limits. The result marks truncated observations and residual unknowns rather
than treating missing data as proof of equivalence. Cancellation and timeout
run the same owned-process cleanup path. Settlement reports whether the
sampled process group quiesced or whether cleanup was needed or unverifiable;
sampling cannot prove that every short-lived or detached descendant was seen.
Set `reactive` to a process reactive scenario declaration when interaction
must follow observed output instead of guessed delays. The graph declares an
initial state, absolute scenario and state deadlines, explicitly prioritized
transitions, caller-chosen use and visit counts, trigger predicates, ordered
actions, and either a next state or `passed` outcome.
## Compare two captures
The admitted runtime slice matches decoded UTF-8 terminal literals and exact
`terminal_raw`, `interaction`, `process`, `filesystem`, `http`, `websocket`, or
`shim` journal events. Process events are sampled observations, not an
event-complete lifecycle. Filesystem events identify named checkpoints; shim
events identify a command and route. Live predicates receive the same normalized
and redacted payload later persisted in Evidence. Predicates may compose with
`all`, `any`, `sequence`, and `repeat` nodes and may select a frontier at
scenario start, state entry, a prior checkpoint, or an exact event ID. Actions
can write PTY input, resize it, signal the root process, or capture a named
filesystem checkpoint. `terminal_rendered`, `lifecycle`, and `replay_transition`
event triggers and non-root signal selectors fail during preflight before the
target launches.
Compare saved capture Evidence with:
Reactive results are recorded in `reactive_run`, including the terminal outcome,
active state, matched event IDs, action evidence IDs, and deterministic
transition journal. The reducer retains and evaluates every admitted
observation through the scenario deadline; it has no fixed state, transition,
predicate, action, repeat, or history-count ceilings. Recursive input depth has
a stack safety guard. Terminal controls also commit the last admitted
observation order, so later output cannot be moved ahead of a timeout,
cancellation, or target loss during validation. If the process exits before the graph finishes,
the outcome is `target_lost`; state/scenario deadlines remain distinct
`predicate_timeout`/`scenario_deadline` outcomes. Timed `events` remain
available for fixed schedules, but they are not silently translated into the
reactive graph.
## Checkpoints and deterministic shims
Checkpoint triggers may use an elapsed time, a terminal literal and occurrence
count, root exit, or settlement. REA always includes `before` and
`after_settlement` snapshots. Snapshot effects are relative to the preceding
checkpoint, so transient files remain visible even when the final tree matches
the initial tree.
Each command shim matches an exact argument array. Routes can emit timed stdout
or stderr chunks and then exit or receive `SIGINT`, `SIGTERM`, or `SIGKILL`.
Unmatched and exhausted calls are recorded. Shim observations are included in
the capture without a separate fixed count ceiling. Other configured
observation and byte budgets remain explicit in the capture result when they
affect completeness.
## Run a replay machine directly
Use `rea run-replay-machine ./run.json` to validate a finite replay machine
against an ordered `events` array without launching a process or opening a
socket. MCP clients use `run_replay_machine` with the same `{ machine, events }`
input. The result retains one decision per offered event, a transition journal
with capture alias metadata, and one redacted action-table entry per used
transition. It never returns request bodies, request headers, or captured
values. It also reports the initial and final states, whether the final state is
terminal, configured limits, and committed usage.
The direct evaluator has no fixed state, transition, event, guard, capture,
action, or action-byte count ceiling. It indexes transitions by protocol and
path, evaluates events in order, and returns a decision for each offered event
plus actions for used transitions. Caller-declared `max_uses`, `max_visits`,
`max_transitions`, and protocol budgets determine the finite run; there is no
upper schema cap on those values. Result size follows the input and is not
truncated by an arbitrary item quota.
Refused events remain data rather than aborting the run. Outcomes distinguish
unmatched events, invalid states, failed guards, exhausted transitions, invalid
captures, unexpected reconnects, and exhausted limits. This direct runner
evaluates the same domain runtime used by loopback Process Capture, but performs
no network or target execution.
## Compare captures
Compare two saved Evidence records with:
```bash
```sh
rea compare-process-captures authority.json reconstruction.json
```
Pass an optional third path, `rea compare-process-captures authority.json
reconstruction.json trace.json`, when valid executions may differ in declared
concurrent ordering. MCP callers pass the same object as `trace_spec` to
`compare_process_captures`. A trace specification declares exact named events
and chooses one language:
The comparison checks terminal, interaction, exit, filesystem, and
process observations under a shared comparison contract. It reports observed
differences with their locations, while residual unknowns or truncated
observations prevent a complete-equivalence claim. Matching scenario
commitments do not reveal whether redacted sensitive inputs were equal.
- `partial_order` requires every event pair to be related by a
`happens_before` edge (including transitive edges) or an explicit
`unordered_groups` declaration. `not_before` expresses negative ordering
constraints without inferring causality. Optional `prefix` and `suffix`
arrays remain exact.
- `finite_traces` lists the complete accepted variants. An `unordered` token
accepts permutations of exactly its declared event multiset; it does not
drop or broadly sort events.
For scenarios where independent scheduling can change event order, the CLI
also accepts an optional trace-specification JSON file as the third argument.
That specification must state the exact events and ordering constraints to
accept; it does not discard unmatched observations or make timestamps into
causal evidence. Trace comparison returns `unknown` when a capture lacks the
required complete event journal or contains relevant residual unknowns.
Each event uses one source family (`terminal_raw`, `terminal_rendered`,
`interaction`, `lifecycle`, `process`, `filesystem`, `http`, `websocket`,
`shim`, or `replay_transition`), an exact JSON payload, and a discriminated
cardinality: `required`, `optional`, `exact`, or `range`. Specifications with
duplicate or overlapping predicates, unknown references, cycles, implicit
ordering gaps, ordered/unordered conflicts, or finite variants that violate
cardinality are rejected before comparison.
Event payloads are exact by default. To compare schedules whose recorder
metadata necessarily changes, an event may explicitly list top-level
`ignore_fields` from the bounded set `sequence`, `at_ms`, `scheduled_at_ms`,
`dispatched_at_ms`, and `elapsed_ms`; its `exact` object must omit those fields.
All predicates for one source use the same ignore set, so relaxed metadata
cannot create declaration-order matching ambiguity. Other payload fields are
never dropped or normalized by trace comparison.
Process Capture records a monotonic `event_journal` at observation time. Trace
comparison uses only this capture order for cross-source causality; timestamps
are retained as event data but never promoted into happens-before evidence.
Older captures without a complete journal, truncated captures, and relevant
residual unknowns return a trace verdict of `unknown`. A passing result retains
each side's raw journal locations and identifies either the matched finite
variant or the complete satisfied constraint set. A failure returns the first
declaration-ordered predicate, cardinality, edge, prefix, suffix, or language
violation with its relevant event locations.
If both captures have the same invalid declared trace, the nested verdict is
`nonconforming` while the top-level capture comparison remains unchanged. Side
statuses and the diagnostic distinguish conformance from pairwise difference.
The result classifies terminal, interaction, exit, filesystem, protocol,
process, and shim behavior. `first_divergence` points to the earliest observed
difference. Residual unknowns prevent a claim of complete equivalence, but they
do not hide an observed difference in another dimension.
Captures must have equal comparison-contract commitments, but may identify
different scenarios and executables. MCP callers may set `max_capture_age_ms`;
the application clock then rejects stale captures before comparison.
## Limits and lifecycle behavior
Scenarios bound output bytes, frames, files, file bytes, process observations,
protocol/shim events, connections, filesystem depth, total runtime, idle time,
and settlement time. Process trees are sampled rather than syscall-traced, and
short-lived descendants may be missed. Filesystem observations are snapshots,
not filesystem event streams.
REA tags launched processes with a private run identifier. During cleanup it
revalidates that identifier before signaling sampled process groups, including
groups detached from the original parent. Temporary HOME, replay, shim,
terminal, checkpoint, and sampling resources are released on success, failure,
timeout, and cancellation.
After root exit, REA checks token-owned process groups every 50 ms. Two
consecutive empty observations establish `quiesced`; the settlement deadline
otherwise produces `alive_at_deadline`, while inspection or identity failures
produce `unverifiable`. Sampling can still miss short-lived or early-detached
descendants, which remains a residual unknown.
Each capture carries commitments for the selected scenario, executable,
comparison contract, and normalization rules. Comparison rejects captures
whose comparison contracts differ. Captures are local Evidence files; keep
their source artifacts and invocation context available when interpreting a
difference.
+90 -43
View File
@@ -1,30 +1,34 @@
{
"package": {
"name": "rea-agents",
"version": "3.2.1",
"version": "4.0.1",
"sdk": {
"server": "2.0.0",
"client": "2.0.0",
"core": "2.0.0"
"server": "2.3.1",
"client": "2.3.1",
"core": "2.3.1"
},
"skill_version": "23"
"skill_version": "24"
},
"tools": {
"total": 114,
"total": 118,
"families": [
{
"id": "direct",
"surface": "official-proxy",
"count": 36,
"count": 41,
"tools": [
"address_name",
"address_to_file_offset",
"annotate_native_function",
"comment",
"current_address",
"current_document",
"current_procedure",
"goto_address",
"inline_comment",
"inspect_native_data_type",
"inspect_native_instruction",
"inspect_native_load_image",
"list_bookmarks",
"list_documents",
"list_names",
@@ -43,6 +47,7 @@
"read_bytes",
"read_function_instructions",
"resolve_containing_procedure",
"resolve_native_call_targets",
"search_procedures",
"search_strings",
"set_address_name",
@@ -64,7 +69,6 @@
"analyze_swift_types",
"batch_decompile",
"binary_overview",
"find_code_for_string",
"find_xrefs_to_name",
"get_call_graph",
"get_objc_classes",
@@ -73,42 +77,46 @@
"inspect_native_dispatch_metadata",
"trace_call_path",
"trace_feature",
"trace_native_ui_action"
"trace_native_ui_action",
"trace_native_values"
]
},
{
"id": "native",
"surface": "native-provider",
"count": 5,
"count": 7,
"tools": [
"capture_native_ui_scenario",
"demangle_swift",
"inspect_macho",
"inspect_plist",
"inspect_signature",
"list_architectures"
"list_architectures",
"observe_native_ui"
]
},
{
"id": "artifact",
"surface": "artifact-provider",
"count": 3,
"count": 5,
"tools": [
"decode_interface_builder",
"extract_artifact",
"inspect_artifact"
"inspect_artifact",
"inspect_asset_catalog",
"inspect_keyed_archive"
]
},
{
"id": "managed",
"surface": "managed",
"count": 8,
"count": 7,
"tools": [
"compare_managed_members",
"import_managed_reconstruction",
"inspect_managed_artifact",
"inspect_managed_members",
"inspect_managed_native_boundaries",
"plan_managed_runtime_correlation",
"project_managed_application_graph",
"verify_managed_native_boundaries"
]
@@ -153,16 +161,13 @@
{
"id": "application",
"surface": "application-workflow",
"count": 10,
"count": 7,
"tools": [
"build_reconstruction_obligation_ledger",
"compare_application_versions",
"compare_javascript_export_shapes",
"compare_source_to_bundle",
"evaluate_reconstruction_coverage",
"execute_node_characterization",
"prepare_node_characterization",
"run_controlled_replay",
"trace_application_feature",
"trace_javascript_semantics"
]
@@ -170,7 +175,7 @@
{
"id": "session",
"surface": "session",
"count": 22,
"count": 21,
"tools": [
"binary_session",
"build_call_path",
@@ -190,7 +195,6 @@
"list_unknowns",
"open_binary",
"record_unknown",
"run_replay_machine",
"update_unknown",
"verify_reconstruction",
"verify_unknown_resolution"
@@ -205,7 +209,12 @@
"version": null,
"capabilities": [
"address_name",
"address_to_file_offset",
"analyze_function",
"annotate_native_function",
"inspect_native_data_type",
"inspect_native_instruction",
"inspect_native_load_image",
"list_documents",
"list_names",
"list_procedures",
@@ -218,8 +227,10 @@
"procedure_info",
"procedure_pseudo_code",
"procedure_references",
"read_bytes",
"read_function_instructions",
"resolve_containing_procedure",
"resolve_native_call_targets",
"search_procedures",
"search_strings",
"xrefs"
@@ -274,11 +285,13 @@
"name": "macOS native inspection utilities",
"version": null,
"capabilities": [
"capture_native_ui_scenario",
"demangle_swift",
"inspect_macho",
"inspect_plist",
"inspect_signature",
"list_architectures"
"list_architectures",
"observe_native_ui"
]
},
{
@@ -288,7 +301,9 @@
"capabilities": [
"decode_interface_builder",
"extract_artifact",
"inspect_artifact"
"inspect_artifact",
"inspect_asset_catalog",
"inspect_keyed_archive"
]
},
{
@@ -329,7 +344,6 @@
"capabilities": [
"compare_managed_members",
"import_managed_reconstruction",
"plan_managed_runtime_correlation",
"project_managed_application_graph",
"verify_managed_native_boundaries"
]
@@ -350,9 +364,6 @@
"compare_javascript_export_shapes",
"compare_source_to_bundle",
"evaluate_reconstruction_coverage",
"execute_node_characterization",
"prepare_node_characterization",
"run_controlled_replay",
"trace_application_feature",
"trace_javascript_semantics"
]
@@ -425,20 +436,53 @@
{
"id": "devin",
"display_name": "Devin",
"format": "unsupported",
"configuration": "detect-only"
"format": "json",
"configuration": "managed"
},
{
"id": "opencode",
"display_name": "OpenCode",
"format": "opencode",
"configuration": "managed"
},
{
"id": "antigravity",
"display_name": "Antigravity",
"format": "json",
"configuration": "managed"
},
{
"id": "copilot_cli",
"display_name": "GitHub Copilot CLI",
"format": "copilot_cli",
"configuration": "managed"
},
{
"id": "commandcode",
"display_name": "Command Code",
"format": "commandcode",
"configuration": "managed"
},
{
"id": "vscode",
"display_name": "VS Code",
"format": "vscode",
"configuration": "managed"
}
],
"cli": {
"primary_count": 67,
"primary_count": 73,
"commands": [
"address-to-file-offset",
"analyze",
"analyze-javascript-application",
"analyze-web-bundle",
"annotate-native-function",
"build-reconstruction-obligation-ledger",
"capabilities",
"capture-browser-scenario",
"capture-electron-scenario",
"capture-native-ui-scenario",
"capture-process",
"capture-web-screenshot",
"compare",
@@ -457,20 +501,24 @@
"evaluate-reconstruction-coverage",
"evidence-export",
"evidence-import",
"execute-node-characterization",
"extract-artifact",
"function",
"import-managed-reconstruction",
"import-reference-source",
"inspect",
"inspect-artifact",
"inspect-asset-catalog",
"inspect-electron-page",
"inspect-keyed-archive",
"inspect-macho",
"inspect-managed-artifact",
"inspect-managed-members",
"inspect-managed-native-boundaries",
"inspect-native-api",
"inspect-native-data-type",
"inspect-native-dispatch-metadata",
"inspect-native-instruction",
"inspect-native-load-image",
"inspect-plist",
"inspect-signature",
"inspect-web-page",
@@ -480,23 +528,22 @@
"list-electron-targets",
"list-javascript-runtime-targets",
"observe-javascript-runtime",
"observe-native-ui",
"observe-web-session",
"plan-managed-runtime-correlation",
"policy",
"prepare-node-characterization",
"project-managed-application-graph",
"providers",
"read-bytes",
"reconcile-javascript-runtime",
"run-controlled-replay",
"run-replay-machine",
"resolve-native-call-targets",
"search",
"setup",
"trace",
"trace-application-feature",
"trace-javascript-semantics",
"trace-native-ui-action",
"trace-native-values",
"uninstall",
"upgrade",
"update",
"verify-managed-native-boundaries",
"xrefs"
],
@@ -509,15 +556,15 @@
},
"runtime_catalog": {
"counts": {
"cli_commands": 67,
"mcp_tools": 114,
"cli_commands": 73,
"mcp_tools": 118,
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "eb79567cc08f4afec223254c3977dc167d498f766b920077db0e1eead8be67b9",
"prompts_sha256": "fa889fc671185ac9c968c6dc8309cf609e709cdc837dbbf39b7cdfadb84ac2d7",
"combined_sha256": "3e9ab4e05563ea2609b7cf4db67638a79cd8a74188c292718d99a63451d5b9db",
"providers_sha256": "03498d8adb7d941f8293eea8d71375871fb30a278e310fdc19b98df6781fc1a7"
"tools_sha256": "bbd9aacf7bb0bc54a8f8743491583482e1176252513d99780a309d0fddeb3df0",
"prompts_sha256": "4762db348821fec787a5b8c6423f2c48a71f6f0ec2afee97b16ad69030ec504b",
"combined_sha256": "b4f7e5860d90b029876b58fe7cd4a8041fef1f2add23440e37e90827d808e503",
"providers_sha256": "e094c361b60ec930a6062a1a90ee05536ce411541dba5b51ace7e2da2ba25af0"
}
}
}
+67 -65
View File
@@ -1,26 +1,25 @@
# Static-analysis provider evaluation
Status: the Ghidra read-only analysis provider is shipped on Linux x64 and has
an experimental Windows x64 P0 for approved native PE applications. macOS is
not an admitted Ghidra host. The provider
validates an exact bring-your-own Ghidra 12.1.4/JDK 21 environment, resolves a
provider/version/profile commitment, runs one isolated read-only headless
import, and publishes 19 operation-level capabilities after the authenticated
post-analysis handshake.
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.
The provider-neutral target, provider registry, deterministic target binding,
analysis-profile commitment, Evidence provenance, snapshot caching, and bounded
provider-process lifecycle foundations are implemented. The Ghidra launcher,
packaged Java bridge, doctor/setup projection, bounded client lifecycle, and
multi-target real Linux verifier, curated Windows CI, and controlled real
Windows verifier are also implemented. Program identity,
procedure/string/symbol inventory, memory blocks, address/name and
containing-procedure resolution, complete inventory search, function metadata,
decompilation, assembly, resolved calls, typed references, xrefs, CFG, and
function dossiers are admitted. A provider is not a drop-in replacement for
Hopper: every capability is mapped explicitly, with truthful `unavailable`,
unknown, or degraded results where the engine cannot provide equivalent
semantics.
REA imports one target into a temporary project and exposes 25 read-only
operations after analysis completes. They cover inventories, search,
decompilation, assembly, function metadata, resolved calls, references, control
flow, instructions, and recovered types. Results identify the provider version,
analysis settings, target digest, and any unavailable or incomplete facts.
On Linux and macOS, `annotate_native_function` additionally edits names and entry
comments atomically, verifies readback, and returns refreshed function analysis.
Changes live only in the session database and leave executable bytes unchanged.
Ghidra does not expose GUI controls; Windows P0 remains read-only.
Windows x64 P0 supports native x86-64 PE applications on local NTFS with
bundled Job Object ownership, protected runtime DACLs, and handle-based path
admission. Real ordinary-user verification covers all 25 read-only operations
through the packaged CLI and MCP, target integrity, and cleanup. This remains
an experimental boundary; see [Windows Ghidra P0](windows-ghidra-p0.md) and
[issue #527](https://github.com/morluto/rea/issues/527).
[ADR-0001](adr/0001-provider-selection-and-analysis-profiles.md) fixes the
provider registry, deterministic selection, target binding, analysis profile,
@@ -32,8 +31,8 @@ follow.
- Keep the existing provider-neutral CLI and MCP tool names.
- Bind one deep-analysis provider to a target for the target's lifetime; never
fail over silently between Hopper and Ghidra.
- Start with bring-your-own Ghidra and a compatible Java runtime. Setup must not
install or upgrade Java.
- Use an existing Ghidra installation and a compatible Java runtime. Setup must
not install or upgrade Ghidra or Java.
- Run Ghidra headlessly in an owned process with a private temporary project,
startup and cleanup deadlines, caller cancellation, and complete replies.
- Prefer a packaged Java bridge loaded through Ghidra's script path. PyGhidra
@@ -54,10 +53,10 @@ follow.
`GHIDRA_INSTALL_DIR` must identify an extracted official 12.1.4 release;
optional `JAVA_HOME` must identify a 64-bit full JDK 21, otherwise doctor probes
`java`/`javac` or `java.exe`/`javac.exe` from `PATH`. Linux accepts x86, x86-64,
ARM, and ARM64 ELF, PE, and Mach-O executable targets. Windows P0 accepts only
native x86-64 PE applications and rejects DLLs, managed images, unknown roles,
other formats, and other architectures before launch.
`java`/`javac` or `java.exe`/`javac.exe` from `PATH`. Supported Linux and macOS
hosts accept compatible ELF, PE, and Mach-O executable targets. Host admission
and target compatibility are separate checks. The intended Windows P0 accepts
only native x86-64 PE applications; it remains blocked before launch.
The launcher creates one ephemeral runtime root with project,
home/cache/config/data/temp, logs, descriptor, endpoint, target snapshot, and
@@ -66,18 +65,19 @@ It passes `-readOnly` and `-deleteProject`, and uses Ghidra's default analysis
and resource settings; inherited Java option injection variables are cleared.
On Linux, the mode-0600 descriptor carries the random token without
exposing it in argv or environment and the Java bridge binds a mode-restricted
Unix socket. Windows uses authenticated IPv4 loopback with a strict token-free
endpoint record. Both transports report actual
Ghidra/language/compiler/analysis/import-digest metadata, accept only the exact
authenticated `ping`, `shutdown`, and ten
inventory plus nine function-analysis methods, and deletes the socket. Close,
cancellation, timeout,
malformed protocol, or process exit stops the owned process resources and
removes the entire runtime root. Windows P0 uses bounded `taskkill` tree
termination and explicitly does not claim Job Object, private-DACL, or
reparse-point authority; see [Windows Ghidra P0](windows-ghidra-p0.md).
Unix socket. macOS uses the same local transport. The bridge reports actual
Ghidra/language/compiler/analysis/import-digest metadata and accepts only
authenticated `ping`, `shutdown`, ten inventory methods, and twelve
function-analysis methods. Close, cancellation, timeout, malformed protocol,
or process exit stops the owned process resources, closes the socket, and
removes the runtime root.
The provider catalog lists only the 19 proved Ghidra operations. GUI cursor,
The experimental Windows transport uses authenticated IPv4 loopback with a
token-free endpoint record, but cannot start while the native authority is
unavailable. Its `taskkill` cleanup code does not establish Job Object ownership,
private DACLs, or reparse-safe paths.
The provider catalog lists the 25 Ghidra operations. GUI cursor,
navigation, and mutation operations remain absent; the router therefore
reports them unavailable instead of borrowing Hopper semantics or inferring
capability from a successful import.
@@ -98,21 +98,21 @@ capability from a successful import.
## Admitted function-analysis semantics
| Concern | Ghidra contract |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Decompiler lifetime | One persistent `DecompInterface` is opened for the imported Program and disposed during bridge shutdown. Native decompilation has no fixed per-function deadline. External functions or functions without bodies return `null`; cancellation and native failure remain distinct. |
| Serialization | A FIFO sends one Program request at a time. Caller cancellation removes queued work promptly and is passed to active socket waits. This is an adapter safety commitment, not a claim that every Ghidra API is thread-safe. |
| Function identity | Every function result carries the entry address and Ghidra FunctionManager classification for external, thunk, and resolved thunk target. These are observations, not proof that unresolved targetless calls have been recovered. |
| Assembly and pseudocode | Assembly is complete Ghidra Listing text; pseudocode is Ghidra decompiler output. Neither is original source, and cross-provider comparison never treats Hopper and Ghidra text as equal or unequal semantic facts. |
| Instruction fast path | `read_function_instructions` returns every raw Listing instruction for the requested function without invoking the decompiler or whole-program name/string inventories. |
| Calls and references | Callers/callees contain only resolved functions. Reference edges preserve exact ReferenceManager type and call/jump/data/read/write/indirect/computed/conditional/terminal/external facts. Targetless computed flow remains unknown. Synthetic entry-point references without actionable memory sources are omitted explicitly. |
| CFG | Dossiers use `BasicBlockModel` and retain only non-call successors inside the function body. CFG topology is address-normalized for comparison; provider-specific block construction remains a declared difference. |
| P-code value flow | Function dossiers include up to 3,000 high-p-code operations, at most 64 inputs per operation, and 12,000 def-use edges. Results identify memory reads/writes, branches, and calls by p-code opcode, and mark decompiler-dead operations. Large results report truncation, an omitted-operation lower bound, and known omitted-input/edge counts. This is an intra-function decompiler graph: it does not resolve cross-function data flow, memory aliasing, call side effects, runtime behavior, persistent state, or business meaning. |
| Result extent | Function instruction scans and native API boundary observations remain complete. P-code flow is the explicit bounded exception; its count fields distinguish exact retained counts from lower-bound or known-omitted counts. Caller cancellation and provider failures remain distinct. |
| Concern | Ghidra contract |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Decompiler lifetime | One persistent `DecompInterface` is opened for the imported Program and disposed during bridge shutdown. Native decompilation has no fixed per-function deadline. External functions or functions without bodies return `null`; cancellation and native failure remain distinct. |
| Serialization | A FIFO sends one Program request at a time. Caller cancellation removes queued work promptly and is passed to active socket waits. This is an adapter safety commitment, not a claim that every Ghidra API is thread-safe. |
| Function identity | Every function result carries the entry address and Ghidra FunctionManager classification for external, thunk, and resolved thunk target. These are observations, not proof that unresolved targetless calls have been recovered. |
| Assembly and pseudocode | Assembly is complete Ghidra Listing text; pseudocode is Ghidra decompiler output. Neither is original source, and cross-provider comparison never treats Hopper and Ghidra text as equal or unequal semantic facts. |
| Instruction fast path | `read_function_instructions` returns every raw Listing instruction for the requested function without invoking the decompiler or whole-program name/string inventories. |
| Calls and references | Callers/callees contain only resolved functions. Reference edges preserve exact ReferenceManager type and call/jump/data/read/write/indirect/computed/conditional/terminal/external facts. Targetless computed flow remains unknown. Synthetic entry-point references without actionable memory sources are omitted explicitly. |
| CFG | Dossiers use `BasicBlockModel` and retain only non-call successors inside the function body. CFG topology is address-normalized for comparison; provider-specific block construction remains a declared difference. |
| P-code value flow | Function dossiers include up to 3,000 high-p-code operations, at most 64 inputs per operation, and 12,000 def-use edges. Results identify memory reads/writes, branches, and calls by p-code opcode, and mark decompiler-dead operations. Large results report truncation, an omitted-operation lower bound, and known omitted-input/edge counts. The dossier is an intra-function graph; `trace_native_values` composes derived parameter/argument and return/output dependencies across resolved calls. It does not resolve memory aliasing, call side effects, runtime behavior, persistent state, or business meaning. |
| Result extent | Function instruction scans and native API boundary observations remain complete. P-code flow is the explicit bounded exception; its count fields distinguish exact retained counts from lower-bound or known-omitted counts. Caller cancellation and provider failures remain distinct. |
`npm run verify:ghidra` compiles the versioned C oracle into debug and stripped
host-native targets (x86-64 ELF on Linux x64). It proves all
19 operations, external functions, resolved thunks, exports, stripped-name
host-native targets (x86-64 ELF on Linux x64 or Mach-O on macOS), plus a native
DWARF 4 type-layout object. It proves the admitted operations, external functions, resolved thunks, exports, stripped-name
behavior, direct and targetless indirect calls, typed references, strings/xrefs,
multi-block CFG, bounded p-code def-use/effect links, semantic enhanced workflows, cancellation, startup deadlines,
serialized concurrency, malformed-target rejection, profile identity, and
@@ -126,24 +126,26 @@ Mach-O target coverage. It requires `clang`, LLD, and `lld-link`; set
these tools before compilation. Keeping this matrix separate lets Linux
host/provider acceptance run with only the host compiler.
`npm run verify:ghidra:windows` generates a deterministic source-owned native
x86-64 PE application, proves all 19 operations through the production Windows
launcher and loopback transport, checks target/snapshot/import SHA-256 linkage,
and requires project, endpoint, process, and runtime cleanup. Passing that lane
proves only the documented P0 boundary, not the remaining Windows security
gates.
`npm run verify:ghidra:windows` checks the source-owned native x86-64 PE
fixture, all 25 read-only operations, target/snapshot/import SHA-256 linkage,
and project, endpoint, process, and runtime cleanup. The independent native
lane checks DACLs, handle admission, cancellation, and Job Object lifecycle.
`npm run verify:ghidra:windows:package` packs and installs REA into an isolated
prefix, then checks ordinary-user CLI/MCP operations against canonical contracts.
All lanes require matching native controls; package startup alone establishes
only package compatibility.
## Shared provider-process foundation
`src/process/` now provides the mechanisms that a long-lived Hopper or Ghidra
adapter genuinely shares: POSIX run-token-authenticated process-group ownership,
adapter shares: POSIX run-token-authenticated process-group ownership,
ephemeral temporary runtime roots, one absolute startup deadline, correlated
request cancellation and lifecycle-deadline cleanup, bounded stdout and stderr retention with
exact byte counts, process-exit diagnostics, and bounded TERM-to-KILL shutdown.
Reusable fixtures exercise exit, timeout, cancellation, graceful termination,
forced termination, double-close, spawn failure, and resource release. Windows
P0 adds bounded process-tree termination as an explicitly weaker host backend;
Job Object ownership remains unimplemented.
process-tree termination code exists, but the missing Job Object and private
runtime controls prevent it from establishing an available Ghidra session.
The foundation does not define a bridge schema, socket framing, health payload,
analysis model, or shutdown acknowledgement. Hopper keeps its authenticated
@@ -153,12 +155,12 @@ only the generic process mechanisms.
## Shortlist
| Provider | License / automation surface | What it brings | REA fit and blockers |
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Broad static analysis, many processors and formats, scripting, and project/database workflows | Read-only analysis shipped: exact BYO checks, isolated state and provider lifecycle, serial API queue, packaged bridge, 19 inventory/function operations, and real ELF/PE/Mach-O x86-64 plus AArch64 conformance. GUI and mutation semantics remain intentionally unavailable. |
| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. |
| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. |
| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. |
| Provider | License / automation surface | What it brings | REA fit and blockers |
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Static analysis, multiple processors and formats, scripting, and project/database workflows | 25 read-only operations shipped on Linux/macOS with installation checks, temporary projects, a serial API queue, and real ELF/PE/Mach-O conformance. Experimental Windows x64 P0 supports native x86-64 PE on local NTFS with bundled native controls and ordinary-user CLI/MCP verification; Linux/macOS additionally support atomic session function annotations; Windows mutation and all Ghidra GUI operations are unavailable. |
| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. |
| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. |
| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. |
## Recommended order
+2 -2
View File
@@ -36,7 +36,7 @@ check project-owned conformance fixtures and fail-closed report behavior.
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `discover-classify` | target-free startup, artifact inventory, compatible workflow selection, and exact visible limits |
| `diagnose-environment` | broken runtime separated from analysis failure, version skew detected, and revision changed after recovery |
| `acquire-authority` | exact missing scope, narrow grant, expansion re-prompt, and no launch after denial/cancellation |
| `declare-request` | exact target, operation, and lifecycle are carried by the request before launch |
| `static-analysis` | native and JavaScript routing plus preserved ambiguity, truncation, unsupported syntax, and residual unknowns |
| `reactive-scenarios` | predicate-driven interaction, descendant settlement, Electron correlation, and cancellation cleanup Evidence |
| `compare-authority-candidate` | explicit partial-order/finite-trace semantics, primary divergence localization, and unknown for incomplete comparison |
@@ -63,7 +63,7 @@ The evaluator rejects or withholds readiness when:
- contradiction Evidence or its affected comparison is missing;
- the reconstruction obligation ledger is not `ready`, has no required
obligation, or retains an unowned/unverified required obligation;
- denied authority launches a process, cancellation loses diagnostics, or owned
- a process exceeds its declared target/lifecycle, cancellation loses diagnostics, or owned
resources remain;
- replay is nondeterministic, tampering/staleness is not detected, or the
expected source digest differs.
+69 -32
View File
@@ -1,32 +1,69 @@
# Installation roadmap
# REA roadmap
## Tool direction
REA provides inspection, capture, and comparison tools that return evidence
directly. Agents choose commands, paths, browser actions, scripts, and local
fixture servers, then compose experiments from those results. CLI and MCP must
expose equivalent behavior and preserve observations, inferences, and unknowns.
[#555](https://github.com/morluto/rea/pull/555) removed REA permission grants,
scope ceilings, elicitation, and repeated approval fields.
[#572](https://github.com/morluto/rea/pull/572) removed the replay engines,
Node characterization prepare/execute flow, and plan-only managed runtime
correlation tool. Those systems are retired roadmap items. Existing evidence
authority labels remain readable as provenance; they do not imply a current
replay executor.
Local operations use the current user's OS permissions. Input and protocol
validation, provider prerequisites, target identity, cancellation, and owned
resource cleanup remain part of each tool's contract. Setup still discloses
installation changes and requires approval before writing or installing.
## Remaining evidence and provider work
The [platform roadmap](https://github.com/morluto/rea/issues/32),
[process and Hopper fidelity tracker](https://github.com/morluto/rea/issues/48),
and [browser tracker](https://github.com/morluto/rea/issues/39) track remaining
capabilities and proof. Their scope follows direct tools and agent-composed
experiments; custom replay languages and grant systems are not completion gates.
[Inline Evidence composition](https://github.com/morluto/rea/pull/585) now returns
the complete record accepted by comparison tools. Managed inspection uses an
explicit PE/CLI target independently of the native provider selection. Comparisons
preserve unknowns when observation is incomplete, and PTY diagnostics retain the
actual loader or startup failure. Browser cleanup reports incomplete teardown
alongside the primary failure; transient documents invalidate only the affected
frame's WebMCP registrations. Real-provider and platform claims still require
their corresponding verification lanes.
## Shipped behavior
REA setup currently configures the capabilities that exist today: its bundled
skill, detected agent integrations, optional Hopper provider, and validated
bring-your-own Ghidra paths. It detects
Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, and Devin. The
first six have documented local MCP configuration boundaries and can be updated
additively; Devin is reported but left unchanged.
REA setup lets you select agent integration and optional Hopper installation.
It installs the bundled workflow, configures detected agents, and can save
verified paths for an existing Ghidra installation. It configures Claude Code,
Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity,
GitHub Copilot CLI, Command Code, and VS Code using each client's configuration format.
The Ghidra foundation supports Linux x64 with exact Ghidra 12.1.4 and a 64-bit
full JDK 21. macOS is not an admitted Ghidra host. An experimental
Windows x64 P0 admits approved native x86-64 PE
applications through the same 19-operation Java bridge. Doctor validates those coordinates; approved Linux setup propagates them
to MCP registrations without installing or modifying either dependency. REA's
packaged Java bridge then proves an isolated read-only headless import,
post-analysis handshake, complete cleanup, ten admitted read-only inventory
operations, and nine function-analysis operations. Real source-owned x86-64
debug/stripped ELF, AArch64 ELF, PE, and Mach-O fixtures cover program,
procedure, string, symbol, external/thunk, memory, resolution, search,
decompilation, assembly, calls, typed references, xrefs, and CFG semantics.
Function dossiers also expose bounded Ghidra high-p-code def-use and memory or
control-flow effects, mark decompiler-dead operations, and report truncation
plus known omitted counts. This is intra-function
decompiler evidence; cross-function state flow remains unimplemented.
Hosted Windows CI covers build, package, target admission, transport, and
lifecycle seams; a controlled self-hosted workflow covers the real Ghidra P0
claim.
Ghidra analysis supports Linux x64 and macOS x64/arm64 with Ghidra 12.1.4 and a
64-bit full JDK 21. macOS also requires the matching native decompiler. The
adapter exposes thirteen inventory operations and twelve function-analysis
operations, for 25 read-only operations total. Linux and macOS additionally support
atomic function-name and entry-comment edits with refreshed analysis; metadata
is discarded on close and executable bytes stay unchanged. Approved setup saves verified
installation paths in agent configurations without installing or changing
Ghidra or Java.
Real-provider verification uses host-native debug and stripped fixtures plus
a native type-layout object. A separate lane covers AArch64 ELF, PE, and
Mach-O cross-target fixtures. See [testing](testing.md) for the prerequisites
and exact commands.
Experimental Windows x64 P0 now supplies bundled Job Object ownership,
protected runtime DACLs, and handle-based admission for native x86-64 PE
applications on local NTFS. Real ordinary-user CLI/MCP verification covers
all 25 read-only operations and cleanup. The [Windows P0 guide](windows-ghidra-p0.md)
describes this boundary and unverified broader coverage.
## Ghidra maintenance boundary
@@ -36,16 +73,16 @@ not expected to match; unresolved targetless flow remains unknown. Automatic
Ghidra acquisition, if ever added, remains a separately planned and approved
related-tool change; setup must never install Java.
Before Windows advances beyond P0, add current-user-only DACL creation and
readback, handle-based reparse-point-safe path authority, a DACL-protected IPC
backend, and Job Object assignment before provider execution. Process capture,
controlled replay, Hopper, and broad filesystem-sensitive workflows remain
separate Windows projects rather than implied parity.
Maintain Windows P0 with independent DACL readback, handle-based admission,
private authenticated IPC, and Job Object assignment before provider execution.
Process capture, Hopper, and broad filesystem-sensitive workflows remain
separate Windows projects; P0 does not imply their parity.
## Capability-selective setup
After REA ships at least two real optional toolchains, setup can evolve into a
capability-oriented wizard. It may ask whether the operator wants to investigate
Setup already lets users select agent integration and Hopper installation.
Once REA supports installing additional analysis tools, it can expand these
choices to match the work a user wants to do. It may ask whether the operator wants to investigate
native binaries, websites, mobile applications, firmware, or runtime protocols,
then propose only the tools needed for the selected work.
+169
View File
@@ -0,0 +1,169 @@
# Test suite audit
## Scope and priority
The expanded audit on 2026-10-04 reviewed every one of the 355 Vitest files
present at its start: 82 domain/contracts, 56 application/composition, 147
adapter/configuration/process/filesystem/CLI files, and 70 MCP/acceptance/
conformance/evaluation files. Three review passes checked the proposed removals
against existing stronger coverage and checked the final changes for lost
assertions. The remaining suite has 351 files.
Prefer full real-provider E2E, then integration across schemas and transports,
then real-data goldens. Remove lower-level duplication once the replacement
asserts the same behavior. Keep distinct failure regressions that a successful
E2E cannot exercise. Do not lower coverage thresholds to accommodate pruning.
## Changes made
| Previous test or assertion | Decision | Replacement or retained claim |
| ----------------------------------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Asset application tests using fabricated `.car` bytes and injected utility JSON | Remove two plumbing cases | `verify:asset-catalog`: real actool → assetutil → CLI and stdio MCP, including digest, full metadata, pagination and invalid input |
| Keyed archive positive binary/XML objects assembled with the same plist library as the reader | Remove two synthetic success cases | Captured Foundation XML plus reviewed complete object/reference graph golden; real Foundation binary/XML → CLI and stdio MCP |
| Generic contract schema/annotation/example loops and output-map inventory comparisons | Consolidate into one SDK wire test | Assert exported schemas, guidance, effect annotations and valid examples on the actual advertised MCP catalog |
| Ghidra function operation enum membership | Remove | Canonical catalog, provider handshake and actual operation roundtrips establish admission; malformed provider results remain covered |
| Asset page assertion pinning `10_001` | Remove | Invalid-page rejection through real MCP; malformed utility data still covered |
| Suggested prompt tool positions | Remove incidental ordering assertions | Keep authority, evidence, untrusted input and preparation constraints |
| Package “acceptance” assertions pinning CI matrix/shard values and fake launcher command arrays | Remove configuration-shaped cases | Actual package/install verifier remains; retain a compact release verification-before-publish and registry-canary static guard |
| Package failure classifier under acceptance | Move to package evaluation | Preserve all malformed/unrelated-failure rejection cases; do not call them E2E |
| Native value graph checks conditional on success | Strengthen | Require success before pagination/ambiguity assertions; unexpected errors can no longer pass |
| Native UI verifier treating permission denial as a positive pass | Separate outcomes | Default lane requires real capture/action success; opt-in permissions lane explicitly reports no positive E2E proof |
Ten previous test cases were removed or consolidated; three replacement
Vitest cases cover the captured goldens and release guard. The two real artifact
E2E scripts add product-level evidence outside the deterministic Vitest count.
## Exhaustive follow-up
The follow-up removes or consolidates another 31 test declarations, including
three redundant test files. Parameterized declarations and executed cases have
different counts; the figures above count source declarations. It also repairs
assertions that could pass without exercising their stated behavior.
| Area | Cleanup | Stronger retained evidence |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Configuration | Merge duplicate target/loader tests and preserve custom launcher, explicit provider and actionable error diagnostics | Independent malformed inputs; each capability flag is rejected separately |
| Browser scenarios | Remove fake launch success and expression-selected artifact success | Real Chromium scenarios, actual artifacts, CLI capture and owned/external cleanup in `verify:browser` |
| CLI policy | Remove duplicate confirmation helper cases | Compiled CLI proves project files stay unchanged until `--yes`; distinct interactive choices remain |
| Provider capabilities | Remove numerical catalog freezes; consolidate Hopper byte-read discovery | Exact canonical capability identities, admission, mutation effects and detached descriptors |
| Evidence ledger | Merge reordered-key checks into serialized bundle import; remove one-record quota claims | Atomic import, conflicts, residual revisions, actual large collections and byte regressions |
| MCP batching and concurrency | Replace maximum concurrency and empty indistinguishable responses | Every distinct ordered batch result; concurrently gated requests complete in reverse order with matching payloads |
| MCP inventories and graphs | Remove traversal/inventory call order and redundant catalog/prose checks | Exact complete cyclic graph, full inline inventories, canonical SDK catalog and all-handler roundtrips |
| Authority and lifecycle | Attempt the refused Ghidra mutation; require successful setup, call and switch; close twice | Typed refusal with zero mutation dispatch, restored target calls and exact owned-resource cleanup |
| Process and provider output | Remove incidental environment read and command option order/defaults | PID/ownership validation, exact disabled-policy failure and complete decoded response above 10 MiB |
| Conformance and CI guards | Merge identical replay runs; remove duplicate missing-row fault, trivial hash/self equality and incidental worker/step counts | Distinct faulty evidence, digest order, actual test discovery, file existence and release/runner authority |
| Pure schemas | Remove fabricated comparison-dimension injection and impossible long boundary success | Real artifact comparison and SDK coverage evaluation with returned boundary identity |
The removed long boundary selector test only parsed a 1,000-character request;
workspace boundary identities admit at most 200 characters, so that positive
could never represent a successful evaluation. The actual MCP evaluation stays
covered. One unused ledger serialization helper was also retired after removing
its only test consumer; no CLI or MCP contract changed.
No further immediately safe pruning was identified in the final review. Retained
replacement candidates below are coverage gaps, rather than permission to delete
unique tests. Test-only domain scaffolding also stays pending a separate decision
about the corresponding source behavior; deleting those tests alone would leave
unverified code and fail unused-export checks.
## Stronger replacements from the closing pass
Three more synthetic success cases were removed. Native value composition now
runs through the production CLI and a separate stdio MCP subprocess with real
Ghidra, using a source-built native fixture. It checks argument/parameter/return
relationships, the fixture's global operand, authenticated Evidence, upstream
and workflow analysis profiles, identical CLI/MCP graphs, discovery availability
and successful MCP session closure. Ambiguity, cancellation, target changes,
missing flow and bounded-work regressions remain in the focused suite.
That replacement found missing CLI workflow routing for `trace_native_values`
and missing workflow profile classification. The routing and discovery now use
the composed operation's actual provider prerequisites; workflow Evidence commits
to the upstream Ghidra profile.
The lone hand-built client-document equality test is replaced by a real TOML
filesystem transaction: reversed environment keys leave the original bytes
unchanged and create no backup. A real ZIP entry with an application root longer
than 5,000 characters replaces injected schema-only positive data. Inventory
completeness tests partition actual scans into authenticated partial observations
and require exact assembled results and every source citation, including more
than 100 citations. These are consumer merge tests, not provider pagination
claims. Replay aggregation now covers all four statuses and retains actual runner
diagnostics; it does not claim to execute a real sandbox. Retained pixel-metric
fixtures now write correct PNG chunk checksums.
The all-zero screenshot buffer is replaced by a real Chromium PNG of 9,467,940
bytes, transported intact through CLI and stdio MCP and decoded by the production
pixel comparator. The verifier configures the SDK client's receive buffer because
its 10 MiB default is smaller than the inline JSON response. This does not claim
large comparison-request transport coverage. A real SDK initialize request,
captured during a successful production stdio handshake, replaces the JSON-RPC
decoder's field-count-only oracle with exact field/value expectations.
The closing pass also removed assertions tied to internal call order, defaults,
private cache sizes, duplicated polling and fabricated schema extensions. Unique
malformed-input, security-capacity and cleanup regressions remain.
## Real process regression found
The full suite exposed a renderer ordering race. Publishing a terminal output
journal entry could synchronously trigger a reactive resize before the preceding
output was queued for rendering. Queue that output first. The existing real
source-owned interactive PTY test now also requires the first resized frame to
contain the output that triggered the resize, preserving the original event
order rather than sorting or changing timestamps to conceal it.
## Coverage kept
Malformed archives, broken and cyclic references, unsupported metadata pointers,
relative Objective-C method layouts, ambiguous call candidates, digest mismatch,
cancellation, host permission denial, bounded output and owned-resource cleanup
remain covered. These cases provide evidence beyond a successful real-tool run.
The hand-assembled relative method fixture stays until compiler-produced
relative-list fixtures establish equivalent assertions.
## Remaining priorities
1. Extend the real Ghidra CLI/stdio MCP journey from value tracing to dispatch
and UI handlers before pruning their hand-authored composition success cases.
Native value tracing now establishes full transport parity.
2. Real binary dispatch and nib → full product transports, with normalized
semantic goldens. The compiler-backed readers already run in the new Apple
CI lane; synthetic pointer/layout rejection tests remain useful.
3. An approved interactive macOS runner for native UI capture/actions, including
stdio MCP scenarios. Hosted runners without permissions cannot supply this
proof. No arbitrary application's windows should be opened for this lane.
4. Real macOS/Linux Ghidra CI runners with bring-your-own installations. This
change adds Apple artifact CI without installing Java or Ghidra.
5. Classify fake-runner replay/application acceptance accurately and replace
repeated happy paths only after real sandbox journeys assert the same claims.
The replay workflow's allow-unavailable mode must not imply executed replay.
6. Replace repeated-array schema ceiling tests with real producer/transport
regressions before deleting them. Existing integration tests do not yet
establish their large-input guarantees.
Some static guards protect irreversible release authority rather than product
behavior. Keep these few guards explicit and separate from E2E evidence.
## Verification after the expanded audit
- `npm run check:pr`: 351 files passed; 1,794 tests passed and two skipped.
Typecheck, lint, formatting, unused-code and generated-document gates passed.
- `npm run check:fast`: passed; 70 lint warnings and zero errors.
- `npm run verify:browser`: actual Chromium observation and scenario CLI routes,
retained artifacts, owned/external lifecycle checks and large CLI/stdio MCP PNG
capture passed.
- `npm run verify:ghidra`: macOS ARM64 with Ghidra 12.1.4/JDK 21 passed, including
real native value tracing through CLI and stdio MCP: 171 nodes, 298 edges and
six decompilations.
- `npm run verify:asset-catalog`: real actool/assetutil → CLI and stdio MCP,
complete records, pagination and malformed input checks passed.
- `npm run verify:keyed-archive`: real Foundation binary/XML → CLI and stdio MCP,
full graph golden, shared/cyclic identity and malformed scope checks passed.
- `npm run verify:fixtures`: seven source-built host fixtures, source/artifact
digests and exhaustive inventory checks passed.
No Hopper windows were opened for this audit. The closing pass runs real Ghidra
on macOS ARM64; earlier Hopper evidence remains separate. No new hosted CI success
is claimed.
+185 -37
View File
@@ -1,8 +1,15 @@
# Testing REA
REA's test suite follows one rule: use the lowest behavioral depth that can
prove a claim. Tests are grouped into native Vitest projects so ownership,
allowed dependencies, and runtime cost are visible from their paths.
Prefer evidence in this order: full end-to-end workflows with real production
providers, integration tests across data/API boundaries, then golden regressions
from real captured inputs. Keep focused module tests for distinct failure or
semantic cases that these workflows cannot reliably reproduce. A test's path
or suite name does not establish its behavioral depth.
Tests are grouped into Vitest projects so ownership, allowed dependencies and
runtime cost are visible from their paths. Before pruning a test, identify the
replacement scenario and its assertions; passing success journeys do not
replace malformed input, cancellation, permission or cleanup coverage.
## Behavioral depths
@@ -12,7 +19,7 @@ allowed dependencies, and runtime cost are visible from their paths.
| Composition | `tests/composition/**` | Provider-neutral session and registry wiring; filesystem access is limited to fixture materialization |
| Boundary | `tests/boundary/**` | Exactly one production filesystem, process, network, browser, CLI, or provider boundary |
| MCP boundary | `tests/boundary/mcp/**` | MCP transport and tool-contract boundaries with isolated sessions and explicit cleanup |
| Acceptance | `tests/acceptance/**` | A complete compiled CLI or MCP workflow |
| Acceptance | `tests/acceptance/**` | A compiled CLI or MCP journey; injected providers still make it integration |
| Conformance | `tests/conformance/**` | Shared provider contracts, parameterized by declared capabilities and explicit opt-outs |
| Evaluation | `tests/evaluation/**` | Deterministic evaluator parsing, scoring, and report generation |
@@ -31,17 +38,55 @@ with the suite and excluded from package builds; broader runtime and provider
fixtures remain under `tests/fixtures/**`.
`tests/process-global/**` is reserved for cases with a demonstrated dependency
on process-global state. Those tests run without file parallelism. Reusable,
on process-global state. Those tests use isolated forks so environment and
exit-status changes cannot leak between files. Serialize a case only when it
demonstrably shares an external resource that cannot be isolated. Reusable,
test-scoped fixtures live under `tests/support/**`; immutable source artifacts
remain under `tests/fixtures/**`.
The process-global Vitest configuration contract rejects new direct temporary-root
creation outside the workspace seam and its narrowly documented boundary/package
exceptions.
Real Hopper, Ghidra, browser, package, managed-code, and controlled-replay
claims belong to their explicit `npm run verify:*` lanes. They are not inferred
from mocks or folded into the deterministic local gate. Real model trials are
manual; Vitest covers only deterministic evaluator logic.
Real Hopper, Ghidra, browser, package, and managed-code claims belong to their
explicit `npm run verify:*` lanes. The reconstruction-readiness lane also
checks deterministic rerun, tamper, and stale-input handling; those checks do
not execute extracted JavaScript modules. When application runtime behavior is
needed, exercise the actual target through browser, Electron, or process
capture. Real model trials are manual; Vitest covers deterministic evaluator
logic.
## End-to-end, integration and golden evidence
Full E2E tests invoke the production command dispatcher and real providers,
without fake launchers, runners or responses. `verify:keyed-archive` writes an
actual Foundation binary archive, runs the CLI and a separate stdio MCP
subprocess, checks parity and pagination, rejects an escaping path and checks
an XML graph golden. `verify:asset-catalog` compiles source-owned colors with
`actool`, invokes real `assetutil`, then checks CLI/MCP results, exact catalog
digest, every raw metadata field, pagination and malformed input rejection.
Neither artifact workflow requires Hopper or launches it. Both run in macOS CI.
MCP SDK transport tests with recording providers remain integration tests.
They are useful for schema drift and failure projection but do not prove that
Hopper, Ghidra or another substituted engine works. `verify:package` proves
packaging/install behavior and fake-provider integration; use the corresponding
real-provider lanes for engine claims. Real Apple dispatch and Interface
Builder verifiers currently prove format integration through production readers.
Golden tests use immutable captured text inputs with producer/source provenance
under `tests/fixtures/golden/`. Expected results are reviewed for the semantic
claim; capture commands do not automatically approve new expected outputs.
Do not call handcrafted utility output or synthetic binary builders real-data
goldens. Keep unsupported binary layouts and malformed boundaries as targeted
regressions until a real fixture establishes equivalent coverage.
`verify:browser` also captures a source-owned noise canvas as a real PNG above
8 MiB through the CLI and stdio MCP, with complete byte/digest parity and real PNG
decoding. Its SDK client explicitly permits the larger inline JSON response;
this lane does not establish large image-comparison request transport coverage.
The [test suite audit](test-suite-audit.md) records the pruning decisions,
replacement evidence and remaining priorities.
## Real-toolchain verification lanes
@@ -54,16 +99,74 @@ that a host or target is covered when it was skipped.
| Ghidra lane | Supported runner/target | Additional local tools |
| ------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `npm run verify:ghidra` | Linux x64 with x86-64 ELF | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 Linux ELF relocatable fixture | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra` | Linux x64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:switch` | Linux x64 ELF; GCC/Clang optimized and stripped switch fixtures | GCC, Clang, GNU nm/objdump/strip, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 ELF; byte/halfword tables; host ARM64 Mach-O | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:cross-format` | Any supported Ghidra host; also analyzes AArch64 ELF, x86-64 PE, and x86-64 Mach-O | `clang`, LLD, and `lld-link` in addition to host-lane prerequisites |
| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE | Ghidra 12.1.4, full JDK 21, and the Windows P0 fixture toolchain |
| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE | Ghidra 12.1.4, full JDK 21, and the matching native artifact |
Windows native conformance runs with `npm run verify:windows-native` and does
not require Ghidra or Java. An optional independently compiled Windows fixture
adds in-place reparse, breakaway, environment, and token observations.
`npm run verify:ghidra:windows:package` additionally packs and installs REA into
an isolated prefix and runs ordinary-user CLI/MCP operations against their
canonical schemas. Run the controlled fixture generator before that lane, or
supply an installed package root and an explicit fixture as arguments.
The host-native Ghidra lane also verifies native value tracing through the
production CLI and a separate stdio MCP process. It compares complete dependency
graphs, validates Evidence and upstream/workflow profiles, checks capability
discovery, and closes the MCP session. No provider or transport is mocked.
The Linux switch lane checks dense, sparse-with-holes, shared-body, nonzero,
negative, and nonexact JSON integer labels plus a comparison-only control.
Independent source labels, ELF file bytes, table slots, and bounds branches
define expected case/default destinations. Production CLI and MCP must agree;
debug labels must retain their signed values. For stripped negative fixtures,
the independently checked 32-bit dispatch permits equivalent unsigned labels
only alongside the reported low-confidence `undefined4` parameter; original
source signedness remains unknown and the ABI residual must remain visible.
unsafe labels remain unresolved and every recovered destination is retained.
The compiler oracle also injects malformed records to check that its assertions
reject missing/default-confused labels, wrong destinations, and numeric guesses.
It also invokes the actual bridge methods on detached Ghidra model objects to
check ambiguous dispatches, signed literals, precision bounds, shared targets,
and conflicting labels. This reflection fixture depends on the pinned Ghidra
model and does not claim a compiler produced those synthetic graph shapes.
Pass `--entrypoint /path/to/installed/rea-agents/scripts/rea.mjs` directly to
`scripts/verify-real-ghidra-switch.mjs` to verify an installed package through
the same compiler oracles and CLI/MCP checks.
The cross-format Ghidra lane also analyzes an optimized AArch64 ELF switch
fixture. It checks the recovered case values against the source cases and
requires unresolved table bounds or case mappings to remain visible as
residual unknowns.
`npm run verify:inspector` requires the supported Node.js runtime and installed
REA dependencies. CI runs it on Linux and Windows. It starts owned loopback
Node Inspector fixtures and verifies discovery and passive observation through
the CLI and stdio MCP, including special filenames, unresolved discovery
locations, and independently resolved loaded scripts. Double-quote filenames
are tested on POSIX only because Windows does not support them.
## DOS Ghidra analysis
`npm run verify:ghidra:dos` requires the supported Ghidra and JDK installation
on Linux x64 or macOS x64/arm64. It generates a source-owned MZ fixture without
a DOS emulator or compiler, then checks real 16-bit decoding, segment
relocation, near/far calls, decompilation, disjoint function body ranges,
stable CLI/MCP observations, unchanged source bytes, and owned process/project
cleanup. Raw p-code address-space selector tokens are reported separately from
the stable observation comparison. Linux x64 is verified; macOS DOS remains
unverified. This lane is separate from host-native and optional cross-format
verification. See [DOS analysis](ghidra-dos.md).
`npm run verify:ghidra:com` uses a generated headerless fixture with no compiler,
DOS emulator or game data. It exercises explicit admission, BinaryLoader entry
preparation, measured register context, whole-file byte readback, source offsets,
unmapped PSP/partial reads, actual decompilation, CLI/MCP parity and owned cleanup.
It has the same Ghidra/JDK prerequisites as the MZ lane. Neither lane claims DOS
runtime or PC-98 device execution.
## Apple Interface Builder archives
`npm run verify:interface-builder` compiles the source-owned AppKit XIB into a
@@ -77,30 +180,61 @@ not make native host acceptance unavailable.
## Developer commands
`npm test` runs every deterministic project once. The narrower feedback loops
are:
Use source feedback while editing, explicit boundary checks for the changed
behavior, and complete CI evidence before merging.
| Command | Scope |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `npm run test:fast` | Domain, service, adapter, composition, boundary, MCP boundary, and conformance projects |
| `npm run test:local` | Changed and related tests without the build step; lightweight local feedback loop |
| `npm run test:boundary` | Boundary, MCP boundary, and process-global projects |
| `npm run test:mcp` | MCP boundary project only |
| `npm run test:acceptance` | Complete CLI and MCP acceptance workflows |
| `npm run test:changed` | Changed tests in non-serial projects via Vitest's import graph |
| `npm run test:watch` | Changed domain, service, adapter, composition, boundary, MCP boundary, conformance, and evaluation tests in watch mode |
| `npm run test:watch:all` | Changed tests from every deterministic project in watch mode |
| `npm run check:changed` | Cached static checks followed by changed tests |
| `npm run check:pr` | Static, generated-document, and complete deterministic PR gate |
| Command | Scope |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `npm run test:local` | Dirty source tests via the import graph, without build; explicit source paths run regardless of Git status |
| `npm run test:focused -- PATH...` | Exact existing test files; compiled boundaries build first, and unmatched paths fail |
| `npm run test:changed` | Source tests affected since the merge base with `origin/main`, including committed and dirty changes |
| `npm run test:fast` | All domain, service, adapter, composition, conformance, and evaluation tests without build |
| `npm run test:boundary` | Boundary, MCP boundary, process boundary, and process-global projects |
| `npm run test:mcp` | MCP boundary project |
| `npm run test:acceptance` | Complete compiled CLI and MCP acceptance workflows |
| `npm run test:watch` | Dirty source tests in watch mode, without build |
| `npm run test:watch:all` | Changed tests from every project; builds at startup, so rebuild after production edits before relying on compiled tests |
| `npm run check:changed` | Cached typecheck/lint and branch-related source feedback |
| `npm run check:pr` | Opt-in complete local deterministic gate and generated-file checks |
| `npm run docs:check` | Committed generated-document freshness |
Changed-test selection is a fast feedback aid, not release evidence. It can
miss behavior connected through runtime registration, generated data, shell
entrypoints, or other relationships that are absent from the import graph.
Use `npm run check:pr` before handing off a contribution.
For example:
Local full-suite Vitest runs are intentionally capped at one worker and one
project at a time. Boundary fixtures own real subprocesses, and this local cap
keeps aggregate memory predictable; CI retains the existing two-worker budget.
```bash
npm run test:local -- src/config.test.ts
npm run test:focused -- tests/acceptance/applications/runtime.test.ts
npm run test:changed -- --base origin/main
npm run test:changed -- --dry-run
```
`test:focused` accepts exact repository-relative test file paths. Source-only
paths do not build; boundary, acceptance, process-global, or unfamiliar `tests/`
paths build conservatively. Tests use Vitest concurrency and isolated
workspaces; commands do not hold a broad test lock. Build and documentation
writers retain checkout-local locks for their shared output files. Explicit
selections do not use `--changed` or permit zero-test success.
The dry-run option reports the chosen merge base, scope and build prerequisite
without executing tests or building. A missing Git base reports how to fetch
it or select another revision.
Changed selection can miss runtime registration, generated data, shell
entrypoints, bridges, or other relationships absent from the import graph.
An empty changed selection means no tests were selected, not verified
correctness. Select relevant boundary files and real-provider lanes explicitly.
Source projects also contain large capacity regressions; `test:fast` promises
no compiled-runtime prerequisite, not a fixed time budget.
Routine iterations and rebases need focused regressions and relevant checks.
Before handing off a PR, run `npm run check` and generated-document checks when
applicable; CI owns the full suite and coverage. Use the full local gate for
broad changes or diagnosing CI, rather than after every edit. Package/install
changes additionally need package verification; provider changes need actual
provider evidence.
Local full-suite Vitest runs use up to two workers and schedule projects one
at a time. Process, acceptance and process-global projects serialize their
files to prevent competing lifecycle observations. CI retains its two-worker
budget.
The pure domain/contracts and recording-port service projects share one worker
module context because their tests own no mutable runtime resources. MCP
boundary files also share the immutable server module graph while creating and
@@ -109,8 +243,7 @@ process-global, and other boundary projects retain per-file isolation.
`npm test`, `npm run docs:check`, and `npm run docs:generate` share
repository-local locks and fail fast when the same class of command is already
running. The `npm test` build is inside that lock. `check:pr` runs its test task
before starting documentation validation, so TypeDoc does not compete with the
full suite for memory.
before starting generated-document validation.
Vitest and Node persistent compile caches are deliberately not enabled by
default. To evaluate repeated local runs, opt in for both cold and warm
@@ -144,5 +277,20 @@ for another.
The PR acceptance target is a median `npm run check:pr` wall time below three
minutes across three warm-build runs on the benchmark host. Keep Vitest caches
cold unless separately identified. A PR that touches packaging or real-system
behavior also requires `npm run verify:package` and the applicable
`verify:*` lanes.
behavior requires the applicable `verify:*` lanes; packaging and installation
changes also require `npm run verify:package`. The full-gate benchmark measures
that explicit lane, not the routine iteration requirement.
## Apple native metadata and UI
`npm run verify:apple-dispatch` compiles Objective-C class/protocol and Swift
conformance/vtable fixtures, inspects their bytes and repeats after stripping
local symbols. It requires macOS and the host Xcode toolchain; targets are not
executed. `npm run verify:native-ui` launches exactly one source-owned fixture
window and requires successful selected-window capture and selected actions.
An OS permission denial fails the positive lane. `npm run verify:native-ui:permissions`
allows a host-permission-boundary-only result and explicitly reports
`positive_e2e: false`; it must not be reported as capture/action proof.
Both commands reject a changed executable digest and clean up the fixture
process and helper. These lanes require an interactive macOS desktop. See [native investigation](native-investigation.md)
for the exact ABI, authority, graph and observation boundaries.
+10 -9
View File
@@ -8,14 +8,14 @@ protocol, authority, and resource-safety constraints, and report their effects.
## Choose the tool shape
| Shape | Use it for | Contract should return |
| -------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `inspect` | Facts about one explicit target, address, object, or resource | Relevant fields, source locations, and facet-level availability |
| `search` / `list` | Finding candidate targets or entities | Stable ordering and useful result context; expose pagination when the result format or caller's query needs it |
| `trace` | Relationships across code, metadata, UI resources, or observations | Typed edges, supporting evidence, and unresolved paths; identify any genuine traversal boundary |
| `compare` | Two explicitly identified artifacts, versions, or evidence sets | Paired identity, comparable coverage, and deltas with evidence |
| `workflow` | A distinct analyst outcome that benefits from composition inside REA | A useful inline result, contributing evidence, and partial/unavailable facets |
| `observe` / `capture` / `replay` | A question that requires runtime behavior | Required authority, launch/attach behavior, real operational constraints, lifecycle, and cleanup status |
| Shape | Use it for | Contract should return |
| --------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `inspect` | Facts about one explicit target, address, object, or resource | Relevant fields, source locations, and facet-level availability |
| `search` / `list` | Finding candidate targets or entities | Stable ordering and useful result context; expose pagination when the result format or caller's query needs it |
| `trace` | Relationships across code, metadata, UI resources, or observations | Typed edges, supporting evidence, and unresolved paths; identify any genuine traversal boundary |
| `compare` | Two explicitly identified artifacts, versions, or evidence sets | Paired identity, comparable coverage, and deltas with evidence |
| `workflow` | A distinct analyst outcome that benefits from composition inside REA | A useful inline result, contributing evidence, and partial/unavailable facets |
| `observe` / `capture` | A question that requires runtime behavior | Required authority, launch/attach behavior, real operational constraints, lifecycle, and cleanup status |
These are task shapes, not required prefixes. Name a tool for the action and
object agents reason about; keep the name distinct from nearby alternatives.
@@ -72,7 +72,8 @@ or model-authored loop when one call can answer the question.
- Separate observed facts from derived and inferred edges. Cite the evidence
supporting every important relationship; preserve unresolved edges.
- Declare read-only, mutation, process, filesystem, network, and UI effects
truthfully in the contract and permission boundary.
truthfully in the contract and enforce the target and lifecycle named in the
request.
- Keep provider-specific types out of provider-neutral domain and application
layers. Normalize supported provider results without implying equal coverage.
+5 -5
View File
@@ -55,7 +55,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
45.5,src/application/Uninstall.ts:removeClient restore,Configuration update fails and restoring the original also fails,"Configuration could not be updated or restored. Restore its `.rea.backup` manually, then rerun uninstall.","High risk; persistent configuration may be damaged, but retained backup is authoritative",Keep new recovery copy,implemented,verified by injected restoration failure test
46,src/application/Uninstall.ts:removeManagedPath symlink,Managed cache/state/skill path is a symbolic link,Refused to follow symlink {path}.,Exposes local path and does not explain manual safety check,"REA did not remove this item because its managed path is a symbolic link. Verify the link target before removing it manually.",implemented,focused assertion passed
47,src/application/Uninstall.ts:removeManagedPath failure,Managed item removal fails,{path} could not be removed.,Exposes local path and gives no recovery,"This item could not be removed. Check file permissions, then rerun uninstall.",implemented,verified by injected managed-path removal failure test
48,src/application/Doctor.ts:node,Runtime version is outside supported ranges,Install Node.js 22.19+ or 24.11+.,Low risk; exact dependency recovery,Keep current copy,unchanged,verified by doctor unit test
48,src/application/DoctorDiagnostics.ts:node,Runtime version is outside supported ranges,"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact dependency recovery,Keep current copy,unchanged,verified by doctor unit test
49,src/application/Doctor.ts:host,Host OS/distribution is unsupported,"REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.",Low risk; clearly states supported choices,Keep current copy,unchanged,verified by doctor unit tests
50,src/application/Doctor.ts:analysis engine,Optional deep-analysis executable cannot be found,"Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.",Low risk in setup-specific diagnostic; exact recovery paths,Keep current copy,unchanged,verified by doctor unit tests
51,src/application/Doctor.ts:target,Optional target path fails shared validation,Supply a readable local app or program path.,Low risk; tells caller valid target class,Keep current copy,unchanged,verified by doctor unit test
@@ -65,7 +65,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
55,src/application/Setup.ts:optional engine,Setup completes without optional deep-analysis engine,Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis.,Exposes provider terminology but this is an explanatory setup status rather than an error; recovery command is exact,Keep current copy,unchanged,verified by setup unit test
56,src/application/Setup.ts:doctor fallback,Post-setup diagnostics remain unhealthy,Run rea doctor and apply each reported remediation.,Low risk; delegates to structured per-check recovery,Keep current copy,unchanged,verified by exact setup unit test
57,src/application/Setup.ts:unsupported platform,Platform is neither macOS nor Linux,REA supports Hopper on macOS and selected 64-bit Linux distributions.,Low risk; states support boundary,Keep current copy,unchanged,verified by setup unit test
58,src/application/Setup.ts:unsupported Node,Runtime version cannot run setup,Install Node.js 22.19+ or 24.11+ and rerun setup.,Low risk; exact dependency and retry,Keep current copy,unchanged,verified by setup host tests
58,src/application/SetupHost.ts:unsupported Node,Runtime version cannot run setup,"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ and rerun setup.",Low risk; exact dependency and retry,Keep current copy,unchanged,verified by setup host tests
59,src/application/Setup.ts:unsupported macOS,macOS version is missing or older than 12,Upgrade to macOS 12 or newer.,Low risk; exact support floor,Keep current copy,unchanged,verified by setup host tests
60,src/application/Setup.ts:unsupported Linux,Linux distribution is outside supported set,"REA supports Hopper on Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.",Low risk; exact supported alternatives,Keep current copy,unchanged,verified by setup unit test
61,src/application/ProcessCli.ts:readJson read,Process scenario or evidence file cannot be read,Raw filesystem exception through CLI framework,Can expose local paths/codes and no recovery,"Process input file could not be read. Check that the path exists and is readable.",implemented,verified by focused CLI test
@@ -83,11 +83,11 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
74,install.sh:unknown option,Installer receives an unsupported flag,unknown option with caller-provided flag,Low risk; identifies offending input,Keep current copy,unchanged,verified by shell branch inspection
75,install.sh:platform,Installer runs outside macOS or supported Linux,REA supports macOS and selected 64-bit Linux distributions.,Low risk; states support boundary,Keep current copy,unchanged,verified by shell branch inspection
76,install.sh:curl missing,curl is absent,curl is required.,No recovery instruction,"curl is required. Install curl, then rerun this installer.",implemented,verified by exact missing-curl installer test
77,install.sh:node missing,node is absent,Node.js 22.19+ or 24.11+ is required; install it from nodejs.org and retry.,Low risk; exact versions and recovery,Keep current copy,unchanged,verified by shell branch inspection
77,install.sh:node missing,node is absent,"Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from nodejs.org and retry.",Low risk; exact versions and recovery,Keep current copy,unchanged,verified by shell branch inspection
78,install.sh:npm missing,npm is absent,npm is required; install it with Node.js and retry.,Low risk; exact recovery,Keep current copy,unchanged,verified by shell branch inspection
79,install.sh:node unreadable,node exists but does not report a parseable version,could not determine the active Node.js version.,No cause-local recovery,"The active Node.js version could not be read. Check that node works and is on PATH, then retry.",implemented,verified by exact unreadable-Node-version installer test
80,install.sh:Node 22 floor,Node 22 minor version is below 19,Node.js VERSION is unsupported; use Node.js 22.19+ or 24.11+.,Low risk; exact detected version and supported alternatives,Keep current copy,unchanged,verified by exact installer test using Node 20 and branch inspection
81,install.sh:Node 24 floor,Node 24 minor version is below 11,Node.js VERSION is unsupported; use Node.js 24.11 or newer.,Low risk; exact detected version and floor,Keep current copy,unchanged,verified by shell branch inspection
80,install.sh:Node 22 floor,Node 22 minor version is below 19,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact detected version and supported alternatives,Keep current copy,unchanged,verified by exact installer test using Node 20 and branch inspection
81,install.sh:Node 24 floor,Node 24 minor version is below 11,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact detected version and floor,Keep current copy,unchanged,verified by shell branch inspection
82,install.sh:version syntax,Explicit version is not exact semantic version,version must be an exact semantic version.,Low risk; exact correction,Keep current copy,unchanged,verified by shell branch inspection
83,install.sh:release lookup,Latest GitHub release request fails,could not resolve the latest GitHub release.,No fallback despite supporting pinned versions,"The latest REA release could not be resolved. Check network access or pass --version VERSION, then retry.",implemented,verified by exact release-lookup installer test
84,install.sh:release response,Latest release response is not valid expected JSON,Node JSON parser stack followed by the safe release-response message,Leaks a parser stack and buries recovery guidance,The release response was invalid. Retry later or pass --version VERSION.,implemented,verified by exact malformed-release test with parser stderr suppressed
1 harm_rank location trigger current_copy user_risk proposed_replacement implementation_status verification_result
55 45.5 src/application/Uninstall.ts:removeClient restore Configuration update fails and restoring the original also fails Configuration could not be updated or restored. Restore its `.rea.backup` manually, then rerun uninstall. High risk; persistent configuration may be damaged, but retained backup is authoritative Keep new recovery copy implemented verified by injected restoration failure test
56 46 src/application/Uninstall.ts:removeManagedPath symlink Managed cache/state/skill path is a symbolic link Refused to follow symlink {path}. Exposes local path and does not explain manual safety check REA did not remove this item because its managed path is a symbolic link. Verify the link target before removing it manually. implemented focused assertion passed
57 47 src/application/Uninstall.ts:removeManagedPath failure Managed item removal fails {path} could not be removed. Exposes local path and gives no recovery This item could not be removed. Check file permissions, then rerun uninstall. implemented verified by injected managed-path removal failure test
58 48 src/application/Doctor.ts:node src/application/DoctorDiagnostics.ts:node Runtime version is outside supported ranges Install Node.js 22.19+ or 24.11+. Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Low risk; exact dependency recovery Keep current copy unchanged verified by doctor unit test
59 49 src/application/Doctor.ts:host Host OS/distribution is unsupported REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux. Low risk; clearly states supported choices Keep current copy unchanged verified by doctor unit tests
60 50 src/application/Doctor.ts:analysis engine Optional deep-analysis executable cannot be found Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH. Low risk in setup-specific diagnostic; exact recovery paths Keep current copy unchanged verified by doctor unit tests
61 51 src/application/Doctor.ts:target Optional target path fails shared validation Supply a readable local app or program path. Low risk; tells caller valid target class Keep current copy unchanged verified by doctor unit test
65 55 src/application/Setup.ts:optional engine Setup completes without optional deep-analysis engine Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis. Exposes provider terminology but this is an explanatory setup status rather than an error; recovery command is exact Keep current copy unchanged verified by setup unit test
66 56 src/application/Setup.ts:doctor fallback Post-setup diagnostics remain unhealthy Run rea doctor and apply each reported remediation. Low risk; delegates to structured per-check recovery Keep current copy unchanged verified by exact setup unit test
67 57 src/application/Setup.ts:unsupported platform Platform is neither macOS nor Linux REA supports Hopper on macOS and selected 64-bit Linux distributions. Low risk; states support boundary Keep current copy unchanged verified by setup unit test
68 58 src/application/Setup.ts:unsupported Node src/application/SetupHost.ts:unsupported Node Runtime version cannot run setup Install Node.js 22.19+ or 24.11+ and rerun setup. Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ and rerun setup. Low risk; exact dependency and retry Keep current copy unchanged verified by setup host tests
69 59 src/application/Setup.ts:unsupported macOS macOS version is missing or older than 12 Upgrade to macOS 12 or newer. Low risk; exact support floor Keep current copy unchanged verified by setup host tests
70 60 src/application/Setup.ts:unsupported Linux Linux distribution is outside supported set REA supports Hopper on Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux. Low risk; exact supported alternatives Keep current copy unchanged verified by setup unit test
71 61 src/application/ProcessCli.ts:readJson read Process scenario or evidence file cannot be read Raw filesystem exception through CLI framework Can expose local paths/codes and no recovery Process input file could not be read. Check that the path exists and is readable. implemented verified by focused CLI test
83 74 install.sh:unknown option Installer receives an unsupported flag unknown option with caller-provided flag Low risk; identifies offending input Keep current copy unchanged verified by shell branch inspection
84 75 install.sh:platform Installer runs outside macOS or supported Linux REA supports macOS and selected 64-bit Linux distributions. Low risk; states support boundary Keep current copy unchanged verified by shell branch inspection
85 76 install.sh:curl missing curl is absent curl is required. No recovery instruction curl is required. Install curl, then rerun this installer. implemented verified by exact missing-curl installer test
86 77 install.sh:node missing node is absent Node.js 22.19+ or 24.11+ is required; install it from nodejs.org and retry. Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from nodejs.org and retry. Low risk; exact versions and recovery Keep current copy unchanged verified by shell branch inspection
87 78 install.sh:npm missing npm is absent npm is required; install it with Node.js and retry. Low risk; exact recovery Keep current copy unchanged verified by shell branch inspection
88 79 install.sh:node unreadable node exists but does not report a parseable version could not determine the active Node.js version. No cause-local recovery The active Node.js version could not be read. Check that node works and is on PATH, then retry. implemented verified by exact unreadable-Node-version installer test
89 80 install.sh:Node 22 floor Node 22 minor version is below 19 Node.js VERSION is unsupported; use Node.js 22.19+ or 24.11+. Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Low risk; exact detected version and supported alternatives Keep current copy unchanged verified by exact installer test using Node 20 and branch inspection
90 81 install.sh:Node 24 floor Node 24 minor version is below 11 Node.js VERSION is unsupported; use Node.js 24.11 or newer. Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Low risk; exact detected version and floor Keep current copy unchanged verified by shell branch inspection
91 82 install.sh:version syntax Explicit version is not exact semantic version version must be an exact semantic version. Low risk; exact correction Keep current copy unchanged verified by shell branch inspection
92 83 install.sh:release lookup Latest GitHub release request fails could not resolve the latest GitHub release. No fallback despite supporting pinned versions The latest REA release could not be resolved. Check network access or pass --version VERSION, then retry. implemented verified by exact release-lookup installer test
93 84 install.sh:release response Latest release response is not valid expected JSON Node JSON parser stack followed by the safe release-response message Leaks a parser stack and buries recovery guidance The release response was invalid. Retry later or pass --version VERSION. implemented verified by exact malformed-release test with parser stderr suppressed
@@ -1,5 +1,5 @@
{
"ledger_id": "ecl_b01c0b28362b48128765d176113471a5a82637fca0acfcd3182971155942e0d4",
"ledger_id": "ecl_132e857a8a93f0b366cd72f9cfa1c338ffd4100460a7834ef69abff936c84472",
"records": [
{
"claim_id": "managed.application-graph-projection",
@@ -78,13 +78,6 @@
"ev_f974f4b9d039cda841894c6173cae86427ecef48f4e7a4d6c73edf7b23af2b2a"
]
},
{
"claim_id": "managed.runtime-correlation-admission-plan",
"status": "pass",
"evidence_ids": [
"ev_3e546c20493faf9696502171a5cfe9504569cffb22f0561ee3000f0027b9bbd8"
]
},
{
"claim_id": "managed.unicode-obfuscated-identifiers",
"status": "pass",
@@ -101,8 +94,8 @@
}
],
"summary": {
"total": 14,
"pass": 12,
"total": 13,
"pass": 11,
"fail": 0,
"unsupported": 2,
"truncated": 0,
@@ -1,5 +1,5 @@
{
"manifest_id": "ecm_26db97ee1f42255040febd543bb2f3feac773f48d5ef38bd6651cd8770b36013",
"manifest_id": "ecm_125743de92b0cbb08a3cb11ee0d9305f60e5c4fb81152b8b748d348f91f99ee8",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "28122d8fd448db430249431aedfa9d3d865da8a323b703631fec5c44041d9d77"
"sha256": "26338313efd5c941e1f3e1f1d35e2605d47749f674c359458e76d5a470b0cbb1"
}
],
"claims": [
@@ -169,20 +169,6 @@
"version": "1"
}
},
{
"claim_id": "managed.runtime-correlation-admission-plan",
"scenario": {
"id": "runtime-correlation-admission-plan",
"version": 1
},
"artifact_sha256s": [
"cead4cc99733e4e56a5fd0cec8a0813bbc24e249aa72a10eca4c47ee3b0e129d"
],
"provider": {
"id": "rea-managed-conformance",
"version": "1"
}
},
{
"claim_id": "managed.unicode-obfuscated-identifiers",
"scenario": {
+109 -100
View File
@@ -1,156 +1,165 @@
# Windows Ghidra P0
Status: experimental. This boundary is useful for approved local fixtures, but
it is not general Windows REA support and must not be used for hostile,
sensitive, security-decision, or concurrently mutable targets.
Status: experimental Windows x64 support for the read-only P0 boundary. The
published package bundles native process, filesystem, and DACL controls. REA
uses them automatically when the analyst selects a Ghidra operation; no
additional permission flag or degraded mode is required.
## Supported boundary
The P0 accepts exactly:
- a Windows x64 host;
- Node.js 22.19+ or 24.11+;
- Windows 10+ x64 with fixed local NTFS target and temporary volumes;
- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+;
- an operator-installed official Ghidra 12.1.4 distribution;
- a 64-bit full JDK 21;
- an explicit native, non-managed, non-DLL x86-64 PE application; and
- the existing 19 read-only Ghidra inventory and function-analysis operations.
- the 25 read-only Ghidra inventory, memory, and function-analysis operations.
Hopper, Ghidra GUI state, mutation, controlled JavaScript replay, process
capture, browser/Electron observation, artifact extraction, and general
Windows feature parity are not implied by this boundary. Managed PE/CLI
inspection remains a separate execution-free provider and is not routed
through Windows Ghidra P0.
Loaded memory reads and file offsets preserve Ghidra's observed source mappings.
PE load-image inspection returns measurements with an explicit unsupported
attestation status; independent format-specific verification currently supports
DOS MZ only.
REA does not install or upgrade Ghidra, Java, Python, npm, Node.js, Hopper, or
another package on Windows. The production adapter uses the packaged Java
`HeadlessScript`; Python and PyGhidra are not prerequisites.
This boundary does not establish general Windows feature parity. Ghidra GUI
state, annotations, other target architectures and formats, and Hopper-only
operations remain unavailable through this provider. Managed PE/CLI inspection
uses its separate execution-free provider.
REA does not install or upgrade Ghidra, Java, Python, npm, Node.js, Hopper, or a
compiler. The adapter uses the packaged Java `HeadlessScript`; Python and
PyGhidra are not prerequisites. Users do not build the native addon.
## Configuration
Install REA, Ghidra, and JDK 21 separately. In PowerShell:
```powershell
npm install --global rea-agents
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
$env:JAVA_HOME = "C:\Program Files\Java\jdk-21"
$env:JAVA_HOME = "C:\tools\jdk-21"
$env:REA_ANALYSIS_PROVIDER = "ghidra"
rea doctor --json
rea providers --json
rea inspect "C:\approved-fixtures\sample.exe" --provider ghidra --format json
rea inspect "C:\fixtures\sample.exe" --provider ghidra --format json
```
`rea doctor` checks the x64 host, exact Ghidra release,
`support\analyzeHeadless.bat`, `java.exe`, `javac.exe`, JDK bitness, and JDK
major version. `rea setup` remains unavailable on Windows and makes no changes.
`rea doctor` distinguishes installation prerequisites from native artifact or
OS failures. It checks the Ghidra release, headless launcher, full JDK, x64
host, and native controls. A missing or mismatched addon requires the matching
Windows x64 package; changing MCP registration cannot supply that artifact.
A filesystem or access failure identifies the failed native constraint.
Other doctor checks, including unconfigured agent integrations, can remain
unready while Ghidra itself is available.
For an MCP client, register the resolved Node entry point and preserve the
three environment variables above. A representative configuration is:
`rea setup` can register supported agent integrations and the bundled skill.
Its existing preview and approval apply to configuration writes. Setup
preserves valid Ghidra and Java settings even when a separate runtime
prerequisite is unavailable.
For MCP, register the resolved Node entry point and the same provider settings:
```json
{
"mcpServers": {
"rea-windows-ghidra-p0": {
"command": "C:\\Program Files\\nodejs\\node.exe",
"args": [
"C:\\Users\\analyst\\AppData\\Roaming\\npm\\node_modules\\rea-agents\\scripts\\rea.mjs",
"mcp"
],
"rea": {
"command": "C:\\tools\\node\\node.exe",
"args": ["C:\\tools\\rea\\scripts\\rea.mjs", "mcp"],
"env": {
"REA_ANALYSIS_PROVIDER": "ghidra",
"GHIDRA_INSTALL_DIR": "C:\\tools\\ghidra_12.1.4_PUBLIC",
"JAVA_HOME": "C:\\Program Files\\Java\\jdk-21"
"JAVA_HOME": "C:\\tools\\jdk-21"
}
}
}
}
```
Resolve the actual global package path on the intended machine; do not copy the
example path blindly. Restart the MCP client after changing its registration.
These paths are examples. Use the actual package location and restart the MCP
client after registration changes. Cold Ghidra import can exceed a client's
short default request timeout; the real verification lane allows the existing
330-second provider startup deadline to complete.
## Target and import identity
## Native authority and target identity
PE parsing records architecture, COFF executable role, and whether the CLI
header data-directory entry is non-empty. Header classification and SHA-256
are read from the same open file handle. Windows admission then rejects:
The verified boundary excludes SUBST aliases and changing drive-letter/DOS-device
namespaces. REA does not claim a separate SUBST detector. Mounted-folder entries
that expose reparse tags fail component admission; broader namespace variants
have not been established by this lane. Requested paths, final handle paths,
volume serials, and file IDs remain separate observations.
- non-PE formats;
- x86, ARM, ARM64, or unknown architectures;
- DLL/shared-library and non-executable image roles;
- managed or incompletely classified PE targets; and
- malformed optional headers or data-directory declarations.
The loader validates package version, ABI, Node-API compatibility, PE machine,
and artifact SHA-256 before loading the package-owned addon. Native failure
reasons remain distinct from a valid installation and an unsupported target.
After admission, REA copies the target into the ephemeral Ghidra runtime and
requires the snapshot digest to match the admitted SHA-256. Ghidra imports only
that snapshot. Before serving a request, the Java bridge requires Ghidra's
`Program.getExecutableSHA256()` observation to match the same digest. A path
replacement that changes bytes therefore fails before Evidence is created.
PE header classification and SHA-256 come from the same open file. The Windows
provider admits native x86-64 PE applications and rejects unsupported roles,
architectures, managed images, and malformed headers. The original selected
source coordinate is preserved for native admission.
This linkage does not establish Windows reparse-point or hostile-path safety.
The current implementation has no handle-based `CreateFileW` authority backend
and no verified no-follow equivalent for every path component.
Admission opens and checks every path component without following a reparse
point. It retains handles, verifies local NTFS semantics, and observes volume
serial, 128-bit file ID, final handle path, requested path, and size. Reparse
paths, alternate streams, ambiguous coordinates, UNC paths, and other
filesystems are rejected with the actual constraint.
## Local transport and lifecycle
The snapshot streams from the admitted source handle on a background worker.
Its digest must match the parsed target digest. Ghidra imports that private,
immutable snapshot, and the bridge requires the imported program SHA-256 to
match before serving operations. Source and snapshot identities remain
separate diagnostic observations. Snapshot cancellation is checked between
64 KiB reads and cleanup waits for worker settlement.
Linux continues to use a mode-restricted Unix socket. Windows uses a Java
listener bound to `127.0.0.1` on an ephemeral port. The bridge atomically
publishes a bounded endpoint record containing only the literal loopback host
and port. The random 256-bit request token remains only in the
ephemeral session descriptor and every request is authenticated.
## Private runtime, transport, and lifecycle
`analyzeHeadless.bat` is invoked through an absolute `cmd.exe` with a fixed
argument sequence, command extensions enabled for the Ghidra script, delayed
expansion disabled, and Node shell execution disabled. Every batch token is
quoted. Paths containing command-interpreter metacharacters are rejected
instead of escaped heuristically.
Runtime creation installs a protected DACL permitting exactly the current user
and SYSTEM. Readback checks the actual owner and ACEs by handle. Descriptors
and snapshots remain locked until owner cleanup. Child files inherit the
private boundary. Analysis runs with the caller's authority; these controls
are not a sandbox for the Ghidra engine or other processes using the same user.
Close, cancellation, timeout, protocol failure, or provider exit removes the
project and runtime root. Windows P0 uses bounded `taskkill /T /F` process-tree
termination. It does not assign Ghidra to a Job Object and cannot make the same
PID-reuse and descendant-ownership claim as the POSIX process-group backend.
Windows uses an authenticated listener bound to `127.0.0.1` on an ephemeral
port. The endpoint record contains only the literal loopback host and port.
The bridge writes a new record and closes its writer before the native reader
can admit it. The reader refuses concurrent writers and verifies its opened
object's private DACL. The random 256-bit bearer token stays in the private,
immutable session descriptor and is redacted from diagnostic output.
## Known security gates
The batch launcher uses an absolute `cmd.exe`, fixed switches, delayed
expansion disabled, and quoted tokens. Command-interpreter metacharacters in
launch paths are unsupported. JVM home and temporary properties use quoted
`JDK_JAVA_OPTIONS`, preserving spaces. `APPDATA` and `LOCALAPPDATA` point into
the private runtime, isolating Ghidra preferences, logs, and caches.
The following remain explicitly unavailable:
- verified current-user-only DACLs for runtime directories, descriptors, logs,
snapshots, and endpoint records;
- a DACL-protected Windows named pipe or equivalent OS-identity-restricted IPC;
- reparse-point-safe, handle-based input and output authority;
- Job Object assignment before child execution and kill-on-close proof;
- general extraction containment and filesystem-sensitive workflows; and
- independent security acceptance for hostile or mutable targets.
Successful symlink creation or ConPTY availability does not satisfy any of
these gates. Inspect the machine-readable report with:
```powershell
npm run probe:windows-capabilities
```
Process creation assigns a suspended child atomically to an owned Job Object.
Membership and kill-on-owner-close policy are verified before resume.
Breakaway is disabled. Only the child's default file owner is normalized;
the caller token stays unchanged. Caller CPU affinity and priority are
preserved. Close, cancellation, timeout, startup failure, and provider failure
terminate the job and verify all members have settled before runtime cleanup.
Cleanup removes admitted objects by handle and unlinks reparse entries without
following their targets. Abrupt owner death kills the job but can leave
private runtime files; crash-time transactional deletion is not claimed.
## Verification
Hosted CI builds, type-checks, runs the curated Windows tests, records the named
host capability report, packs the real npm artifact, installs it in isolation,
and compares the packaged MCP catalog with `TOOL_CONTRACTS`.
The controlled real-engine lane is intentionally separate:
See [native controls](../native/windows/README.md) for the separate artifact
build lane and native boundary checks. Development tarballs without the built
artifact report native controls unavailable; publishing requires the verified
artifact and manifest.
```powershell
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
$env:JAVA_HOME = "C:\Program Files\Java\jdk-21"
npm ci
npm run verify:windows-native
npm run verify:ghidra:windows
npm run verify:ghidra:windows:package -- C:\fixtures\installed-rea C:\fixtures\sample.exe
```
The verifier creates an ignored deterministic x86-64 PE fixture, checks its
fixed SHA-256, opens Ghidra 12.1.4 through the production Java bridge, exercises
all 19 admitted operations, verifies the target/snapshot/import digest chain,
and checks endpoint, project, process, and runtime cleanup. The GitHub workflow
accepts only the fixed `real-ghidra-windows` repository-dispatch event against
default-branch `main`, a protected `real-ghidra-windows` environment, and a
self-hosted runner labelled `Windows`, `x64`, and `ghidra-12-1-2`.
The package lane must run with an ordinary, non-elevated user token. It exercises
CLI inspection and every admitted MCP operation, validates named canonical
input/output contracts, checks the source digest stays unchanged, and verifies
runtime cleanup. Use a source-owned fixture, one analysis process at a time,
and a bounded heap for real-provider verification.
Passing this verifier proves the bounded P0 operation claim. It does not close
the DACL, reparse-point, named-pipe, or Job Object gates above.
The controlled GitHub workflow runs only default-branch code on its protected
self-hosted Windows environment. A separate Linux job builds the matching
native artifact. Hosted Windows CI verifies native controls and packaged
startup separately from real Ghidra acceptance. Broader hostile-target,
concurrently mutable-path, extraction, and IPC-security claims require their
own acceptance and are not implied by this P0 result.
-28
View File
@@ -1,28 +0,0 @@
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist/**", "node_modules/**"] },
eslint.configs.recommended,
...tseslint.configs.strictTypeChecked,
{
files: ["**/*.ts"],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-confusing-void-expression": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-unnecessary-condition": "error",
"@typescript-eslint/no-unsafe-type-assertion": "error",
"@typescript-eslint/only-throw-error": "error",
"@typescript-eslint/require-await": "error",
"@typescript-eslint/switch-exhaustiveness-check": "error",
},
},
);
+4 -6
View File
@@ -55,17 +55,15 @@ fi
platform="$(uname -s)"
[[ "$platform" == "Darwin" || "$platform" == "Linux" ]] || fail "REA supports macOS and selected 64-bit Linux distributions."
command -v curl >/dev/null 2>&1 || fail "curl is required. Install curl, then rerun this installer."
command -v node >/dev/null 2>&1 || fail "Node.js 22.19+ or 24.11+ is required; install it from https://nodejs.org and retry."
command -v node >/dev/null 2>&1 || fail "Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from https://nodejs.org and retry."
command -v npm >/dev/null 2>&1 || fail "npm is required; install it with Node.js and retry."
node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
IFS=. read -r node_major node_minor _node_patch <<<"$node_version"
[[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "the active Node.js version could not be read. Check that node works and is on PATH, then retry."
if ! ((node_major == 22 && node_minor >= 19 || node_major >= 24)); then
fail "Node.js $node_version is unsupported; use Node.js 22.19+ or 24.11+."
fi
if ((node_major == 24 && node_minor < 11)); then
fail "Node.js $node_version is unsupported; use Node.js 24.11 or newer."
if ! [[ "$_node_patch" =~ ^[0-9]+(\+[0-9A-Za-z.-]+)?$ ]] ||
! ((node_major == 22 && node_minor >= 19 || node_major == 24 && node_minor >= 11 || node_major >= 26)); then
fail "Node.js $node_version is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+."
fi
if [[ -n "$version" ]]; then
-7
View File
@@ -3,14 +3,7 @@
"entry": ["src/main.ts", "src/cli.ts", "src/contracts/errorSchemas.ts"],
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
"ignoreDependencies": ["electron"],
"ignoreBinaries": ["ps"],
"ignoreExportsUsedInFile": true,
"ignoreIssues": {
"src/domain/mobileApplicationInvestigation.ts": ["types"],
"src/domain/objcSwiftMetadata.ts": ["types"],
"src/domain/interfaceBuilderGraph.ts": ["types"],
"src/domain/nativeInvestigationGraph.ts": ["types"]
},
"rules": {
"files": "error",
"dependencies": "error",
+102
View File
@@ -0,0 +1,102 @@
# Windows native controls
REA ships one Windows x64 Node-API 8 addon. Users do not compile it or install
another runtime. Ghidra and a full JDK remain bring-your-own prerequisites.
The addon uses Windows 10+ APIs and admits fixed local NTFS volumes only.
The package owns the artifact path. The loader checks the package version,
ABI, Node-API compatibility, PE architecture, and SHA-256 before loading it.
There is no environment override for loading an arbitrary native module.
## Build
The artifact lane requires Linux, MinGW-w64 `x86_64-w64-mingw32-g++`,
`x86_64-w64-mingw32-dlltool`, and `tar`. It never installs these commands.
Node-API headers are reused from the build cache or downloaded from the
pinned official Node release with its published SHA-256 checked.
```sh
npm run build:windows-native
npm run verify:windows-native:artifact
npm pack
```
`build/` is ignored by Git. The npm file inventory includes only the addon and
its manifest. Release CI builds both, and `prepublishOnly` rejects a missing,
stale, mismatched, or wrong-architecture artifact. A development tarball built
without this lane reports Windows native controls as unavailable.
## Boundaries
- Opaque Node objects carry a native type tag, resource kind, and closed state.
A pathname, serialized object, or forged handle cannot grant authority.
- Input admission opens every component with `OPEN_REPARSE_POINT`, verifies
the actual NTFS object, and retains handles that deny replacement and source
writing. An admitted child keeps each ancestor nonempty; ancestor handles
permit unrelated writes while NTFS rejects in-place reparse conversion. Volume serial, 128-bit file ID, final handle path,
requested path, size, and digest observations are preserved separately.
- New runtime roots and private child objects are created relative to admitted
parent handles with `NtCreateFile`. A protected DACL and current-user owner
are installed at creation. Readback permits exactly the current user and SYSTEM. Child
files inherit this boundary; private files are also checked by handle.
- Snapshots stream through a 64 KiB buffer on a Node async worker. Cancellation
is checked between reads, cleanup waits for settlement, and the digest
commits to the bytes written. Descriptor and snapshot handles remain held
until runtime cleanup.
Snapshot ownership is single-flight per runtime: an overlapping request fails
with `ERROR_BUSY` before acquiring cancellation ownership. The TypeScript owner
records only the accepted promise and abort listener; aborting a rejected copy
must not cancel the active copy.
- `PROC_THREAD_ATTRIBUTE_JOB_LIST` assigns a suspended child atomically to an
unnamed, non-inherited Job Object. Membership and kill-on-close policy are
checked before resume. Breakaway is disabled, and cleanup waits for all
job members and captured output, including detached descendants.
- Only the suspended child's default file owner is normalized. Caller token
privileges and ownership remain unchanged. Caller CPU affinity and process
priority are preserved in the child. Environment names are sorted and
deduplicated using Windows ordinal comparison, preserving their native Unicode
semantics and caller-selected values.
- Cleanup walks locked directory objects, removes files by handle, and unlinks
reparse entries without traversing their targets. Normal close, cancellation,
startup failure, and timeout release provider resources. Abrupt owner death
kills the owned job; it can leave private runtime files for later operator
cleanup and does not claim transactional deletion after a crash.
Windows failures retain their constraint, requested coordinate, Win32 code,
and system message. Unsupported filesystems and reparse paths are distinct
from OS access denial and missing packaged controls.
The bearer-token descriptor remains under its immutable private lease until
`runtime_close` on Windows; POSIX removes the descriptor after the bridge reads
it. Job ownership guarantees process termination on owner death, not deletion
of private files after a crash. SUBST drive aliases and changing DOS-device
namespaces are outside the verified P0 scope and are not detected as a separate
alias policy. Mounted-folder paths that report a reparse tag are rejected by
component admission; broader namespace variants remain unverified. Preserve
requested and final handle coordinates rather than treating them as one identity.
## Real verification
```powershell
npm run verify:windows-native
npm run verify:ghidra:windows
```
The native lane checks DACL readback independently, source write/replacement
rejection, unrelated sibling renames, reparse rejection, immutable snapshots,
asynchronous cancellation, cleanup containment,
descendant termination, unrelated-process preservation, normal exit, owner
close, and forced owner exit. An optional independently built
`tests/fixtures/windows/processBoundary.cc` executable adds breakaway and
caller/child token observations and an in-place ancestor reparse attempt:
```powershell
node scripts/verify-windows-native.mjs . C:\fixtures\process-boundary.exe
```
`nonAdminRunner.cc` is a test-only launcher for an elevated development shell.
It duplicates an existing non-elevated Explorer token belonging to the same
user, creates private temporary window-station objects, and launches the test
inside a kill-on-close fixture job. It does not create accounts, modify host
policy, or change the caller's token. A real-provider claim must additionally
verify the packaged CLI and MCP on that ordinary-user token.
+123
View File
@@ -0,0 +1,123 @@
#include "authority.hpp"
namespace rea {
static const napi_type_tag RESOURCE_TAG = {0x45fcbf0b49e84fe3ULL, 0x9e3e5074130de843ULL};
void check(napi_status status) {
if (status != napi_ok) throw Failure("Invalid native boundary value", L"", ERROR_INVALID_PARAMETER);
}
napi_value object(napi_env env) { napi_value value; check(napi_create_object(env, &value)); return value; }
napi_value null(napi_env env) { napi_value value; check(napi_get_null(env, &value)); return value; }
napi_value string(napi_env env, const std::string& text) {
napi_value value; check(napi_create_string_utf8(env, text.data(), text.size(), &value)); return value;
}
napi_value string(napi_env env, const std::wstring& text) {
static_assert(sizeof(wchar_t) == sizeof(char16_t));
napi_value value;
check(napi_create_string_utf16(env, reinterpret_cast<const char16_t*>(text.data()), text.size(), &value));
return value;
}
napi_value number(napi_env env, double numeric) {
napi_value value; check(napi_create_double(env, numeric, &value)); return value;
}
napi_value boolean(napi_env env, bool flag) {
napi_value value; check(napi_get_boolean(env, flag, &value)); return value;
}
void set(napi_env env, napi_value target, const char* key, napi_value value) {
check(napi_set_named_property(env, target, key, value));
}
std::wstring wide(napi_env env, napi_value value) {
size_t size = 0;
check(napi_get_value_string_utf16(env, value, nullptr, 0, &size));
require(size <= 32767, "Windows value exceeds the native string limit", L"", ERROR_INVALID_PARAMETER);
std::vector<char16_t> buffer(size + 1);
check(napi_get_value_string_utf16(env, value, buffer.data(), buffer.size(), &size));
std::wstring result(reinterpret_cast<const wchar_t*>(buffer.data()), size);
require(result.find(L'\0') == std::wstring::npos, "Windows value contains NUL", L"", ERROR_INVALID_PARAMETER);
return result;
}
double numeric(napi_env env, napi_value value) {
double result; check(napi_get_value_double(env, value, &result)); return result;
}
std::vector<napi_value> array(napi_env env, napi_value value) {
bool valid; check(napi_is_array(env, value, &valid));
require(valid, "Native arguments must be an array", L"", ERROR_INVALID_PARAMETER);
uint32_t length; check(napi_get_array_length(env, value, &length));
std::vector<napi_value> result(length);
for (uint32_t index = 0; index < length; ++index)
check(napi_get_element(env, value, index, &result[index]));
return result;
}
static void finalize(napi_env, void* data, void*) { delete static_cast<Resource*>(data); }
napi_value wrap(napi_env env, std::unique_ptr<Resource> value) {
auto result = object(env);
check(napi_type_tag_object(env, result, &RESOURCE_TAG));
check(napi_wrap(env, result, value.get(), finalize, nullptr, nullptr));
value.release();
return result;
}
Resource& resource(napi_env env, napi_value value, Kind kind) {
bool tagged = false;
check(napi_check_object_type_tag(env, value, &RESOURCE_TAG, &tagged));
require(tagged, "Native handle does not belong to REA", L"", ERROR_INVALID_HANDLE);
void* pointer = nullptr; check(napi_unwrap(env, value, &pointer));
auto& result = *static_cast<Resource*>(pointer);
require(result.kind == kind && !result.closed, "Native handle is closed or has the wrong kind", L"", ERROR_INVALID_HANDLE);
return result;
}
napi_value failureValue(napi_env env, const Failure& failure) {
wchar_t* message = nullptr;
FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, failure.win32, 0, reinterpret_cast<wchar_t*>(&message), 0, nullptr);
std::wstring detail = message == nullptr ? L"" : message;
if (message != nullptr) LocalFree(message);
const std::wstring constraint(failure.constraint.begin(), failure.constraint.end());
auto text = string(env, constraint + (failure.path.empty() ? L"" : L" [" + failure.path + L"]") +
L" (Win32 " + std::to_wstring(failure.win32) + L"): " + detail);
napi_value error; napi_create_error(env, nullptr, text, &error);
const char* code = failure.win32 == ERROR_FILE_NOT_FOUND || failure.win32 == ERROR_PATH_NOT_FOUND ? "ENOENT"
: failure.win32 == ERROR_ACCESS_DENIED ? "EACCES" : "ERR_REA_WINDOWS_NATIVE";
set(env, error, "code", string(env, std::string(code)));
set(env, error, "constraint", string(env, failure.constraint));
set(env, error, "requestedPath", string(env, failure.path));
set(env, error, "win32Code", number(env, failure.win32));
set(env, error, "win32Message", string(env, detail));
return error;
}
static napi_value call(napi_env env, napi_callback_info info) {
try {
std::array<napi_value, 2> args;
size_t count = args.size();
check(napi_get_cb_info(env, info, &count, args.data(), nullptr, nullptr));
require(count == 2, "Native call requires operation and arguments", L"", ERROR_INVALID_PARAMETER);
const auto operation = wide(env, args[0]);
const auto values = array(env, args[1]);
if (operation == L"inspect") {
require(values.empty(), "Inspect takes no arguments", L"", ERROR_INVALID_PARAMETER);
return inspect(env);
}
if (operation.rfind(L"process_", 0) == 0) return processCall(env, operation, values);
return filesystemCall(env, operation, values);
} catch (const Failure& failure) {
auto error = failureValue(env, failure);
napi_throw(env, error);
return nullptr;
} catch (const std::exception& failure) {
napi_throw_error(env, "ERR_REA_WINDOWS_NATIVE", failure.what());
return nullptr;
}
}
static napi_value initialize(napi_env env, napi_value exports) {
napi_value function;
if (napi_create_function(env, "call", NAPI_AUTO_LENGTH, call, nullptr, &function) != napi_ok) return nullptr;
if (napi_set_named_property(env, exports, "call", function) != napi_ok) return nullptr;
return exports;
}
} // namespace rea
NAPI_MODULE(NODE_GYP_MODULE_NAME, rea::initialize)
+120
View File
@@ -0,0 +1,120 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include <aclapi.h>
#include <bcrypt.h>
#include <node_api.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <cstdint>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace rea {
struct Failure : std::runtime_error {
DWORD win32;
std::wstring path;
std::string constraint;
Failure(std::string constraint, std::wstring path = L"", DWORD code = GetLastError())
: std::runtime_error(constraint), win32(code), path(std::move(path)),
constraint(std::move(constraint)) {}
};
inline void require(bool condition, const char* constraint, const std::wstring& path = L"") {
if (!condition) {
const auto code = GetLastError();
throw Failure(constraint, path, code);
}
}
inline void require(bool condition, const char* constraint, const std::wstring& path, DWORD code) {
if (!condition) throw Failure(constraint, path, code);
}
class Handle {
HANDLE value_ = INVALID_HANDLE_VALUE;
public:
Handle() = default;
explicit Handle(HANDLE value) : value_(value) {}
~Handle() { reset(); }
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
Handle(Handle&& other) noexcept : value_(other.release()) {}
Handle& operator=(Handle&& other) noexcept { reset(other.release()); return *this; }
HANDLE get() const { return value_; }
bool valid() const { return value_ != INVALID_HANDLE_VALUE && value_ != nullptr; }
HANDLE release() { return std::exchange(value_, INVALID_HANDLE_VALUE); }
void reset(HANDLE next = INVALID_HANDLE_VALUE) {
if (valid()) CloseHandle(value_);
value_ = next;
}
};
enum class Kind { File, Runtime, Process };
struct Resource {
const Kind kind;
bool closed = false;
explicit Resource(Kind value) : kind(value) {}
virtual ~Resource() = default;
};
struct File : Resource {
std::wstring requestedPath;
std::wstring path;
std::vector<Handle> handles;
explicit File(std::wstring path) : Resource(Kind::File), requestedPath(path), path(std::move(path)) {}
HANDLE get() const { return handles.back().get(); }
};
struct Runtime : Resource {
std::wstring path;
std::vector<Handle> parents;
Handle directory;
std::vector<Handle> immutableFiles;
// Main-thread admission is single-flight; only the accepted copy owns the
// atomic cancellation flag consumed by its async worker. ERROR_BUSY rejects
// another copy before it can reset that flag or replace pending ownership.
bool snapshotPending = false;
std::atomic_bool snapshotCancelled{false};
Runtime() : Resource(Kind::Runtime) {}
};
struct Process : Resource {
Handle job, process, stdoutRead, stderrRead;
DWORD pid = 0;
Process() : Resource(Kind::Process) {}
};
void check(napi_status status);
napi_value failureValue(napi_env env, const Failure& failure);
napi_value object(napi_env env);
napi_value string(napi_env env, const std::string& value);
napi_value string(napi_env env, const std::wstring& value);
napi_value number(napi_env env, double value);
napi_value boolean(napi_env env, bool value);
napi_value null(napi_env env);
void set(napi_env env, napi_value object, const char* key, napi_value value);
std::wstring wide(napi_env env, napi_value value);
double numeric(napi_env env, napi_value value);
std::vector<napi_value> array(napi_env env, napi_value value);
napi_value wrap(napi_env env, std::unique_ptr<Resource> resource);
Resource& resource(napi_env env, napi_value value, Kind kind);
std::unique_ptr<File> openFile(const std::wstring& path, DWORD access = GENERIC_READ,
bool directory = false);
napi_value identity(napi_env env, HANDLE handle, const std::wstring& path);
std::unique_ptr<Runtime> createRuntime(const std::wstring& parent, const std::wstring& prefix);
void closeRuntime(Runtime& runtime);
napi_value filesystemCall(napi_env env, const std::wstring& operation,
const std::vector<napi_value>& args);
std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::wstring& commandLine,
const std::wstring& cwd, const std::vector<std::wstring>& environment);
napi_value processCall(napi_env env, const std::wstring& operation,
const std::vector<napi_value>& args);
napi_value inspect(napi_env env);
} // namespace rea
+531
View File
@@ -0,0 +1,531 @@
#include "authority.hpp"
#include <sddl.h>
#include <winternl.h>
namespace rea {
static std::wstring hex(const unsigned char* bytes, size_t count) {
constexpr wchar_t digits[] = L"0123456789abcdef";
std::wstring result;
for (size_t index = 0; index < count; ++index) {
result += digits[bytes[index] >> 4]; result += digits[bytes[index] & 15];
}
return result;
}
static std::wstring localPath(std::wstring path) {
if (path.rfind(L"\\\\?\\", 0) == 0) path.erase(0, 4);
require(path.size() >= 3 && path[1] == L':' && path[2] == L'\\' &&
((path[0] >= L'A' && path[0] <= L'Z') || (path[0] >= L'a' && path[0] <= L'z')),
"Only absolute local NTFS drive paths are supported", path, ERROR_NOT_SUPPORTED);
require(path.find(L'/', 0) == std::wstring::npos && path.find(L':', 2) == std::wstring::npos,
"Alternate streams and non-local path syntax are unsupported", path, ERROR_NOT_SUPPORTED);
require(GetDriveTypeW(path.substr(0, 3).c_str()) == DRIVE_FIXED,
"Only fixed local NTFS volumes are supported", path, ERROR_NOT_SUPPORTED);
if (path.size() > 3 && path.back() == L'\\') path.pop_back();
require(path.size() == 3 || path.back() != L'\\', "Repeated path separators are unsupported", path, ERROR_NOT_SUPPORTED);
size_t begin = 3;
while (begin < path.size()) {
const auto end = path.find(L'\\', begin);
const auto component = path.substr(begin, end == std::wstring::npos ? end : end - begin);
require(!component.empty() && component != L"." && component != L".." &&
component.back() != L'.' && component.back() != L' ',
"Ambiguous Windows path component is unsupported", path, ERROR_NOT_SUPPORTED);
if (end == std::wstring::npos) break;
begin = end + 1;
}
return path;
}
static Handle openComponent(const std::wstring& path, DWORD access, bool directory, DWORD sharing = FILE_SHARE_READ) {
// Attribute-only opens do not participate in Windows share-access checks.
// Directory guards need FILE_LIST_DIRECTORY to prevent replacement and
// in-place reparse conversion while descendant paths are being consumed.
Handle handle(CreateFileW((L"\\\\?\\" + path).c_str(), access | FILE_READ_ATTRIBUTES | READ_CONTROL |
(directory ? FILE_LIST_DIRECTORY : 0),
sharing, nullptr, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr));
require(handle.valid(), "Open Windows path component failed", path);
FILE_ATTRIBUTE_TAG_INFO info{};
require(GetFileInformationByHandleEx(handle.get(), FileAttributeTagInfo, &info, sizeof(info)),
"Read Windows path attributes failed", path);
if ((info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
throw Failure("Reparse path component is unsupported (tag " + std::to_string(info.ReparseTag) + ")",
path, ERROR_NOT_SUPPORTED);
require(((info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) == directory,
directory ? "Expected a directory path component" : "Target is not a regular file", path,
directory ? ERROR_PATH_NOT_FOUND : ERROR_DIRECTORY);
require(GetFileType(handle.get()) == FILE_TYPE_DISK, "Windows object is not a disk file", path, ERROR_NOT_SUPPORTED);
return handle;
}
std::unique_ptr<File> openFile(const std::wstring& requested, DWORD access, bool directory) {
auto result = std::make_unique<File>(localPath(requested));
result->requestedPath = requested;
const auto& path = result->path;
require(directory || path.size() > 3, "Target is not a regular file", requested, ERROR_DIRECTORY);
result->handles.push_back(openComponent(path.substr(0, 3), 0, true));
wchar_t filesystem[64]{};
DWORD flags = 0;
require(GetVolumeInformationByHandleW(result->handles.front().get(), nullptr, 0, nullptr, nullptr,
&flags, filesystem, 64), "Read volume semantics failed", path);
require(std::wstring(filesystem) == L"NTFS" && (flags & FILE_PERSISTENT_ACLS) != 0,
"Backing volume must be NTFS with persistent ACLs", path, ERROR_NOT_SUPPORTED);
size_t offset = 3;
while (offset < path.size()) {
auto end = path.find(L'\\', offset);
const bool final = end == std::wstring::npos;
auto child = openComponent(path.substr(0, end), final ? access : 0, !final || directory);
// Once the next component is pinned against deletion, its parent cannot
// become empty and NTFS cannot convert that parent into a reparse point.
// Relax only ancestor write sharing so unrelated child renames can proceed.
// Independent evidence: tests/fixtures/windows/processBoundary.cc attempts
// FSCTL_SET_REPARSE_POINT on the nonempty pinned ancestor and observes denial.
// SUBST aliases and mounted-folder/reparse namespace variants are outside
// the admitted P0 evidence; requested and final handle paths remain distinct.
const auto parentPath = offset == 3 ? path.substr(0, 3) : path.substr(0, offset - 1);
result->handles.back() = openComponent(parentPath, 0, true, FILE_SHARE_READ | FILE_SHARE_WRITE);
result->handles.push_back(std::move(child));
if (final) break;
offset = end + 1;
}
return result;
}
napi_value identity(napi_env env, HANDLE handle, const std::wstring& path) {
FILE_ID_INFO info{};
require(GetFileInformationByHandleEx(handle, FileIdInfo, &info, sizeof(info)),
"Stable 128-bit Windows file identity is unavailable", path);
std::array<wchar_t, 32768> finalPath;
const auto length = GetFinalPathNameByHandleW(handle, finalPath.data(), finalPath.size(), FILE_NAME_NORMALIZED);
require(length > 0 && length < finalPath.size(), "Read final handle path failed", path);
LARGE_INTEGER size{};
FILE_STANDARD_INFO standard{};
require(GetFileInformationByHandleEx(handle, FileStandardInfo, &standard, sizeof(standard)),
"Read handle metadata failed", path);
size = standard.EndOfFile;
require(size.QuadPart >= 0 && size.QuadPart <= 9007199254740991LL, "File size exceeds exact numeric representation", path,
ERROR_NOT_SUPPORTED);
auto result = object(env);
set(env, result, "requestedPath", string(env, path));
set(env, result, "finalPath", string(env, std::wstring(finalPath.data(), length)));
set(env, result, "filesystem", string(env, std::string("NTFS")));
std::array<unsigned char, 8> serial;
for (size_t index = 0; index < serial.size(); ++index)
serial[index] = static_cast<unsigned char>(info.VolumeSerialNumber >> ((7 - index) * 8));
set(env, result, "volumeSerial", string(env, hex(serial.data(), serial.size())));
set(env, result, "fileId", string(env, hex(info.FileId.Identifier, 16)));
set(env, result, "size", number(env, static_cast<double>(size.QuadPart)));
set(env, result, "directory", boolean(env, standard.Directory != FALSE));
return result;
}
struct Security {
std::vector<unsigned char> user;
std::array<unsigned char, SECURITY_MAX_SID_SIZE> system{};
std::vector<unsigned char> acl;
SECURITY_DESCRIPTOR descriptor{};
PSID userSid() const { return reinterpret_cast<const TOKEN_USER*>(user.data())->User.Sid; }
Security() {
Handle token;
HANDLE raw;
require(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw), "Read current user token failed");
token.reset(raw);
DWORD size = 0;
GetTokenInformation(token.get(), TokenUser, nullptr, 0, &size);
user.resize(size);
require(GetTokenInformation(token.get(), TokenUser, user.data(), size, &size), "Read current user SID failed");
DWORD systemSize = system.size();
require(CreateWellKnownSid(WinLocalSystemSid, nullptr, system.data(), &systemSize), "Create SYSTEM SID failed");
acl.resize(sizeof(ACL) + 2 * (sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD)) + GetLengthSid(userSid()) + systemSize);
auto access = reinterpret_cast<PACL>(acl.data());
require(InitializeAcl(access, acl.size(), ACL_REVISION), "Initialize private DACL failed");
for (PSID sid : {userSid(), static_cast<PSID>(system.data())})
require(AddAccessAllowedAceEx(access, ACL_REVISION, OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE,
FILE_ALL_ACCESS, sid), "Populate private DACL failed");
require(InitializeSecurityDescriptor(&descriptor, SECURITY_DESCRIPTOR_REVISION) &&
SetSecurityDescriptorOwner(&descriptor, userSid(), FALSE) &&
SetSecurityDescriptorDacl(&descriptor, TRUE, access, FALSE) &&
SetSecurityDescriptorControl(&descriptor, SE_DACL_PROTECTED, SE_DACL_PROTECTED),
"Create protected security descriptor failed");
}
};
static Handle createRelative(HANDLE parent, const std::wstring& name, DWORD access, bool directory,
Security& security, const std::wstring& path) {
require(!name.empty() && name.find_first_of(L"\\/:\"<>|?*") == std::wstring::npos && name.size() < 32768,
"Private object requires one filename component", path, ERROR_INVALID_PARAMETER);
UNICODE_STRING coordinate{};
coordinate.Buffer = const_cast<wchar_t*>(name.data());
coordinate.Length = static_cast<USHORT>(name.size() * sizeof(wchar_t));
coordinate.MaximumLength = coordinate.Length;
OBJECT_ATTRIBUTES attributes;
InitializeObjectAttributes(&attributes, &coordinate, OBJ_CASE_INSENSITIVE, parent, &security.descriptor);
IO_STATUS_BLOCK observation{};
HANDLE raw = INVALID_HANDLE_VALUE;
const auto status = NtCreateFile(&raw, access | SYNCHRONIZE, &attributes, &observation, nullptr,
FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_CREATE,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT |
(directory ? FILE_DIRECTORY_FILE : FILE_NON_DIRECTORY_FILE), nullptr, 0);
if (status < 0) throw Failure("Create private object relative to its admitted parent failed", path,
RtlNtStatusToDosError(status));
Handle result(raw);
require(result.valid() && observation.Information == FILE_CREATED,
"Private creation did not return a new object handle", path, ERROR_ACCESS_DENIED);
return result;
}
static void verifyPrivate(HANDLE handle, const std::wstring& path, bool root) {
Security expected;
PSID owner = nullptr;
PACL acl = nullptr;
PSECURITY_DESCRIPTOR raw = nullptr;
const DWORD code = GetSecurityInfo(handle, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
&owner, nullptr, &acl, nullptr, &raw);
if (code != ERROR_SUCCESS) throw Failure("Read private DACL by handle failed", path, code);
const auto descriptor = std::unique_ptr<void, decltype(&LocalFree)>(raw, LocalFree);
require(owner != nullptr && EqualSid(owner, expected.userSid()), "Private object owner is not the current user", path,
ERROR_ACCESS_DENIED);
SECURITY_DESCRIPTOR_CONTROL control;
DWORD revision;
require(GetSecurityDescriptorControl(raw, &control, &revision), "Read DACL control flags failed", path);
require(acl != nullptr && (control & SE_DACL_PRESENT) != 0 && (!root || (control & SE_DACL_PROTECTED) != 0),
"Private object requires a present protected DACL", path, ERROR_ACCESS_DENIED);
require(acl->AceCount == 2, "Private DACL contains unexpected trustees", path, ERROR_ACCESS_DENIED);
bool user = false, system = false;
for (DWORD index = 0; index < acl->AceCount; ++index) {
void* rawAce;
require(GetAce(acl, index, &rawAce), "Read private DACL entry failed", path);
const auto ace = static_cast<ACCESS_ALLOWED_ACE*>(rawAce);
require(ace->Header.AceType == ACCESS_ALLOWED_ACE_TYPE && ace->Mask == FILE_ALL_ACCESS &&
(ace->Header.AceFlags & INHERIT_ONLY_ACE) == 0 &&
(!root || (ace->Header.AceFlags & (OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE)) ==
(OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE)),
"Private DACL contains unsupported rights or ACE type", path, ERROR_ACCESS_DENIED);
PSID sid = const_cast<DWORD*>(&ace->SidStart);
if (EqualSid(sid, expected.userSid())) user = true;
else if (EqualSid(sid, expected.system.data())) system = true;
else throw Failure("Private DACL permits an unrelated principal", path, ERROR_ACCESS_DENIED);
}
require(user && system, "Private DACL must allow only current user and SYSTEM", path, ERROR_ACCESS_DENIED);
}
std::unique_ptr<Runtime> createRuntime(const std::wstring& parentPath, const std::wstring& prefix) {
auto parent = openFile(parentPath, 0, true);
require(!prefix.empty() && prefix.find_first_of(L"\\/:\"<>|?*") == std::wstring::npos,
"Runtime prefix must be one filename component", parentPath, ERROR_INVALID_PARAMETER);
std::array<unsigned char, 16> random;
require(BCryptGenRandom(nullptr, random.data(), random.size(), BCRYPT_USE_SYSTEM_PREFERRED_RNG) == 0,
"Generate runtime identity failed", parentPath, ERROR_GEN_FAILURE);
auto result = std::make_unique<Runtime>();
result->path = parent->path + L"\\" + prefix + hex(random.data(), random.size());
Security security;
result->directory = createRelative(parent->get(), prefix + hex(random.data(), random.size()),
DELETE | FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | READ_CONTROL,
true, security, result->path);
try {
verifyPrivate(result->directory.get(), result->path, true);
// The owned child pins the parent nonempty before write sharing is relaxed.
parent->handles.back() = openComponent(parent->path, 0, true, FILE_SHARE_READ | FILE_SHARE_WRITE);
result->parents = std::move(parent->handles);
} catch (...) {
FILE_DISPOSITION_INFO deletion{TRUE};
SetFileInformationByHandle(result->directory.get(), FileDispositionInfo, &deletion, sizeof(deletion));
throw;
}
return result;
}
static std::wstring relativePath(Runtime& root, const std::wstring& relative) {
require(!relative.empty() && relative[0] != L'\\' && relative.find(L':') == std::wstring::npos,
"Runtime coordinate must be a relative path", relative, ERROR_INVALID_PARAMETER);
return localPath(root.path + L"\\" + relative);
}
static std::unique_ptr<File> runtimeDirectory(Runtime& root, const std::wstring& path) {
auto result = std::make_unique<File>(path);
HANDLE duplicate;
require(DuplicateHandle(GetCurrentProcess(), root.directory.get(), GetCurrentProcess(), &duplicate, 0, FALSE,
DUPLICATE_SAME_ACCESS), "Duplicate private directory handle failed", path);
result->handles.emplace_back(duplicate);
size_t offset = root.path.size() + 1;
while (offset < path.size()) {
const auto end = path.find(L'\\', offset);
result->handles.push_back(openComponent(path.substr(0, end), 0, true));
if (end == std::wstring::npos) break;
offset = end + 1;
}
return result;
}
static void directory(Runtime& root, const std::wstring& relative) {
const auto path = relativePath(root, relative);
const auto parentPath = path.substr(0, path.find_last_of(L'\\'));
auto parent = runtimeDirectory(root, parentPath);
verifyPrivate(parent->get(), parentPath, parentPath == root.path);
Security security;
try {
auto created = createRelative(parent->get(), path.substr(path.find_last_of(L'\\') + 1),
FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | READ_CONTROL,
true, security, path);
verifyPrivate(created.get(), path, false);
} catch (const Failure& failure) {
if (failure.win32 != ERROR_ALREADY_EXISTS && failure.win32 != ERROR_FILE_EXISTS) throw;
auto existing = runtimeDirectory(root, path);
verifyPrivate(existing->get(), path, false);
}
}
static Handle createPrivateFile(Runtime& root, const std::wstring& relative) {
const auto path = relativePath(root, relative);
const auto parentPath = path.substr(0, path.find_last_of(L'\\'));
auto parent = runtimeDirectory(root, parentPath);
verifyPrivate(parent->get(), parentPath, parentPath == root.path);
Security security;
auto file = createRelative(parent->get(), path.substr(path.find_last_of(L'\\') + 1),
GENERIC_WRITE | GENERIC_READ | READ_CONTROL, false, security, path);
verifyPrivate(file.get(), path, false);
return file;
}
static void writeBytes(HANDLE file, const void* bytes, DWORD count, const std::wstring& path) {
DWORD written;
require(WriteFile(file, bytes, count, &written, nullptr) && written == count, "Write private file failed", path);
}
static void dispose(HANDLE handle, const std::wstring& path) {
FILE_BASIC_INFO info{};
require(GetFileInformationByHandleEx(handle, FileBasicInfo, &info, sizeof(info)), "Read cleanup attributes failed", path);
if ((info.FileAttributes & FILE_ATTRIBUTE_READONLY) != 0) {
info.FileAttributes &= ~FILE_ATTRIBUTE_READONLY;
require(SetFileInformationByHandle(handle, FileBasicInfo, &info, sizeof(info)), "Clear owned readonly attribute failed", path);
}
FILE_DISPOSITION_INFO deletion{TRUE};
require(SetFileInformationByHandle(handle, FileDispositionInfo, &deletion, sizeof(deletion)), "Delete owned object by handle failed", path);
}
void closeRuntime(Runtime& root) {
if (root.closed) return;
// Windows keeps the bearer descriptor and snapshot leases until this owner
// cleanup. POSIX removes its consumed descriptor earlier. Abrupt owner death
// closes process jobs but cannot run this walk, so private files can remain.
root.immutableFiles.clear();
struct ReleaseRuntime {
Runtime& root;
~ReleaseRuntime() { root.directory.reset(); root.parents.clear(); root.closed = true; }
} release{root};
// Each traversed directory stays locked against replacement. Reparse entries
// are unlinked as objects; their targets are never enumerated or deleted.
struct Frame {
Handle directory;
HANDLE search = INVALID_HANDLE_VALUE;
std::wstring path;
WIN32_FIND_DATAW next{};
bool pending = false;
Frame(Handle handle, std::wstring path) : directory(std::move(handle)), path(std::move(path)) {
search = FindFirstFileExW((L"\\\\?\\" + this->path + L"\\*").c_str(), FindExInfoBasic, &next,
FindExSearchNameMatch, nullptr, 0);
pending = search != INVALID_HANDLE_VALUE;
require(pending || GetLastError() == ERROR_FILE_NOT_FOUND, "Enumerate runtime cleanup failed", this->path);
}
~Frame() { if (search != INVALID_HANDLE_VALUE) FindClose(search); }
};
std::vector<std::unique_ptr<Frame>> frames;
HANDLE duplicate;
require(DuplicateHandle(GetCurrentProcess(), root.directory.get(), GetCurrentProcess(), &duplicate, 0, FALSE,
DUPLICATE_SAME_ACCESS), "Duplicate runtime cleanup handle failed", root.path);
frames.push_back(std::make_unique<Frame>(Handle(duplicate), root.path));
while (!frames.empty()) {
auto& frame = *frames.back();
if (!frame.pending) {
if (frame.search != INVALID_HANDLE_VALUE) { FindClose(frame.search); frame.search = INVALID_HANDLE_VALUE; }
dispose(frame.directory.get(), frame.path);
frames.pop_back();
continue;
}
const auto name = std::wstring(frame.next.cFileName);
frame.pending = FindNextFileW(frame.search, &frame.next) != FALSE;
require(frame.pending || GetLastError() == ERROR_NO_MORE_FILES, "Continue runtime cleanup enumeration failed", frame.path);
if (name == L"." || name == L"..") continue;
const auto path = frame.path + L"\\" + name;
Handle child(CreateFileW((L"\\\\?\\" + path).c_str(), DELETE | GENERIC_READ | FILE_WRITE_ATTRIBUTES,
FILE_SHARE_READ, nullptr, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr));
require(child.valid(), "Open owned cleanup object failed", path);
FILE_ATTRIBUTE_TAG_INFO info{};
require(GetFileInformationByHandleEx(child.get(), FileAttributeTagInfo, &info, sizeof(info)), "Read cleanup object tag failed", path);
if ((info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 && (info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0)
frames.push_back(std::make_unique<Frame>(std::move(child), path));
else dispose(child.get(), path);
}
}
struct SnapshotWork {
napi_env env;
napi_async_work work = nullptr;
napi_deferred deferred = nullptr;
napi_ref rootReference = nullptr;
Runtime* root;
std::unique_ptr<File> source;
Handle destination;
std::wstring path;
std::wstring digest;
std::unique_ptr<Failure> failure;
SnapshotWork(napi_env env, Runtime& root) : env(env), root(&root) {}
};
static void copySnapshot(napi_env, void* pointer) {
auto& work = *static_cast<SnapshotWork*>(pointer);
try {
BCRYPT_ALG_HANDLE algorithm = nullptr;
require(BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM, nullptr, 0) == 0,
"Initialize snapshot SHA-256 failed", work.source->path, ERROR_GEN_FAILURE);
struct Algorithm { BCRYPT_ALG_HANDLE handle; ~Algorithm() { BCryptCloseAlgorithmProvider(handle, 0); } } owner{algorithm};
BCRYPT_HASH_HANDLE hash = nullptr;
require(BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0) == 0,
"Create snapshot SHA-256 state failed", work.source->path, ERROR_GEN_FAILURE);
struct Hash { BCRYPT_HASH_HANDLE handle; ~Hash() { BCryptDestroyHash(handle); } } hashOwner{hash};
std::array<unsigned char, 65536> buffer;
while (true) {
require(!work.root->snapshotCancelled.load(), "Admitted snapshot was cancelled", work.path, ERROR_OPERATION_ABORTED);
DWORD count;
require(ReadFile(work.source->get(), buffer.data(), buffer.size(), &count, nullptr),
"Read admitted source handle failed", work.source->path);
if (count == 0) break;
writeBytes(work.destination.get(), buffer.data(), count, work.path);
require(BCryptHashData(hash, buffer.data(), count, 0) == 0,
"Hash admitted snapshot bytes failed", work.source->path, ERROR_GEN_FAILURE);
}
std::array<unsigned char, 32> digest;
require(BCryptFinishHash(hash, digest.data(), digest.size(), 0) == 0,
"Finish snapshot SHA-256 failed", work.source->path, ERROR_GEN_FAILURE);
require(FlushFileBuffers(work.destination.get()), "Flush private snapshot failed", work.path);
work.digest = hex(digest.data(), digest.size());
} catch (const Failure& failure) {
work.failure = std::make_unique<Failure>(failure);
} catch (const std::exception& failure) {
work.failure = std::make_unique<Failure>(failure.what(), work.path, ERROR_GEN_FAILURE);
}
}
static void completeSnapshot(napi_env env, napi_status status, void* pointer) {
std::unique_ptr<SnapshotWork> work(static_cast<SnapshotWork*>(pointer));
work->root->snapshotPending = false;
try {
if (work->failure) throw *work->failure;
require(status == napi_ok && !work->root->snapshotCancelled.load(), "Native snapshot worker was cancelled", work->path, ERROR_OPERATION_ABORTED);
auto result = object(env);
set(env, result, "sha256", string(env, work->digest));
set(env, result, "source", identity(env, work->source->get(), work->source->requestedPath));
set(env, result, "snapshot", identity(env, work->destination.get(), work->path));
work->root->immutableFiles.push_back(std::move(work->destination));
check(napi_resolve_deferred(env, work->deferred, result));
} catch (const Failure& failure) {
napi_reject_deferred(env, work->deferred, failureValue(env, failure));
} catch (const std::exception& failure) {
napi_reject_deferred(env, work->deferred,
failureValue(env, Failure(failure.what(), work->path, ERROR_GEN_FAILURE)));
}
napi_delete_reference(env, work->rootReference);
napi_delete_async_work(env, work->work);
}
static napi_value snapshot(napi_env env, napi_value rootValue, Runtime& root,
const std::wstring& sourcePath, const std::wstring& relative) {
auto work = std::make_unique<SnapshotWork>(env, root);
work->source = openFile(sourcePath);
work->path = relativePath(root, relative);
work->destination = createPrivateFile(root, relative);
napi_value promise;
check(napi_create_promise(env, &work->deferred, &promise));
check(napi_create_reference(env, rootValue, 1, &work->rootReference));
const auto label = string(env, std::string("REA admitted Windows snapshot"));
root.snapshotCancelled.store(false);
try {
check(napi_create_async_work(env, nullptr, label, copySnapshot, completeSnapshot, work.get(), &work->work));
check(napi_queue_async_work(env, work->work));
} catch (...) {
if (work->work) napi_delete_async_work(env, work->work);
napi_delete_reference(env, work->rootReference);
throw;
}
root.snapshotPending = true;
work.release();
return promise;
}
napi_value filesystemCall(napi_env env, const std::wstring& operation, const std::vector<napi_value>& args) {
const auto expected = operation == L"open" || operation == L"close" || operation == L"runtime_close" || operation == L"runtime_snapshot_cancel" ? 1
: operation == L"runtime_create" || operation == L"runtime_mkdir" || operation == L"runtime_open" ? 2 : 3;
require(args.size() == static_cast<size_t>(expected), "Wrong native filesystem argument count", L"", ERROR_INVALID_PARAMETER);
if (operation == L"open") {
auto file = openFile(wide(env, args[0]));
auto result = identity(env, file->get(), file->requestedPath);
set(env, result, "handle", wrap(env, std::move(file)));
return result;
}
if (operation == L"close") {
auto& file = static_cast<File&>(resource(env, args[0], Kind::File));
file.handles.clear(); file.closed = true;
return null(env);
}
if (operation == L"read") {
auto& file = static_cast<File&>(resource(env, args[0], Kind::File));
const auto position = numeric(env, args[1]);
const auto length = numeric(env, args[2]);
require(position >= 0 && position <= 9007199254740991.0 && position == static_cast<int64_t>(position) &&
length >= 0 && length <= 65536 && length == static_cast<DWORD>(length),
"Read coordinates exceed the native chunk boundary", file.path, ERROR_INVALID_PARAMETER);
LARGE_INTEGER offset; offset.QuadPart = static_cast<int64_t>(position);
require(SetFilePointerEx(file.get(), offset, nullptr, FILE_BEGIN), "Seek admitted file handle failed", file.path);
std::array<unsigned char, 65536> buffer;
DWORD count;
require(ReadFile(file.get(), buffer.data(), static_cast<DWORD>(length), &count, nullptr), "Read admitted file handle failed", file.path);
napi_value result;
check(napi_create_buffer_copy(env, count, buffer.data(), nullptr, &result));
return result;
}
if (operation == L"runtime_create") {
auto root = createRuntime(wide(env, args[0]), wide(env, args[1]));
auto result = identity(env, root->directory.get(), root->path);
set(env, result, "path", string(env, root->path));
set(env, result, "privateDacl", boolean(env, true));
set(env, result, "handle", wrap(env, std::move(root)));
return result;
}
auto& root = static_cast<Runtime&>(resource(env, args[0], Kind::Runtime));
if (operation == L"runtime_snapshot_cancel") { root.snapshotCancelled.store(true); return null(env); }
require(!root.snapshotPending, "Runtime snapshot is still pending", root.path, ERROR_BUSY);
// Reject overlapping operations before snapshot() resets cancellation. The
// one accepted worker and WindowsPrivateRuntime retain the same root lease.
if (operation == L"runtime_close") { closeRuntime(root); return null(env); }
if (operation == L"runtime_mkdir") { directory(root, wide(env, args[1])); return null(env); }
if (operation == L"runtime_open") {
const auto path = relativePath(root, wide(env, args[1]));
auto file = runtimeDirectory(root, path.substr(0, path.find_last_of(L'\\')));
file->path = path;
file->requestedPath = path;
file->handles.push_back(openComponent(path, GENERIC_READ, false));
verifyPrivate(file->get(), path, false);
auto result = identity(env, file->get(), path);
set(env, result, "handle", wrap(env, std::move(file)));
return result;
}
if (operation == L"runtime_write") {
bool isBuffer; check(napi_is_buffer(env, args[2], &isBuffer));
require(isBuffer, "Private write requires a byte buffer", root.path, ERROR_INVALID_PARAMETER);
void* bytes; size_t length; check(napi_get_buffer_info(env, args[2], &bytes, &length));
require(length <= MAXDWORD, "Private write exceeds Win32 byte representation", root.path, ERROR_INVALID_PARAMETER);
auto file = createPrivateFile(root, wide(env, args[1]));
writeBytes(file.get(), bytes, static_cast<DWORD>(length), root.path);
require(FlushFileBuffers(file.get()), "Flush private runtime file failed", root.path);
root.immutableFiles.push_back(std::move(file));
return null(env);
}
if (operation == L"runtime_snapshot") {
return snapshot(env, args[0], root, wide(env, args[1]), wide(env, args[2]));
}
throw Failure("Unknown native filesystem operation", L"", ERROR_INVALID_PARAMETER);
}
} // namespace rea
+271
View File
@@ -0,0 +1,271 @@
#include "authority.hpp"
namespace rea {
class Attributes {
std::vector<unsigned char> storage_;
public:
explicit Attributes(DWORD count) {
SIZE_T size = 0;
InitializeProcThreadAttributeList(nullptr, count, 0, &size);
storage_.resize(size);
require(InitializeProcThreadAttributeList(get(), count, 0, &size), "Initialize process attributes failed");
}
~Attributes() { DeleteProcThreadAttributeList(get()); }
LPPROC_THREAD_ATTRIBUTE_LIST get() { return reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(storage_.data()); }
void add(DWORD_PTR key, void* value, SIZE_T size) {
require(UpdateProcThreadAttribute(get(), 0, key, value, size, nullptr, nullptr),
"Windows atomic process attribute is unavailable");
}
};
static Handle createJob() {
Handle job(CreateJobObjectW(nullptr, nullptr));
require(job.valid(), "Create owned Job Object failed");
JOBOBJECT_EXTENDED_LIMIT_INFORMATION policy{};
policy.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
require(SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation, &policy, sizeof(policy)),
"Apply kill-on-owner-close Job Object policy failed");
JOBOBJECT_EXTENDED_LIMIT_INFORMATION actual{};
require(QueryInformationJobObject(job.get(), JobObjectExtendedLimitInformation, &actual, sizeof(actual), nullptr),
"Read Job Object policy failed");
require(actual.BasicLimitInformation.LimitFlags == JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
"Job Object policy permits unexpected breakaway", L"", ERROR_ACCESS_DENIED);
return job;
}
static void pipe(Handle& read, Handle& write) {
SECURITY_ATTRIBUTES attributes{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
HANDLE rawRead, rawWrite;
require(CreatePipe(&rawRead, &rawWrite, &attributes, 0), "Create owned process output pipe failed");
read.reset(rawRead); write.reset(rawWrite);
require(SetHandleInformation(read.get(), HANDLE_FLAG_INHERIT, 0), "Protect process output handle inheritance failed");
}
std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::wstring& commandLine,
const std::wstring& cwd, const std::vector<std::wstring>& environment) {
require(command.size() >= 3 && command[1] == L':' && command[2] == L'\\',
"Owned Windows process requires an absolute executable path", command, ERROR_INVALID_PARAMETER);
auto result = std::make_unique<Process>();
result->job = createJob();
Handle stdoutWrite, stderrWrite;
pipe(result->stdoutRead, stdoutWrite); pipe(result->stderrRead, stderrWrite);
SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
Handle input(CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
&security, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
require(input.valid(), "Open owned process null input failed");
Attributes attributes(2);
HANDLE job = result->job.get();
attributes.add(PROC_THREAD_ATTRIBUTE_JOB_LIST, &job, sizeof(job));
std::array<HANDLE, 3> inherited{input.get(), stdoutWrite.get(), stderrWrite.get()};
attributes.add(PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherited.data(), sizeof(inherited));
STARTUPINFOEXW startup{};
startup.StartupInfo.cb = sizeof(startup);
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
startup.StartupInfo.hStdInput = input.get();
startup.StartupInfo.hStdOutput = stdoutWrite.get();
startup.StartupInfo.hStdError = stderrWrite.get();
startup.lpAttributeList = attributes.get();
// Windows environment names use ordinal case-insensitive comparison, not
// locale collation or JavaScript's expanding Unicode uppercase conversion.
auto orderedEnvironment = environment;
auto keyLength = [&](const std::wstring& entry) {
const auto end = entry.find(L'=', entry.front() == L'=' ? 1 : 0);
require(end != std::wstring::npos && end > 0, "Invalid child environment name", command,
ERROR_INVALID_PARAMETER);
return static_cast<int>(end);
};
for (const auto& entry : orderedEnvironment) {
require(!entry.empty(), "Empty child environment entry", command, ERROR_INVALID_PARAMETER);
keyLength(entry);
}
auto compareNames = [&](const std::wstring& left, const std::wstring& right) {
const auto result = CompareStringOrdinal(left.data(), keyLength(left), right.data(), keyLength(right), TRUE);
require(result != 0, "Compare Windows environment names failed", command);
return result;
};
std::stable_sort(orderedEnvironment.begin(), orderedEnvironment.end(),
[&](const std::wstring& left, const std::wstring& right) {
return compareNames(left, right) == CSTR_LESS_THAN;
});
std::vector<std::wstring> uniqueEnvironment;
for (auto& entry : orderedEnvironment) {
if (!uniqueEnvironment.empty() && compareNames(uniqueEnvironment.back(), entry) == CSTR_EQUAL)
uniqueEnvironment.back() = std::move(entry);
else uniqueEnvironment.push_back(std::move(entry));
}
std::vector<wchar_t> variables;
for (const auto& entry : uniqueEnvironment) {
variables.insert(variables.end(), entry.begin(), entry.end()); variables.push_back(L'\0');
}
variables.push_back(L'\0');
if (uniqueEnvironment.empty()) variables.push_back(L'\0');
std::vector<wchar_t> line(commandLine.begin(), commandLine.end()); line.push_back(L'\0');
PROCESS_INFORMATION process{};
require(CreateProcessW(command.c_str(), line.data(), nullptr, nullptr, TRUE,
CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT | CREATE_SUSPENDED | CREATE_NO_WINDOW,
variables.data(), cwd.empty() ? nullptr : cwd.c_str(), &startup.StartupInfo, &process),
"Create process atomically inside owned Job Object failed", command);
result->process.reset(process.hProcess);
Handle thread(process.hThread);
result->pid = process.dwProcessId;
// An elevated token may default file ownership to Administrators. Normalize
// only the suspended child's default owner so its runtime files belong to the
// current user; the caller's token and privileges are not modified.
HANDLE rawToken;
require(OpenProcessToken(result->process.get(), TOKEN_QUERY | TOKEN_ADJUST_DEFAULT, &rawToken),
"Open suspended process default-owner token failed", command);
Handle token(rawToken);
DWORD tokenSize = 0;
GetTokenInformation(token.get(), TokenUser, nullptr, 0, &tokenSize);
std::vector<unsigned char> tokenUser(tokenSize);
require(GetTokenInformation(token.get(), TokenUser, tokenUser.data(), tokenSize, &tokenSize),
"Read suspended process user identity failed", command);
TOKEN_OWNER owner{reinterpret_cast<TOKEN_USER*>(tokenUser.data())->User.Sid};
require(SetTokenInformation(token.get(), TokenOwner, &owner, sizeof(owner)),
"Set suspended process file owner failed", command);
DWORD_PTR affinity = 0, systemAffinity = 0;
require(GetProcessAffinityMask(GetCurrentProcess(), &affinity, &systemAffinity),
"Read caller process affinity failed", command);
if (affinity != 0)
require(SetProcessAffinityMask(result->process.get(), affinity), "Preserve caller process affinity failed", command);
const DWORD priority = GetPriorityClass(GetCurrentProcess());
require(priority != 0 && SetPriorityClass(result->process.get(), priority),
"Preserve caller process priority failed", command);
BOOL assigned = FALSE;
if (!IsProcessInJob(result->process.get(), result->job.get(), &assigned) || !assigned) {
TerminateJobObject(result->job.get(), 1);
throw Failure("Suspended process has no owned Job Object membership", command, ERROR_ACCESS_DENIED);
}
require(ResumeThread(thread.get()) != MAXDWORD, "Resume job-owned process failed", command);
return result;
}
static napi_value readOutput(napi_env env, Handle& read, bool& ended) {
std::array<unsigned char, 65536> bytes;
DWORD available = 0, count = 0;
if (!read.valid()) ended = true;
else if (!PeekNamedPipe(read.get(), nullptr, 0, nullptr, &available, nullptr)) {
require(GetLastError() == ERROR_BROKEN_PIPE, "Observe owned process output failed");
read.reset(); ended = true;
} else if (available > 0) {
require(ReadFile(read.get(), bytes.data(), std::min<DWORD>(available, bytes.size()), &count, nullptr),
"Read owned process output failed");
}
napi_value result;
check(napi_create_buffer_copy(env, count, bytes.data(), nullptr, &result));
return result;
}
static DWORD activeProcesses(Process& process) {
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION state{};
require(QueryInformationJobObject(process.job.get(), JobObjectBasicAccountingInformation, &state, sizeof(state), nullptr),
"Observe owned Job Object settlement failed");
return state.ActiveProcesses;
}
static napi_value callerToken(napi_env env) {
HANDLE raw;
require(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, &raw), "Read caller token failed");
Handle token(raw);
DWORD size;
TOKEN_ELEVATION elevation{};
require(GetTokenInformation(token.get(), TokenElevation, &elevation, sizeof(elevation), &size), "Read caller elevation failed");
std::vector<unsigned char> user(4096), owner(4096), integrity(4096);
require(GetTokenInformation(token.get(), TokenUser, user.data(), user.size(), &size) &&
GetTokenInformation(token.get(), TokenOwner, owner.data(), owner.size(), &size) &&
GetTokenInformation(token.get(), TokenIntegrityLevel, integrity.data(), integrity.size(), &size),
"Read caller token observations failed");
HANDLE rawImpersonation;
require(DuplicateToken(token.get(), SecurityIdentification, &rawImpersonation), "Observe caller group membership failed");
Handle impersonation(rawImpersonation);
std::array<unsigned char, SECURITY_MAX_SID_SIZE> administrators;
DWORD administratorSize = administrators.size();
require(CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, administrators.data(), &administratorSize),
"Create administrator group identity failed");
BOOL administrator = FALSE, assigned = FALSE;
require(CheckTokenMembership(impersonation.get(), administrators.data(), &administrator), "Read caller administrator membership failed");
require(IsProcessInJob(GetCurrentProcess(), nullptr, &assigned), "Read caller job membership failed");
const auto sid = reinterpret_cast<TOKEN_MANDATORY_LABEL*>(integrity.data())->Label.Sid;
auto result = object(env);
set(env, result, "elevated", boolean(env, elevation.TokenIsElevated != 0));
set(env, result, "administrator", boolean(env, administrator != FALSE));
set(env, result, "integrityLevel", number(env, *GetSidSubAuthority(sid, *GetSidSubAuthorityCount(sid) - 1)));
set(env, result, "defaultOwnerIsUser", boolean(env, EqualSid(reinterpret_cast<TOKEN_USER*>(user.data())->User.Sid,
reinterpret_cast<TOKEN_OWNER*>(owner.data())->Owner)));
set(env, result, "inJob", boolean(env, assigned != FALSE));
return result;
}
napi_value processCall(napi_env env, const std::wstring& operation, const std::vector<napi_value>& args) {
if (operation == L"process_caller_token") {
require(args.empty(), "Caller token observation takes no arguments", L"", ERROR_INVALID_PARAMETER);
return callerToken(env);
}
if (operation == L"process_spawn") {
require(args.size() == 4, "Wrong native process launch argument count", L"", ERROR_INVALID_PARAMETER);
std::vector<std::wstring> environment;
for (const auto& entry : array(env, args[3])) environment.push_back(wide(env, entry));
auto process = spawnProcess(wide(env, args[0]), wide(env, args[1]), wide(env, args[2]), environment);
auto result = object(env);
set(env, result, "pid", number(env, process->pid));
set(env, result, "jobAssigned", boolean(env, true));
set(env, result, "killOnOwnerClose", boolean(env, true));
set(env, result, "handle", wrap(env, std::move(process)));
return result;
}
require(args.size() == 1, "Wrong native process handle argument count", L"", ERROR_INVALID_PARAMETER);
auto& process = static_cast<Process&>(resource(env, args[0], Kind::Process));
if (operation == L"process_poll") {
auto result = object(env);
bool stdoutEnded = false, stderrEnded = false;
set(env, result, "stdout", readOutput(env, process.stdoutRead, stdoutEnded));
set(env, result, "stderr", readOutput(env, process.stderrRead, stderrEnded));
set(env, result, "stdoutEnded", boolean(env, stdoutEnded));
set(env, result, "stderrEnded", boolean(env, stderrEnded));
const auto exited = WaitForSingleObject(process.process.get(), 0);
require(exited == WAIT_OBJECT_0 || exited == WAIT_TIMEOUT, "Observe owned process exit failed");
DWORD code = 0;
if (exited == WAIT_OBJECT_0) require(GetExitCodeProcess(process.process.get(), &code), "Read owned process exit code failed");
set(env, result, "exitCode", exited == WAIT_OBJECT_0 ? number(env, code) : null(env));
set(env, result, "activeProcesses", number(env, activeProcesses(process)));
return result;
}
if (operation == L"process_alive") return number(env, activeProcesses(process));
if (operation == L"process_terminate") {
const auto active = activeProcesses(process);
if (active > 0) require(TerminateJobObject(process.job.get(), 1), "Terminate owned Job Object failed");
return boolean(env, active > 0);
}
if (operation == L"process_close") {
// Closing the job is the crash-safe authority; no PID enumeration is used.
process.job.reset(); process.process.reset(); process.stdoutRead.reset(); process.stderrRead.reset();
process.closed = true;
return null(env);
}
throw Failure("Unknown native process operation", L"", ERROR_INVALID_PARAMETER);
}
napi_value inspect(napi_env env) {
wchar_t temp[32768];
const auto length = GetTempPathW(32768, temp);
require(length > 0 && length < 32768, "Read temporary directory coordinate failed");
auto root = createRuntime(std::wstring(temp, length), L"rea-native-probe-");
auto filesystem = identity(env, root->directory.get(), root->path);
closeRuntime(*root);
auto job = createJob();
Attributes attributes(1);
HANDLE raw = job.get();
attributes.add(PROC_THREAD_ATTRIBUTE_JOB_LIST, &raw, sizeof(raw));
auto result = object(env);
set(env, result, "abiVersion", number(env, 1));
set(env, result, "nodeApiVersion", number(env, 8));
set(env, result, "architecture", string(env, std::string("x64")));
set(env, result, "filesystem", filesystem);
set(env, result, "privateDacl", boolean(env, true));
set(env, result, "atomicJobAssignment", boolean(env, true));
set(env, result, "killOnOwnerClose", boolean(env, true));
return result;
}
} // namespace rea
+1660 -1218
View File
File diff suppressed because it is too large Load Diff
+59 -44
View File
@@ -1,7 +1,7 @@
{
"name": "rea-agents",
"mcpName": "io.github.morluto/rea",
"version": "3.2.1",
"version": "4.0.1",
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
"license": "MIT",
"repository": {
@@ -35,14 +35,16 @@
"rea-agents": "scripts/rea.mjs"
},
"engines": {
"node": "^22.19.0 || >=24.11.0"
"node": "^22.19.0 || ^24.11.0 || >=26.0.0"
},
"packageManager": "npm@11.16.0",
"files": [
"dist",
"install.sh",
"bridge/hopper_bridge.py",
"bridge/native/ReaNativeUI.swift",
"bridge/ghidra/ReaGhidraBridge.java",
"bridge/ghidra/ReaGhidraPrepareCom.java",
"scripts/rea.mjs",
"scripts/electron-active-hook.cjs",
"scripts/electron-active-hook-boundaries.cjs",
@@ -51,7 +53,10 @@
"scripts/prepare.mjs",
"skills",
"README.md",
"LICENSE"
"LICENSE",
"native/windows/build/manifest.json",
"native/windows/build/rea-windows-x64.node",
"scripts/verify-windows-native-artifact.mjs"
],
"scripts": {
"deps:check": "node scripts/check-dependency-install.mjs",
@@ -60,44 +65,42 @@
"mcp-registry:check": "node scripts/verify-mcp-registry-metadata.mjs",
"mcp-catalog:generate": "npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs",
"mcp-catalog:check": "node scripts/generate-mcp-tool-catalog.mjs --check",
"prebuild": "npm run deps:check && npm run metadata:check && node scripts/clean-build-output.mjs",
"build": "tsc -p tsconfig.build.json",
"prebuild": "npm run deps:check && npm run metadata:check",
"build": "node scripts/run-exclusive.mjs build npm run build:unlocked",
"build:unlocked": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json",
"precheck": "npm run deps:check",
"check": "turbo run typecheck lint format:check knip metadata:check",
"precheck:fast": "npm run deps:check",
"check:fast": "turbo run typecheck lint",
"check:fast": "turbo run typecheck lint --output-logs=errors-only",
"precheck:changed": "npm run deps:check",
"check:changed": "turbo run typecheck lint && npm run test:changed",
"check:changed": "npm run check:fast && npm run test:changed",
"precheck:test": "npm run deps:check",
"check:test": "turbo run typecheck lint format:check knip metadata:check test:run",
"precheck:pr": "npm run deps:check",
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check",
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check && npm run jscpd && npm run scan:todos",
"precheck:ci": "npm run deps:check",
"check:ci": "turbo run typecheck lint format:check knip metadata:check jscpd scan:todos",
"build:cached": "turbo run build",
"format": "prettier --write .",
"format:check": "prettier --check .",
"format": "oxfmt .",
"format:check": "oxfmt --check .",
"lint": "oxlint",
"lint:fix": "oxlint --fix",
"knip": "knip",
"lint:dead": "knip",
"jscpd": "jscpd src/ tests/",
"scan:todos": "node scripts/scan-todos.mjs",
"start": "node scripts/rea.mjs mcp",
"test": "node scripts/run-exclusive.mjs test npm run test:with-build",
"test": "npm run test:with-build",
"test:with-build": "npm run build:cached && vitest run",
"test:run": "node scripts/run-exclusive.mjs test vitest run",
"test:local": "node scripts/run-exclusive.mjs test vitest run --changed --passWithNoTests --maxWorkers=1",
"pretest:fast": "npm run build:cached",
"test:fast": "vitest run --project domain --project services --project adapters --project composition --project boundary --project process-boundary --project mcp-boundary --project conformance",
"test:run": "vitest run",
"test:local": "node scripts/run-development-tests.mjs local",
"test:fast": "vitest run --project domain --project services --project adapters --project composition --project conformance --project evaluation",
"pretest:boundary": "npm run build:cached",
"test:boundary": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global",
"pretest:mcp": "npm run build:cached",
"test:mcp": "vitest run --project mcp-boundary",
"pretest:acceptance": "npm run build:cached",
"test:acceptance": "vitest run --project acceptance",
"pretest:changed": "npm run build:cached",
"test:changed": "vitest run --project domain --project services --project adapters --project composition --project boundary --project process-boundary --project mcp-boundary --project conformance --project evaluation --changed",
"test:changed": "node scripts/run-development-tests.mjs changed",
"pretest:integration": "npm run build:cached",
"test:integration": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global --project acceptance",
"pretest:coverage": "npm run build:cached",
@@ -109,14 +112,19 @@
"test:ci:merge": "vitest --merge-reports --coverage.enabled --coverage.provider=v8",
"pretest:verbose": "npm run build:cached",
"test:verbose": "vitest run --reporter=verbose",
"pretest:watch": "npm run build:cached",
"test:watch": "vitest --project domain --project services --project adapters --project composition --project boundary --project process-boundary --project mcp-boundary --project conformance --project evaluation --changed",
"test:watch": "vitest --project domain --project services --project adapters --project composition --project conformance --project evaluation --changed",
"pretest:watch:all": "npm run build:cached",
"test:watch:all": "vitest --changed",
"typecheck": "tsc --noEmit",
"verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs",
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
"verify:apple-dispatch": "npm run build:cached && node scripts/verify-apple-dispatch.mjs",
"verify:native-ui": "npm run build:cached && node scripts/verify-native-ui.mjs",
"verify:keyed-archive": "npm run build:cached && node scripts/verify-keyed-archive.mjs",
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
"verify:ghidra:switch": "npm run build:cached && node scripts/verify-real-ghidra-switch.mjs",
"verify:ghidra:dos": "npm run build:cached && node scripts/verify-real-ghidra-dos.mjs",
"verify:ghidra:com": "npm run build:cached && node scripts/verify-real-ghidra-com.mjs",
"verify:ghidra:aarch64-jump-table": "npm run build:cached && node scripts/verify-real-ghidra.mjs --aarch64-jump-table",
"verify:interface-builder": "npm run build:cached && node scripts/verify-interface-builder.mjs",
"verify:ghidra:cross-format": "npm run build:cached && node scripts/verify-real-ghidra.mjs --cross-format",
@@ -125,9 +133,9 @@
"verify:electron": "npm run build:cached && node scripts/verify-real-electron.mjs",
"verify:inspector": "npm run build:cached && node scripts/verify-real-javascript-runtime-observation.mjs",
"verify:javascript-runtime": "npm run build:cached && node scripts/verify-local-javascript-runtime.mjs",
"verify:javascript:digests": "npm run build:cached && node scripts/verify-large-javascript-digests.mjs",
"verify:application-workflows": "npm run build:cached && node scripts/verify-local-application-workflows.mjs",
"verify:managed": "npm run build:cached && node scripts/verify-managed-conformance.mjs",
"verify:replay": "npm run build:cached && node scripts/verify-real-javascript-replay.mjs",
"verify:agent": "npm run build:cached && node scripts/verify-agent-experience.mjs",
"verify:hopper:linux": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-linux.mjs",
"verify:setup:linux": "npm run build:cached && node scripts/verify-linux-setup-e2e.mjs",
@@ -142,22 +150,28 @@
"config:print": "node scripts/print-mcp-config.mjs",
"evidence:generate": "npm run build:cached && node scripts/generate-completion-ledger.mjs",
"evidence:check": "npm run build:cached && node scripts/generate-completion-ledger.mjs --check",
"docs:api": "typedoc",
"docs:api:cached": "turbo run docs:api",
"docs:generate": "node scripts/run-exclusive.mjs docs npm run docs:generate:unlocked",
"docs:generate:unlocked": "npm run metadata:generate && npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs && npm run docs:api:cached && node scripts/generate-error-schema.mjs",
"docs:generate:unlocked": "npm run metadata:generate && npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs && node scripts/generate-error-schema.mjs",
"docs:check": "node scripts/run-exclusive.mjs docs turbo run docs:check:built",
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-mcp-tool-catalog.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check",
"docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs",
"prepack": "node scripts/prepack.mjs",
"prepare": "node scripts/prepare.mjs"
"prepare": "node scripts/prepare.mjs",
"verify:asset-catalog": "npm run build:cached && node scripts/verify-asset-catalog.mjs",
"verify:native-ui:permissions": "npm run build:cached && node scripts/verify-native-ui.mjs --allow-permission-denial",
"test:focused": "node scripts/run-development-tests.mjs focused",
"build:windows-native": "node scripts/build-windows-native.mjs",
"verify:windows-native:artifact": "node scripts/verify-windows-native-artifact.mjs",
"prepublishOnly": "npm run verify:windows-native:artifact",
"verify:windows-native": "node scripts/verify-windows-native.mjs",
"verify:ghidra:windows:package": "node scripts/verify-packaged-ghidra-windows.mjs"
},
"lint-staged": {
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
"prettier --write",
"oxfmt",
"oxlint --fix"
],
"*.{json,md,yml,yaml}": "prettier --write"
"*.{json,md,yml,yaml}": "oxfmt"
},
"dependencies": {
"@babel/parser": "8.0.4",
@@ -166,44 +180,45 @@
"@electron/asar": "4.2.0",
"@jridgewell/trace-mapping": "0.3.31",
"@lydell/node-pty": "1.1.0",
"@modelcontextprotocol/client": "2.0.0",
"@modelcontextprotocol/server": "2.0.0",
"@modelcontextprotocol/client": "2.3.1",
"@modelcontextprotocol/server": "2.3.1",
"@xterm/addon-serialize": "0.14.0",
"@xterm/headless": "6.0.0",
"@zip.js/zip.js": "2.8.26",
"canonicalize": "3.0.0",
"cross-spawn": "7.0.6",
"diff": "9.0.0",
"graphology": "0.26.0",
"graphology-shortest-path": "2.1.0",
"ignore": "7.0.6",
"incur": "0.4.13",
"incur": "0.6.1",
"isomorphic-git": "1.38.7",
"pino": "9.14.0",
"jsonc-parser": "3.3.1",
"pino": "10.4.0",
"playwright-core": "1.58.2",
"plist": "5.0.0",
"semver": "7.8.5",
"smol-toml": "^1.7.0",
"smol-toml": "1.7.0",
"write-file-atomic": "7.0.0",
"ws": "8.21.0",
"zod": "4.4.3"
},
"devDependencies": {
"@fast-check/vitest": "0.4.1",
"@types/node": "24.10.1",
"@fast-check/vitest": "0.5.0",
"@types/cross-spawn": "6.0.6",
"@types/node": "26.6.4",
"@types/semver": "7.7.1",
"@types/write-file-atomic": "4.0.3",
"@types/ws": "8.18.1",
"@vitest/coverage-v8": "4.1.10",
"@vitest/coverage-v8": "5.0.3",
"ajv": "8.20.0",
"husky": "9.1.7",
"jscpd": "5.0.12",
"knip": "6.26.0",
"knip": "6.39.0",
"lint-staged": "16.4.0",
"oxlint": "1.73.0",
"prettier": "3.6.2",
"turbo": "2.10.5",
"typedoc": "0.28.20",
"typescript": "5.9.3",
"vitest": "4.1.10",
"oxfmt": "0.72.0",
"oxlint": "1.87.0",
"turbo": "2.11.7",
"typescript": "7.0.2",
"vitest": "5.0.3",
"yaml": "2.9.0"
}
}
+1
View File
@@ -21,6 +21,7 @@
{ "type": "generic", "path": "README_ja.md" },
{ "type": "generic", "path": "README_ko.md" },
{ "type": "generic", "path": "README_ar.md" },
{ "type": "generic", "path": "docs/installation.md" },
{
"type": "generic",
"path": "src/generatedPackageMetadata.ts"
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const nodeVersion = "22.19.0";
const compiler = "x86_64-w64-mingw32-g++";
const cache = join(homedir(), ".cache", "rea-windows-native");
const output = join(root, "native", "windows", "build");
const run = (command, args) => {
const result = spawnSync(command, args, {
cwd: root,
encoding: "utf8",
maxBuffer: 4 * 1_024 * 1_024,
timeout: 120_000,
});
if (result.error !== undefined)
throw new Error(
`Windows native build lane requires ${command}: ${result.error.message}`,
);
if (result.status !== 0)
throw new Error(
`Windows native build failed: ${result.stdout}${result.stderr}`,
);
return result.stdout;
};
if (process.platform !== "linux")
throw new Error(
"The Windows x64 artifact build lane currently requires Linux and MinGW-w64.",
);
run(compiler, ["--version"]);
await mkdir(cache, { recursive: true });
await mkdir(output, { recursive: true });
const exists = async (path) =>
access(path).then(
() => true,
() => false,
);
const fetchBytes = async (name) => {
const response = await fetch(
`https://nodejs.org/dist/v${nodeVersion}/${name}`,
{
signal: AbortSignal.timeout(60_000),
},
);
if (!response.ok)
throw new Error(
`Node build prerequisite download failed: ${name}: HTTP ${response.status}`,
);
return Buffer.from(await response.arrayBuffer());
};
const checksums = (await fetchBytes("SHASUMS256.txt")).toString("utf8");
const verifiedDownload = async (name, path) => {
if (!(await exists(path))) await writeFile(path, await fetchBytes(name));
const digest = createHash("sha256")
.update(await readFile(path))
.digest("hex");
const expected = checksums
.split("\n")
.find((line) => line.slice(66) === name)
?.slice(0, 64);
if (expected === undefined || digest !== expected)
throw new Error(`Node build prerequisite SHA-256 mismatch: ${name}`);
};
const cachedHeaders = join(
homedir(),
".cache",
"node-gyp",
nodeVersion,
"include",
"node",
);
let headers = cachedHeaders;
if (!(await exists(join(headers, "node_api.h")))) {
const archive = join(cache, `node-v${nodeVersion}-headers.tar.gz`);
await verifiedDownload(`node-v${nodeVersion}-headers.tar.gz`, archive);
run("tar", ["xzf", archive, "-C", cache]);
headers = join(cache, `node-v${nodeVersion}`, "include", "node");
}
const artifact = "rea-windows-x64.node";
// MSVC short-import libraries do not produce a correct Node import table with
// GNU ld. Generate a GNU import library for the exact stable Node-API symbols.
const sources = ["addon.cc", "filesystem.cc", "process.cc"];
const sourceText = (
await Promise.all(
sources.map((path) =>
readFile(join(root, "native", "windows", "src", path), "utf8"),
),
)
).join("\n");
const symbols = [
...new Set(
[...sourceText.matchAll(/\b(napi_[a-z0-9_]+)\s*\(/gu)].map(
(match) => match[1],
),
),
].sort();
const definition = join(cache, "node-api.def");
const library = join(cache, "libnode-api.a");
await writeFile(
definition,
`LIBRARY node.exe\nEXPORTS\n${symbols.join("\n")}\n`,
);
run("x86_64-w64-mingw32-dlltool", ["-d", definition, "-l", library]);
run(compiler, [
"-std=c++17",
"-O2",
"-DNAPI_VERSION=8",
"-D_WIN32_WINNT=0x0A00",
"-shared",
"-static",
"-static-libgcc",
"-static-libstdc++",
"-Wl,--no-insert-timestamp",
`-I${headers}`,
"native/windows/src/addon.cc",
"native/windows/src/filesystem.cc",
"native/windows/src/process.cc",
library,
"-ladvapi32",
"-lbcrypt",
"-lntdll",
"-o",
join(output, artifact),
]);
const packageJson = JSON.parse(
await readFile(join(root, "package.json"), "utf8"),
);
const bytes = await readFile(join(output, artifact));
const metadata = {
packageVersion: packageJson.version,
platform: "win32",
architecture: "x64",
abiVersion: 1,
nodeApiVersion: 8,
artifact,
artifactSha256: createHash("sha256").update(bytes).digest("hex"),
};
await writeFile(
join(output, "manifest.json"),
`${JSON.stringify(metadata, null, 2)}\n`,
);
process.stdout.write(
`${JSON.stringify({ ...metadata, artifactBytes: bytes.length })}\n`,
);
+18 -1
View File
@@ -7,6 +7,21 @@ export async function startBrowserVerifierSite() {
let port = 0;
let sessionGeneration = 0;
const server = createServer((request, response) => {
if (request.url === "/screenshot-noise") {
response.setHeader("content-type", "text/html");
response.end(`<!doctype html><html><body style="margin:0"><canvas width="2048" height="1536"></canvas><script>
const context = document.querySelector('canvas').getContext('2d');
const image = context.createImageData(2048, 1536);
let state = 0x6d2b79f5;
for (let index = 0; index < image.data.length; index++) {
state ^= state << 13; state ^= state >>> 17; state ^= state << 5;
image.data[index] = index % 4 === 3 ? 255 : state & 255;
}
context.putImageData(image, 0, 0);
document.body.dataset.ready = 'true';
</script></body></html>`);
return;
}
if (request.url === "/app.js.map") {
response.setHeader("content-type", "application/source-map+json");
response.end(sourceMap());
@@ -102,7 +117,9 @@ const browserScript = (port) => `
const { generation } = await response.json();
if (generation <= observedGeneration) return;
observedGeneration = generation;
history.pushState({}, "", "/app/session-" + String(generation));
const sessionUrl = new URL(window.location.href);
sessionUrl.pathname = "/app/session-" + String(generation);
history.pushState({}, "", sessionUrl);
}, 100);
//# sourceMappingURL=/app.js.map
`;
+5 -4
View File
@@ -2,7 +2,7 @@ import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { analysisErrorJsonSchema } from "../dist/contracts/errorSchemas.js";
import { format } from "prettier";
import { formatGeneratedFile } from "./lib/format-generated-file.mjs";
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
const arguments_ = new Set(process.argv.slice(2));
@@ -13,9 +13,10 @@ for (const argument of arguments_)
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
await ensureGeneratedFile({
path: join(root, "docs/error-contract.schema.json"),
source: await format(JSON.stringify(analysisErrorJsonSchema), {
parser: "json",
}),
source: await formatGeneratedFile(
"docs/error-contract.schema.json",
JSON.stringify(analysisErrorJsonSchema),
),
check: arguments_.has("--check"),
generateCommand: "npm run docs:generate",
});
+17 -4
View File
@@ -1,7 +1,8 @@
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { format } from "prettier";
import { formatGeneratedFile } from "./lib/format-generated-file.mjs";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { Ajv2020 } from "ajv/dist/2020.js";
import { McpServer } from "@modelcontextprotocol/server";
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
@@ -28,6 +29,16 @@ const sessionToolNames = new Set([
...MANAGED_WORKFLOW_TOOL_CONTRACTS.map(({ name }) => name),
]);
const advertisedTools = await sdkToolCatalog();
const ajv = new Ajv2020({ strict: false, validateFormats: false });
for (const tool of advertisedTools.values()) {
for (const kind of ["inputSchema", "outputSchema"]) {
const schema = tool[kind];
if (schema !== undefined && !ajv.validateSchema(schema))
throw new Error(
`Invalid JSON Schema for ${tool.name}.${kind}: ${ajv.errorsText(ajv.errors)}`,
);
}
}
const catalog = TOOL_CONTRACTS.map((contract) => {
const advertised = advertisedTools.get(contract.name);
if (advertised === undefined)
@@ -78,7 +89,7 @@ async function sdkToolCatalog() {
}
}
const auxiliaryProviders = [
new ArtifactProvider(false, false),
new ArtifactProvider(CATALOG_PLATFORM),
new NativeMacOSProvider(undefined, CATALOG_PLATFORM),
new ManagedStaticProvider(),
].map((provider) => ({
@@ -98,8 +109,11 @@ const canonicalizeJson = (value) => {
const payloadJson = JSON.stringify(
canonicalizeJson({ catalog, auxiliaryProviders }),
null,
2,
);
const source = await format(
const source = await formatGeneratedFile(
"src/generatedMcpToolCatalog.ts",
`import type { ToolAnnotations } from "@modelcontextprotocol/server";
import type {
CapabilityDescriptor,
@@ -183,7 +197,6 @@ function isRecord(
return typeof value === "object" && value !== null && !Array.isArray(value);
}
`,
{ parser: "typescript" },
);
await ensureGeneratedFile({
+64
View File
@@ -0,0 +1,64 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
const environment = () => ({
PATH: process.env.PATH ?? "/usr/bin:/bin",
REA_LOG_LEVEL: "silent",
REA_ANALYSIS_PROVIDER: "auto",
// Artifact workflows have no deep-provider prerequisite or GUI authority.
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
});
/** Execute the compiled CLI with production providers and no test adapters. */
export async function artifactCli(command, target, arguments_ = []) {
const { stdout } = await promisify(execFile)(
process.execPath,
[
fileURLToPath(new URL("../rea.mjs", import.meta.url)),
command,
target,
...arguments_,
"--json",
],
{ env: environment(), timeout: 60000, maxBuffer: 16 * 1024 * 1024 },
);
const evidence = JSON.parse(stdout);
assert.equal(evidence.error, undefined, stdout);
assert.ok(evidence.normalized_result, "CLI omitted normalized result");
return evidence.normalized_result;
}
/** Run a real stdio MCP subprocess, owning and closing its transport. */
export async function withArtifactMcp(target, verify) {
const transport = new StdioClientTransport({
command: process.execPath,
args: [fileURLToPath(new URL("../rea.mjs", import.meta.url)), "mcp"],
env: environment(),
stderr: "pipe",
});
const client = new Client({ name: "artifact-real-e2e", version: "1" });
try {
await client.connect(transport);
const opened = await client.callTool({
name: "open_binary",
arguments: { path: target },
});
assert.notEqual(opened.isError, true, JSON.stringify(opened));
await verify(client);
} finally {
await client.close();
await transport.close();
}
}
/** Require a successful structured MCP tool result rather than its text rendering. */
export async function artifactMcpResult(client, name, arguments_ = {}) {
const result = await client.callTool({ name, arguments: arguments_ });
assert.notEqual(result.isError, true, JSON.stringify(result));
assert.ok(result.structuredContent?.result, "MCP omitted structured result");
return result.structuredContent.result;
}
+252
View File
@@ -0,0 +1,252 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { once } from "node:events";
import { createServer } from "node:http";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { chromium } from "playwright-core";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { PlaywrightBrowserScenarioProvider } from "../../dist/browser/PlaywrightBrowserScenarioProvider.js";
import { PlaywrightScenarioEvents } from "../../dist/browser/PlaywrightScenarioEvents.js";
import { BrowserScenarioSecrets } from "../../dist/browser/BrowserScenarioSecrets.js";
import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
import { compareBrowserScenarios } from "../../dist/domain/browserScenarioDiff.js";
import { browserScenarioCaptureSchema } from "../../dist/domain/browserScenarioCapture.js";
import { parseEvidence } from "../../dist/domain/evidence.js";
const document = (path) => {
if (path.startsWith("/nested"))
return `<script>throw new Error("nested popup failure")</script>
<script>opener.postMessage("nested-ready", "*")</script>`;
if (path.startsWith("/popup"))
return `<script>throw new Error("popup failure")</script>
<script>onmessage = e => {
if (e.data === "nested-ready") opener.postMessage("popup-ready", "*");
}; window.open("/nested");</script><iframe src="/child"></iframe>`;
if (path.startsWith("/child")) return "child frame";
return `<script>onmessage = e => {
if (e.data === "popup-ready") document.body.dataset.ready = "true";
}</script><button onclick="window.open('/redirect')">Open popup</button>`;
};
const scenarioFor = (executable, origin, events) =>
browserScenarioSchema.parse({
browser: { mode: "launch", executable_path: executable },
start_url: { url: origin },
actions: [
{
step_id: "open",
action: "click",
locator: { kind: "role", role: "button", name: "Open popup" },
},
{
step_id: "ready",
action: "wait_for",
locator: { kind: "css", selector: "body[data-ready=true]" },
state: "attached",
timeout_ms: 10_000,
},
],
capture: { events, after_each_step: [], at_end: [] },
});
const capture = async (scenario) => {
const result = await new PlaywrightBrowserScenarioProvider().captureScenario(
scenario,
);
if (!result.ok) throw result.error;
assert.ok(
result.value.steps.every((step) => step.status === "completed"),
JSON.stringify(result.value.steps),
);
return result.value;
};
const assertNetwork = (result, origin) => {
for (const path of ["/redirect", "/popup", "/nested"])
for (const kind of ["request", "response"])
assert.equal(
result.events.items.filter(
(event) => event.kind === kind && event.url?.url === origin + path,
).length,
1,
`${kind} ${path} must be captured exactly once`,
);
assert.equal(result.completeness.equality_eligible, true);
assert.equal(compareWithSelf(result).overall_status, "unchanged");
};
const compareWithSelf = (capture) =>
compareBrowserScenarios({
before_scenario: capture,
after_scenario: capture,
normalization: { rules: [] },
});
const verifySharedContext = async (executable, origin) => {
const browser = await chromium.launch({
executablePath: executable,
headless: true,
});
try {
const context = await browser.newContext();
const page = await context.newPage();
const foreign = await context.newPage();
const secrets = BrowserScenarioSecrets.resolve(
scenarioFor(executable, origin, ["network"]),
{},
);
assert.ok(secrets);
const events = new PlaywrightScenarioEvents({
page,
context,
ownsContext: false,
enabled: new Set(["network"]),
secrets,
});
await Promise.all([
page.goto(origin),
foreign.goto(origin + "/popup?unrelated-tab"),
]);
await page.getByRole("button", { name: "Open popup", exact: true }).click();
await page.locator("body[data-ready=true]").waitFor({ timeout: 10_000 });
const retained = events.result().items;
assert.ok(
events
.limitations()
.some((value) => value.includes("Shared-context network")),
);
assert.ok(retained.some((event) => event.url?.url === origin + "/"));
assert.ok(!JSON.stringify(retained).includes("unrelated-tab"));
} finally {
await browser.close();
}
};
const verifyTransports = async (executable, origin) => {
const scenario = scenarioFor(executable, origin, ["network"]);
const entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
const env = {
PATH: process.env.PATH ?? "/usr/bin:/bin",
REA_LOG_LEVEL: "silent",
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
};
const { stdout } = await promisify(execFile)(
process.execPath,
[
entrypoint,
"capture-browser-scenario",
JSON.stringify(scenario),
"--json",
],
{ env, timeout: 60_000, maxBuffer: 16 * 1024 * 1024 },
);
assertNetwork(
browserScenarioCaptureSchema.parse(
parseEvidence(JSON.parse(stdout)).normalized_result,
),
origin,
);
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({ name: "popup-real-e2e", version: "1" });
try {
await client.connect(transport);
const captured = await client.callTool(
{ name: "capture_browser_scenario", arguments: scenario },
{ timeout: 60_000 },
);
assert.notEqual(captured.isError, true, JSON.stringify(captured));
const evidence = parseEvidence({
...captured.structuredContent?.evidence,
normalized_result: captured.structuredContent?.result,
});
assertNetwork(
browserScenarioCaptureSchema.parse(evidence.normalized_result),
origin,
);
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
};
/** Verify popup timing, selection, coverage, and shared-context isolation on Chromium. */
export async function verifyPopupEventCoverage(executable) {
const server = createServer((request, response) => {
if (request.url === "/redirect") {
response.writeHead(302, { Location: "/popup" });
response.end();
} else {
response.setHeader("Content-Type", "text/html");
response.end(document(request.url ?? "/"));
}
});
server.listen(0, "127.0.0.1");
await once(server, "listening");
try {
const address = server.address();
assert.ok(address !== null && typeof address === "object");
const origin = `http://127.0.0.1:${address.port}`;
assertNetwork(
await capture(scenarioFor(executable, origin, ["network"])),
origin,
);
const errors = await capture(
scenarioFor(executable, origin, ["page-errors"]),
);
assert.deepEqual(
errors.events.items.map((event) => event.kind),
["page-error", "page-error"],
);
assert.ok(
errors.events.items.some((event) => event.message === "popup failure"),
);
assert.ok(
errors.events.items.some(
(event) => event.message === "nested popup failure",
),
);
for (const family of ["frames", "workers", "websockets", "downloads"]) {
const result = await capture(scenarioFor(executable, origin, [family]));
assert.equal(result.completeness.equality_eligible, false);
assert.ok(result.completeness.missing_sections.includes("events"));
assert.equal(compareWithSelf(result).overall_status, "unknown");
assert.ok(
result.limitations.some((limitation) =>
limitation.includes(`Popup ${family}`),
),
);
}
await verifySharedContext(executable, origin);
await verifyTransports(executable, origin);
return {
mocked: false,
initial_network: true,
redirects: true,
nested_popups: true,
unselected_lifecycle_absent: true,
popup_event_gaps_explicit: true,
incomplete_comparison_unknown: true,
shared_context_isolation: true,
cli: true,
stdio_mcp: true,
};
} finally {
server.closeAllConnections();
await new Promise((resolve, reject) =>
server.close((error) =>
error === undefined ? resolve() : reject(error),
),
);
}
}
+54 -25
View File
@@ -1,7 +1,6 @@
import { execFile } from "node:child_process";
import { readdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname } from "node:path";
import { promisify } from "node:util";
import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
@@ -18,7 +17,6 @@ const duplicateOriginStorage = (origin) => ({
value: {
source: "literal",
value: "first",
classification: "public",
},
},
],
@@ -31,9 +29,12 @@ const duplicateOriginStorage = (origin) => ({
value: {
source: "literal",
value: "second",
classification: "public",
},
},
{
name: "rea-secret-seed",
value: { source: "secret", secret_id: "verifier_storage" },
},
],
},
],
@@ -43,15 +44,21 @@ const duplicateOriginStorage = (origin) => ({
export function browserScenario(browser, origin) {
return browserScenarioSchema.parse({
browser,
start_url: { url: `${origin}/app`, query: [] },
allowed_origins: [origin],
start_url: {
url: `${origin}/app`,
query: [
{
name: "scenario_token",
value: { source: "secret", secret_id: "verifier_url" },
},
],
},
environment: {
viewport: { width: 1_280, height: 720, device_scale_factor: 1.25 },
locale: "en-US",
timezone: "UTC",
color_scheme: "light",
reduced_motion: "reduce",
service_workers: "block",
},
actions: [
{
@@ -61,7 +68,6 @@ export function browserScenario(browser, origin) {
kind: "role",
role: "button",
name: "ax-private-label-value",
exact: true,
},
state: "visible",
},
@@ -72,17 +78,20 @@ export function browserScenario(browser, origin) {
kind: "role",
role: "button",
name: "ax-private-label-value",
exact: true,
},
},
],
storage: duplicateOriginStorage(origin),
request_replay: { mode: "disabled" },
secrets: [],
redaction: {
secret_values: "replace-with-secret-reference",
query_parameter_names: ["token"],
},
secrets: [
{
secret_id: "verifier_url",
environment_variable: "REA_BROWSER_VERIFIER_URL",
},
{
secret_id: "verifier_storage",
environment_variable: "REA_BROWSER_VERIFIER_SECRET",
},
],
capture: {
after_each_step: [
"screenshot",
@@ -115,7 +124,7 @@ export function browserScenario(browser, origin) {
}
/** Run scenario capture through the public one-shot CLI. */
export async function runScenarioCli(scenario, configuration) {
export async function runScenarioCli(scenario) {
const { stdout } = await execute(
process.execPath,
[
@@ -126,16 +135,7 @@ export async function runScenarioCli(scenario, configuration) {
],
{
cwd: process.cwd(),
env: {
...process.env,
REA_BROWSER_SCENARIO_ENABLED: "true",
REA_BROWSER_SCENARIO_AUTO_GRANT: "true",
REA_BROWSER_SCENARIO_EXECUTABLE_ROOTS_JSON: JSON.stringify([
dirname(scenario.browser.executable_path ?? process.execPath),
]),
REA_BROWSER_SCENARIO_ALLOWED_ENV_JSON: "[]",
...configuration,
},
env: process.env,
maxBuffer: 64 * 1_024 * 1_024,
},
);
@@ -175,6 +175,35 @@ export function assertScenarioCapture(capture) {
!storageNames.includes("rea-second-seed")
)
throw new Error("Scenario launch did not retain duplicate-origin seeds");
const secretSeed =
final.artifacts.storage.state === "captured"
? final.artifacts.storage.value.local_storage.find(
({ name }) => name === "rea-secret-seed",
)
: undefined;
if (secretSeed?.value_state !== "redacted-secret")
throw new Error("Scenario launch did not redact its declared secret seed");
const finalUrl =
final.artifacts.url.state === "captured"
? final.artifacts.url.value.url
: "";
if (!finalUrl.includes("[REDACTED:verifier_url]"))
throw new Error("Scenario launch did not redact its declared URL secret");
const stepUrl = capture.steps
.map(({ after_url }) => after_url)
.find(({ url }) => url.includes("[REDACTED:verifier_url]"));
if (stepUrl?.redacted !== true)
throw new Error(
"Scenario step URL did not report declared-secret redaction",
);
const eventUrl = capture.events.items
.filter((event) => "url" in event && event.url !== null)
.map((event) => event.url)
.find(({ url }) => url.includes("[REDACTED:verifier_url]"));
if (eventUrl?.redacted !== true)
throw new Error(
"Scenario event URL did not report declared-secret redaction",
);
if (
capture.events.items.length === 0 ||
capture.completeness.equality_eligible !== true
+119
View File
@@ -0,0 +1,119 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { chromium } from "playwright-core";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../../dist/domain/evidence.js";
import { webScreenshotSchema } from "../../dist/domain/webScreenshot.js";
import { comparePngScreenshots } from "../../dist/browser/PngVisualDiff.js";
/** Verify a real, large PNG through production CLI and stdio MCP boundaries. */
export async function verifyLargeScreenshotE2e(endpoint, origin) {
const browser = await chromium.connectOverCDP(endpoint);
const context = await browser.newContext({
viewport: { width: 2048, height: 1536 },
deviceScaleFactor: 1,
});
const entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
const env = {
PATH: process.env.PATH ?? "/usr/bin:/bin",
REA_LOG_LEVEL: "silent",
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
};
let client;
let transport;
try {
const page = await context.newPage();
await page.goto(`${origin}/screenshot-noise`);
await page.waitForFunction(() => document.body.dataset.ready === "true");
const targets = await (await fetch(`${endpoint}/json/list`)).json();
const target = targets.find((value) => value.url === page.url());
assert.ok(
target,
"Owned screenshot page is absent from actual CDP discovery",
);
const { stdout } = await promisify(execFile)(
process.execPath,
[
entrypoint,
"capture-web-screenshot",
endpoint,
target.id,
"--allowed-origins",
origin,
"--json",
],
{ env, timeout: 60000, maxBuffer: 72 * 1024 * 1024 },
);
const cliEvidence = parseEvidence(JSON.parse(stdout));
assert.equal(cliEvidence.operation, "capture_web_screenshot");
const screenshot = webScreenshotSchema.parse(cliEvidence.normalized_result);
assert.ok(
screenshot.artifact.bytes > 8 * 1024 * 1024,
"Real PNG did not cross the former screenshot byte ceiling",
);
const bytes = Buffer.from(screenshot.artifact.data_base64, "base64");
assert.equal(bytes.byteLength, screenshot.artifact.bytes);
assert.deepEqual(
bytes.subarray(0, 8),
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
);
assert.equal(bytes.readUInt32BE(16), 2048);
assert.equal(bytes.readUInt32BE(20), 1536);
transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
// The SDK defaults to a 10 MiB receive buffer; inline PNG output is larger.
maxBufferSize: 64 * 1024 * 1024,
});
client = new Client({ name: "browser-screenshot-real-e2e", version: "1" });
await client.connect(transport);
const captured = await client.callTool(
{
name: "capture_web_screenshot",
arguments: {
cdp_endpoint: endpoint,
target_id: target.id,
},
},
{ timeout: 60000 },
);
assert.notEqual(captured.isError, true, JSON.stringify(captured));
const mcpEvidence = parseEvidence({
...captured.structuredContent?.evidence,
normalized_result: captured.structuredContent?.result,
});
const mcpScreenshot = webScreenshotSchema.parse(
mcpEvidence.normalized_result,
);
assert.deepEqual(mcpScreenshot.artifact, screenshot.artifact);
const compared = comparePngScreenshots({
before: screenshot.artifact,
after: mcpScreenshot.artifact,
channel_threshold: 0,
});
assert.equal(compared.status, "identical");
assert.equal(compared.changed_pixels, 0);
return {
mocked: false,
cli: true,
stdio_mcp: true,
png_bytes: bytes.byteLength,
};
} finally {
try {
if (client !== undefined) await client.close();
} finally {
try {
if (transport !== undefined) await transport.close();
} finally {
await context.close();
await browser.close();
}
}
}
}
+6 -7
View File
@@ -133,7 +133,7 @@ export function assertBundleAnalysis(result) {
);
}
/** Assert explicitly approved console, JSON, and WebSocket shape capture. */
/** Assert explicitly selected console text and JSON/WebSocket shape capture. */
export function assertSensitiveShapes(result) {
const request = result.network.requests.find(
(item) =>
@@ -151,12 +151,11 @@ export function assertSensitiveShapes(result) {
(event) => event.text_capture.values,
);
if (
!consoleText.some(({ text }) => text.includes("[REDACTED]")) ||
consoleText.some(({ text }) => text.includes("console-secret-value"))
!consoleText.some(({ text }) =>
text.includes("authorization=Bearer console-secret-value"),
)
)
throw new Error(
"Real Chrome approved console text was not safely redacted",
);
throw new Error("Real Chrome selected console text was not preserved");
if (
!result.network.websocket_events.some(
(event) =>
@@ -174,8 +173,8 @@ export function assertSensitiveShapes(result) {
for (const secret of [
"request-body-secret-value",
"response-secret-value",
"request-secret-value",
"websocket-secret-value",
"console-secret-value",
])
if (serialized.includes(secret))
throw new Error(`Approved shape capture retained raw value: ${secret}`);
+3 -3
View File
@@ -6,12 +6,12 @@ export const toolFamilyCatalog = (sources) => {
{
id: "direct",
surface: "official-proxy",
contracts: sources.toolContracts.OFFICIAL_TOOL_CONTRACTS,
contracts: sources.officialContracts.OFFICIAL_TOOL_CONTRACTS,
},
{
id: "enhanced",
surface: "enhanced",
contracts: sources.toolContracts.ENHANCED_TOOL_CONTRACTS,
contracts: sources.enhancedContracts.ENHANCED_TOOL_CONTRACTS,
},
{
id: "native",
@@ -59,7 +59,7 @@ export const toolFamilyCatalog = (sources) => {
{
id: "session",
surface: "session",
contracts: sources.toolContracts.SESSION_TOOL_CONTRACTS,
contracts: sources.sessionContracts.SESSION_TOOL_CONTRACTS,
},
].map(({ id, surface, contracts }) => ({
id,
+3
View File
@@ -37,6 +37,9 @@ const SOURCE_PATHS = {
catalogIdentity: "dist/catalogIdentity.js",
cli: "dist/cli.js",
toolContracts: "dist/contracts/toolContracts.js",
officialContracts: "dist/contracts/officialToolContracts.js",
enhancedContracts: "dist/contracts/enhancedToolContracts.js",
sessionContracts: "dist/contracts/sessionToolContracts.js",
nativeContracts: "dist/contracts/nativeToolContracts.js",
artifactContracts: "dist/contracts/artifactToolContracts.js",
managedContracts: "dist/contracts/managedToolContracts.js",
+21
View File
@@ -0,0 +1,21 @@
export interface DevelopmentTestRequest {
readonly mode: "local" | "changed" | "focused";
readonly paths: readonly string[];
readonly base: string;
readonly dryRun: boolean;
}
/** Parse explicit test paths and an optional branch-comparison base. */
export function parseDevelopmentTestRequest(
mode: string,
arguments_: readonly string[],
): DevelopmentTestRequest;
/** Plan source feedback or a strict explicit run with its build prerequisite. */
export function developmentTestPlan(
request: DevelopmentTestRequest,
baseCommit?: string,
): {
readonly needsBuild: boolean;
readonly vitestArguments: readonly string[];
};
+76
View File
@@ -0,0 +1,76 @@
const sourceProjects = [
"domain",
"services",
"adapters",
"composition",
"conformance",
"evaluation",
];
const sourceTest = (path) =>
path.startsWith("src/") ||
["composition", "conformance", "evaluation"].some((project) =>
path.startsWith(`tests/${project}/`),
);
/** Parse explicit test paths and an optional branch-comparison base. */
export const parseDevelopmentTestRequest = (mode, arguments_) => {
if (!["local", "changed", "focused"].includes(mode))
throw new Error("Expected local, changed, or focused test mode");
const paths = [];
let base = "origin/main";
let dryRun = false;
for (let index = 0; index < arguments_.length; index += 1) {
const argument = arguments_[index];
if (argument === "--dry-run") dryRun = true;
else if (argument === "--base" || argument.startsWith("--base=")) {
if (mode !== "changed") throw new Error("--base requires changed mode");
base = argument === "--base" ? arguments_[++index] : argument.slice(7);
if (!base || base.startsWith("-") || base.includes("\0"))
throw new Error("--base requires a Git revision");
} else {
const path = argument.replaceAll("\\", "/").replace(/^\.\//u, "");
if (
!/^(?:src|tests)\/.+\.test\.ts$/u.test(path) ||
path.split("/").some((part) => part === ".." || part === ".")
)
throw new Error(`Expected an exact repository test path: ${argument}`);
if (mode !== "focused" && !sourceTest(path))
throw new Error(
`Use test:focused for compiled boundary tests: ${path}`,
);
paths.push(path);
}
}
if (mode === "focused" && paths.length === 0)
throw new Error("test:focused requires at least one exact test path");
if (mode === "changed" && paths.length > 0)
throw new Error("Use test:focused for explicit test paths");
return { mode, paths: [...new Set(paths)], base, dryRun };
};
/** Plan source feedback or a strict explicit run with its build prerequisite. */
export const developmentTestPlan = (request, baseCommit) => {
const explicit = request.paths.length > 0;
if (
request.mode === "changed" &&
!/^[a-f0-9]{40,64}$/u.test(baseCommit ?? "")
)
throw new Error("Changed tests require a resolved Git merge base");
const projects = request.mode === "focused" ? [] : sourceProjects;
return {
needsBuild: request.paths.some((path) => !sourceTest(path)),
vitestArguments: [
"run",
...projects.flatMap((project) => ["--project", project]),
...(explicit
? request.paths
: [
request.mode === "changed"
? `--changed=${baseCommit}`
: "--changed",
"--passWithNoTests",
]),
],
};
};
+12
View File
@@ -0,0 +1,12 @@
import { format } from "oxfmt";
import config from "../../.oxfmtrc.json" with { type: "json" };
/** Format generated source with the same options as the repository formatter. */
export const formatGeneratedFile = async (fileName, source) => {
const result = await format(fileName, source, config);
if (result.errors.length > 0)
throw new Error(
`Cannot format ${fileName}: ${result.errors.map(({ message }) => message).join("; ")}`,
);
return result.code;
};
@@ -93,11 +93,6 @@ const sourceOwnedClaims = ({
scenarioId: "decompiler-reconstruction-import",
targets: [obfuscated.target],
}),
completionClaim({
claimId: "managed.runtime-correlation-admission-plan",
scenarioId: "runtime-correlation-admission-plan",
targets: [obfuscated.target],
}),
completionClaim({
claimId: "managed.mvid-and-token-drift",
scenarioId: "mvid-and-token-drift",
+247
View File
@@ -0,0 +1,247 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../../dist/domain/evidence.js";
import { analysisProfileSchema } from "../../dist/domain/analysisProfile.js";
import { nativeValueTraceSchema } from "../../dist/domain/nativeValueTrace.js";
const defaultEntrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
const environment = () => ({
PATH: process.env.PATH ?? "/usr/bin:/bin",
REA_LOG_LEVEL: "silent",
REA_ANALYSIS_PROVIDER: "ghidra",
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
...(process.env.GHIDRA_INSTALL_DIR === undefined
? {}
: { GHIDRA_INSTALL_DIR: process.env.GHIDRA_INSTALL_DIR }),
...(process.env.JAVA_HOME === undefined
? {}
: { JAVA_HOME: process.env.JAVA_HOME }),
});
const verifyGraph = (input, target, procedure, globalAddress) => {
const graph = nativeValueTraceSchema.parse(input);
assert.equal(graph.target_sha256, target.sha256);
assert.equal(graph.seed, procedure);
const nodes = new Map(graph.nodes.map((node) => [node.id, node]));
for (const kind of ["argument-binding", "parameter-use", "return-binding"]) {
const edges = graph.edges.filter((edge) => edge.kind === kind);
assert.ok(edges.length > 0, `Real native dependency graph lacks ${kind}`);
for (const edge of edges) {
const source = nodes.get(edge.source);
const destination = nodes.get(edge.target);
assert.ok(source, `Missing source node ${edge.source}`);
assert.ok(destination, `Missing target node ${edge.target}`);
assert.equal(edge.status, "derived");
if (kind === "argument-binding") {
assert.match(source.operation?.opcode ?? "", /^CALL(?:IND)?$/u);
assert.equal(destination.kind, "parameter");
assert.equal(edge.input_index, destination.parameter.ordinal + 1);
} else if (kind === "parameter-use") {
assert.equal(source.kind, "parameter");
assert.ok(destination.operation.inputs[edge.input_index]);
} else {
assert.equal(source.operation?.opcode, "RETURN");
assert.match(destination.operation?.opcode ?? "", /^CALL(?:IND)?$/u);
assert.equal(edge.input_index, 1);
}
}
}
assert.ok(
graph.nodes.some((node) =>
node.operation?.inputs.some((value) => value.location === globalAddress),
),
"Source-owned global operand is absent from the real dependency graph",
);
for (const node of graph.nodes)
assert.match(node.evidence_id, /^ev_[a-f0-9]{64}$/u);
for (const procedure of graph.procedures) {
assert.equal(procedure.provider, "ghidra");
assert.match(procedure.analysis_profile_digest, /^[a-f0-9]{64}$/u);
}
return graph;
};
/** Verify real Ghidra dependency composition through production CLI and stdio MCP. */
export async function verifyNativeValueE2e(
target,
procedure,
globalAddress,
{ entrypoint = defaultEntrypoint } = {},
) {
const { stdout } = await promisify(execFile)(
process.execPath,
[
entrypoint,
"trace-native-values",
target.path,
procedure,
"--provider",
"ghidra",
"--max-depth",
"2",
"--max-functions",
"8",
"--limit",
"5000",
"--json",
],
{ env: environment(), timeout: 240000, maxBuffer: 72 * 1024 * 1024 },
);
const evidence = parseEvidence(JSON.parse(stdout));
assert.equal(evidence.operation, "trace_native_values");
assert.equal(evidence.provider.id, "rea-workflow");
const workflowProfile = analysisProfileSchema.parse(
evidence.analysis_profile,
);
assert.equal(workflowProfile.provider.id, "rea-workflow");
const upstreamProfile = analysisProfileSchema.parse(
workflowProfile.parameters.upstream_analysis_profile,
);
assert.equal(upstreamProfile.provider.id, "ghidra");
const cliGraph = verifyGraph(
evidence.normalized_result,
target,
procedure,
globalAddress,
);
for (const procedure of cliGraph.procedures)
assert.equal(procedure.analysis_profile_digest, upstreamProfile.digest);
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env: environment(),
stderr: "pipe",
});
const client = new Client({ name: "native-values-real-e2e", version: "1" });
try {
await client.connect(transport);
const opened = await client.callTool(
{
name: "open_binary",
arguments: { path: target.path, provider: "ghidra" },
},
{ timeout: 180000 },
);
assert.notEqual(opened.isError, true, JSON.stringify(opened));
const inventory = await client.callTool({
name: "binary_session",
arguments: {},
});
assert.notEqual(inventory.isError, true, JSON.stringify(inventory));
assert.ok(
inventory.structuredContent?.result?.tool_availability.some(
(tool) => tool.name === "trace_native_values" && tool.available,
),
"Native value tracing is unavailable in real provider discovery",
);
const result = await client.callTool(
{
name: "trace_native_values",
arguments: { procedure, max_depth: 2, max_functions: 8, limit: 5000 },
},
{ timeout: 180000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
const mcpEvidence = parseEvidence(result.structuredContent?.evidence);
assert.equal(mcpEvidence.operation, "trace_native_values");
assert.equal(mcpEvidence.provider.id, "rea-workflow");
assert.deepEqual(mcpEvidence.analysis_profile, workflowProfile);
const mcpGraph = verifyGraph(
result.structuredContent?.result,
target,
procedure,
globalAddress,
);
assert.deepEqual(mcpEvidence.normalized_result, mcpGraph);
assert.deepEqual(mcpGraph, cliGraph);
const analyzed = await client.callTool(
{ name: "analyze_function", arguments: { procedure } },
{ timeout: 180000 },
);
assert.notEqual(analyzed.isError, true, JSON.stringify(analyzed));
const functionEvidence = parseEvidence(
analyzed.structuredContent?.evidence,
);
assert.equal(functionEvidence.operation, "analyze_function");
assert.equal(functionEvidence.provider.id, "rea-workflow");
const functionProfile = analysisProfileSchema.parse(
functionEvidence.analysis_profile,
);
assert.deepEqual(
functionProfile.parameters.upstream_analysis_profile,
upstreamProfile,
);
assert.deepEqual(
functionEvidence.normalized_result,
analyzed.structuredContent?.result,
);
const comparison = await client.callTool(
{
name: "compare_functions",
arguments: {
left: analyzed.structuredContent.evidence,
right: analyzed.structuredContent.evidence,
},
},
{ timeout: 180000 },
);
assert.notEqual(comparison.isError, true, JSON.stringify(comparison));
const comparisonEvidence = parseEvidence(
comparison.structuredContent?.evidence,
);
assert.equal(comparisonEvidence.normalized_result.status, "unchanged");
assert.deepEqual(
comparisonEvidence.normalized_result,
comparison.structuredContent?.result,
);
assert.ok(
comparisonEvidence.evidence_links.includes(functionEvidence.evidence_id),
);
const bundle = await client.callTool({
name: "get_evidence_bundle",
arguments: {},
});
assert.notEqual(bundle.isError, true, JSON.stringify(bundle));
for (const evidence of [
mcpEvidence,
functionEvidence,
comparisonEvidence,
]) {
assert.deepEqual(
bundle.structuredContent?.result?.records.find(
(record) => record.evidence_id === evidence.evidence_id,
),
evidence,
);
}
const closed = await client.callTool({
name: "close_binary",
arguments: {},
});
assert.notEqual(closed.isError, true, JSON.stringify(closed));
const status = await client.callTool({
name: "binary_session",
arguments: {},
});
assert.equal(status.structuredContent?.result?.open, false);
} finally {
await client.close();
await transport.close();
}
return {
mocked: false,
cli: true,
stdio_mcp: true,
inline_evidence: true,
direct_function_comparison: true,
nodes: cliGraph.total_nodes,
edges: cliGraph.total_edges,
decompilations: cliGraph.decompilations,
unknowns: cliGraph.unknowns.length,
};
}
+5 -2
View File
@@ -1,4 +1,4 @@
import { format } from "prettier";
import { formatGeneratedFile } from "./format-generated-file.mjs";
import { assertSameNames, digest, loadSources } from "./catalog-core.mjs";
import {
@@ -67,4 +67,7 @@ export const createProductCatalog = async (root) => {
/** Stable checked-in representation of the product catalog. */
export const serializeProductCatalog = (catalog) =>
format(JSON.stringify(catalog, null, 2), { parser: "json" });
formatGeneratedFile(
"docs/product-catalog.json",
JSON.stringify(catalog, null, 2),
);
+8 -13
View File
@@ -31,23 +31,21 @@ export const verifyHopperFunctionBasics = async (
arguments: {
procedure,
direction: "outgoing",
limit: 10,
max_instructions: 100,
},
},
options,
),
"procedure_references",
);
if (!Array.isArray(references?.references?.items)) {
throw new Error("procedure_references returned an invalid bounded result");
if (!Array.isArray(references?.references)) {
throw new Error("procedure_references returned an invalid result");
}
const instructions = requireMcpResult(
await client.callTool(
{
name: "read_function_instructions",
arguments: { procedure, limit: 2 },
arguments: { procedure },
},
options,
),
@@ -55,17 +53,14 @@ export const verifyHopperFunctionBasics = async (
);
if (
instructions?.procedure?.address !== procedure ||
!Array.isArray(instructions.instructions?.items) ||
instructions.instructions.items.length === 0 ||
instructions.instructions.items.length > 2
!Array.isArray(instructions.instructions) ||
instructions.instructions.length === 0
) {
throw new Error(
"read_function_instructions returned an invalid bounded window",
);
throw new Error("read_function_instructions returned an invalid result");
}
return {
outgoingReferenceCount: references.references.items.length,
instructionWindowCount: instructions.instructions.items.length,
outgoingReferenceCount: references.references.length,
instructionWindowCount: instructions.instructions.length,
};
};
+47
View File
@@ -0,0 +1,47 @@
import assert from "node:assert/strict";
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProvider.js";
import {
requireMcpResult,
requireEvidenceProvider,
} from "./mcp-verifier-results.mjs";
/** Verify real Hopper inventories and address filters through the MCP transport. */
export async function verifyHopperInventories(client, options) {
const inventoryCounts = {};
for (const operation of ["list_strings", "list_names"]) {
const called = await client.callTool(
{ name: operation, arguments: {} },
options,
);
const inventory = requireMcpResult(called, operation);
requireEvidenceProvider(called, operation, HOPPER_PROVIDER_IDENTITY.id);
assert.ok(
Array.isArray(inventory) && inventory.length > 0,
`${operation} omitted the real fixture inventory`,
);
let previous = -1n;
for (const item of inventory) {
assert.match(item.address, /^0x[0-9a-f]+$/u);
assert.equal(typeof item.value, "string");
const address = BigInt(item.address);
assert.ok(
address > previous,
`${operation} is not numerically address-sorted`,
);
previous = address;
}
const filtered = requireMcpResult(
await client.callTool(
{
name: operation,
arguments: { address: inventory[0].address },
},
options,
),
operation,
);
assert.deepEqual(filtered, [inventory[0]]);
inventoryCounts[operation] = inventory.length;
}
return inventoryCounts;
}
+1 -1
View File
@@ -39,7 +39,7 @@ const observeVerifierLineage = async (run, observedAt) => {
run,
observedAt,
null,
"Windows verifier lineage ownership is unavailable without Job Objects",
"The verifier has no retained native Job Object lease for lineage observation; provider jobs are verified by their native owners",
);
try {
const { members, observerPid } = await processSnapshot();
+12 -1
View File
@@ -1,6 +1,7 @@
import { execFile } from "node:child_process";
import { lstat } from "node:fs/promises";
import { promisify } from "node:util";
import { Ajv2020 } from "ajv/dist/2020.js";
/** Promisified child_process execution helper. */
export const exec = promisify(execFile);
@@ -28,9 +29,19 @@ export const runWithStatus = async (command, args, env) => {
/** Parse JSON from a command output string. */
export const json = (text) => JSON.parse(text);
/** Verify that the complete advertised MCP catalog matches session metadata. */
/** Verify schema validity and that the MCP catalog matches session metadata. */
export const verifyCompleteToolCatalog = async (client, options) => {
const listed = await client.listTools(undefined, options);
const ajv = new Ajv2020({ strict: false, validateFormats: false });
for (const tool of listed.tools) {
for (const kind of ["inputSchema", "outputSchema"]) {
const schema = tool[kind];
if (schema !== undefined && !ajv.validateSchema(schema))
throw new Error(
`packaged MCP has invalid ${tool.name}.${kind}: ${ajv.errorsText(ajv.errors)}`,
);
}
}
const status = await client.callTool(
{ name: "binary_session", arguments: {} },
options,
+1 -1
View File
@@ -23,7 +23,7 @@ const runtimeFiles = isMcpMode
if (!(await compiledRuntimeExists(runtimeFiles))) {
process.stderr.write(
`REA's compiled runtime is missing. Run \`npm ci\` in ${packageRoot} to install dependencies and build REA, then restart it. If this is an installed package, reinstall rea-agents.\n`,
`REA's compiled runtime is missing. Run \`npm ci && npm run build:cached\` in ${packageRoot} to install dependencies and build REA, then restart it. If this is an installed package, reinstall rea-agents.\n`,
);
process.exitCode = 1;
} else if (isMcpMode) {
+95
View File
@@ -0,0 +1,95 @@
import { spawn } from "node:child_process";
import { stat } from "node:fs/promises";
import { resolve } from "node:path";
import {
developmentTestPlan,
parseDevelopmentTestRequest,
} from "./lib/development-tests.mjs";
let child;
let forceTimer;
let shutdownSignal;
const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
for (const signal of Object.keys(signalExitCodes))
process.on(signal, () => {
if (shutdownSignal !== undefined) return;
shutdownSignal = signal;
if (child !== undefined && child.exitCode === null) {
child.kill(signal);
forceTimer = setTimeout(() => child?.kill("SIGKILL"), 1_000);
forceTimer.unref?.();
}
});
try {
const [mode, ...arguments_] = process.argv.slice(2);
const request = parseDevelopmentTestRequest(mode, arguments_);
for (const path of request.paths) {
if (!(await stat(path)).isFile())
throw new Error(`Test path is not a file: ${path}`);
}
let baseCommit;
if (request.mode === "changed") {
const base = await run("git", ["merge-base", "HEAD", request.base], true);
if (base.code !== 0)
throw new Error(
`Cannot resolve test base ${request.base}. Fetch origin/main or pass --base REVISION.\n${base.output.trim()}`,
);
baseCommit = base.output.trim();
}
const plan = developmentTestPlan(request, baseCommit);
if (request.dryRun) {
console.log(JSON.stringify({ ...request, baseCommit, ...plan }, null, 2));
} else {
console.error(
request.mode === "focused"
? `Focused tests: ${request.paths.join(", ")}`
: `Source-test feedback${baseCommit ? ` from merge base ${baseCommit}` : ""}; compiled boundaries and real providers need explicit verification.`,
);
if (plan.needsBuild) {
if (!process.env.npm_execpath)
throw new Error("Run compiled tests through npm run test:focused");
const built = await run(process.execPath, [
process.env.npm_execpath,
"run",
"build:cached",
]);
if (built.code !== 0) process.exitCode = built.code;
}
if (!process.exitCode && !shutdownSignal) {
const tested = await run(process.execPath, [
resolve("node_modules/vitest/vitest.mjs"),
...plan.vitestArguments,
]);
process.exitCode = tested.code;
}
}
} catch (cause) {
console.error(cause instanceof Error ? cause.message : String(cause));
process.exitCode = 1;
}
if (shutdownSignal) process.exitCode = signalExitCodes[shutdownSignal];
function run(executable, arguments_, capture = false) {
if (shutdownSignal)
return Promise.resolve({
code: signalExitCodes[shutdownSignal],
output: "",
});
child = spawn(executable, arguments_, {
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
});
let output = "";
if (capture) {
child.stdout.on("data", (data) => (output += data.toString()));
child.stderr.on("data", (data) => (output += data.toString()));
}
return new Promise((resolveResult, reject) => {
child.once("error", reject);
child.once("close", (code) => {
clearTimeout(forceTimer);
resolveResult({ code: code ?? 1, output });
});
});
}
+10 -2
View File
@@ -12,6 +12,7 @@ const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
let lockHeld = false;
let child;
let forceTimer;
let shutdownSignal;
process.on("SIGINT", () => handleSignal("SIGINT"));
@@ -96,14 +97,21 @@ function runCommand() {
stdio: "inherit",
});
child.once("error", reject);
child.once("close", (code) => resolveResult({ code }));
child.once("close", (code) => {
clearTimeout(forceTimer);
resolveResult({ code });
});
});
}
function handleSignal(signal) {
if (shutdownSignal !== undefined) return;
shutdownSignal = signal;
if (child !== undefined && child.exitCode === null) child.kill(signal);
if (child !== undefined && child.exitCode === null) {
child.kill(signal);
forceTimer = setTimeout(() => child?.kill("SIGKILL"), 1_000);
forceTimer.unref?.();
}
}
async function readOwner() {
+6 -29
View File
@@ -82,7 +82,7 @@ try {
"depth = 1",
],
["static", "inferred"],
["runtime", "replay"],
["runtime", "behavioral probes"],
],
prompt: `Without directly reading target files, compare the default export in parser.mjs between ${targets.javascriptShapeLeft} and ${targets.javascriptShapeRight}. Use REA to analyze each shipped artifact and then compare its exact static export return shapes. State the exact heading-shape change, cite produced Evidence, and distinguish static inference from runtime semantics.`,
},
@@ -283,13 +283,13 @@ async function createTargets(root, includeManaged) {
mkdir(javascriptShapeRight, { recursive: true }),
]);
await Promise.all([
cp(
join(repositoryRoot, "tests/fixtures/replay/parser.mjs"),
writeFile(
join(javascriptShapeLeft, "parser.mjs"),
'export default function parse() { return { heading: "Title" }; }\n',
),
cp(
join(repositoryRoot, "tests/fixtures/replay/parser-v2.mjs"),
writeFile(
join(javascriptShapeRight, "parser.mjs"),
'export default function parse() { return { heading: { text: "Title", level: 1 } }; }\n',
),
]);
@@ -332,20 +332,6 @@ async function createTargets(root, includeManaged) {
}
async function runCodex(prompt) {
const mcpEnvironment = [
["REA_BROWSER_OBSERVE_ENABLED", "true"],
[
"REA_BROWSER_CDP_ENDPOINTS_JSON",
JSON.stringify(["http://127.0.0.1:9222"]),
],
[
"REA_BROWSER_ALLOWED_ORIGINS_JSON",
JSON.stringify(["http://127.0.0.1:3000"]),
],
];
const mcpEnvironmentOverride = `mcp_servers.rea.env={${mcpEnvironment
.map(([name, value]) => `${name}=${JSON.stringify(value)}`)
.join(",")}}`;
const arguments_ = [
"exec",
"--ignore-user-config",
@@ -367,21 +353,12 @@ async function runCodex(prompt) {
`mcp_servers.rea.args=${JSON.stringify([join(repositoryRoot, "scripts/rea.mjs"), "mcp"])}`,
"-c",
`mcp_servers.rea.startup_timeout_sec=${String(MCP_STARTUP_POLICY.codexStartupTimeoutSeconds)}`,
"-c",
mcpEnvironmentOverride,
...(optionalModel === undefined ? [] : ["--model", optionalModel]),
prompt,
];
const child = spawn(codex, arguments_, {
cwd: evaluationRoot,
env: {
...process.env,
REA_BROWSER_OBSERVE_ENABLED: "true",
REA_BROWSER_CDP_ENDPOINTS_JSON: JSON.stringify(["http://127.0.0.1:9222"]),
REA_BROWSER_ALLOWED_ORIGINS_JSON: JSON.stringify([
"http://127.0.0.1:3000",
]),
},
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
const events = [];
+133
View File
@@ -0,0 +1,133 @@
import { execFile } from "node:child_process";
import { mkdtemp, rm } 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 { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import { inspectAppleDispatchMetadata } from "../dist/native/AppleDispatchMetadata.js";
if (process.platform !== "darwin")
throw new Error(
"Apple dispatch verification requires macOS with a host Objective-C compiler and Foundation SDK",
);
const exec = promisify(execFile);
const root = await mkdtemp(join(tmpdir(), "rea-dispatch-"));
try {
const targetPath = join(root, "fixture");
await exec("/usr/bin/xcrun", [
"clang",
"-O2",
"-framework",
"Foundation",
"-Wl,-no_fixup_chains",
fileURLToPath(
new URL("../tests/conformance/native/dispatch.m", import.meta.url),
),
"-o",
targetPath,
]);
const results = [];
for (const variant of ["symbols", "stripped"]) {
if (variant === "stripped")
await exec("/usr/bin/strip", ["-x", targetPath]);
const target = await parseBinaryTarget(targetPath);
if (!target.ok) throw target.error;
const observation = await inspectAppleDispatchMetadata(target.value, 5000);
const metadata = observation.result;
if (
!metadata.objc_dispatch_implementations.some(
(item) =>
item.class_name === "ReaDispatchFixture" &&
item.selector === "performAction:" &&
item.implementation_address !== null,
) ||
!metadata.objc_dispatch_implementations.some(
(item) =>
item.selector === "fixtureVersion" && item.method_type === "class",
) ||
!metadata.objc_ivars.some(
(item) => item.name === "state" && item.offset !== null,
) ||
!metadata.objc_protocol_records.some(
(protocol) =>
protocol.name === "ReaDispatchProtocol" &&
protocol.methods.some(
(method) => method.selector === "performAction:",
) &&
protocol.optional_methods.some(
(method) => method.selector === "optionalFixtureValue",
),
)
)
throw new Error(
`Apple metadata fixture drifted: ${JSON.stringify(observation)}`,
);
results.push({
variant,
target_sha256: target.value.sha256,
implementations: metadata.objc_dispatch_implementations.length,
ivars: metadata.objc_ivars.length,
coverage: metadata.coverage,
});
}
const swiftPath = join(root, "libReaWitnessFixture.dylib");
await exec("/usr/bin/xcrun", [
"swiftc",
"-emit-library",
"-O",
"-Xlinker",
"-no_fixup_chains",
fileURLToPath(
new URL("../tests/conformance/native/dispatch.swift", import.meta.url),
),
"-o",
swiftPath,
]);
for (const variant of ["swift-symbols", "swift-stripped"]) {
if (variant === "swift-stripped")
await exec("/usr/bin/strip", ["-x", swiftPath]);
const target = await parseBinaryTarget(swiftPath);
if (!target.ok) throw target.error;
const observation = await inspectAppleDispatchMetadata(target.value, 5000);
const metadata = observation.result;
if (
!metadata.swift_dispatch_slots.some(
(slot) =>
slot.table_kind === "class_vtable" &&
slot.owner === "ReaVtableFixture" &&
slot.decode.status === "decoded",
)
)
throw new Error(
`Swift class vtable fixture unavailable: ${JSON.stringify(metadata.coverage)}`,
);
if (
!metadata.swift_conformances.some(
(item) =>
item.type_name === "ReaWitnessFixture" &&
item.protocol_name === "ReaWitnessFixtureProtocol",
) ||
!metadata.swift_dispatch_slots.some(
(item) =>
item.owner === "ReaWitnessFixture: ReaWitnessFixtureProtocol" &&
item.implementation_address !== null,
)
)
throw new Error(
`Swift metadata fixture drifted: ${JSON.stringify(observation)}`,
);
results.push({
variant,
target_sha256: target.value.sha256,
conformances: metadata.swift_conformances.length,
slots: metadata.swift_dispatch_slots.length,
coverage: metadata.coverage,
});
}
process.stdout.write(
`${JSON.stringify({ ok: true, target_executed: false, fixtures: results })}\n`,
);
} finally {
await rm(root, { recursive: true, force: true });
}
+103
View File
@@ -0,0 +1,103 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { execFile } from "node:child_process";
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import {
artifactCli,
artifactMcpResult,
withArtifactMcp,
} from "./lib/artifact-e2e.mjs";
if (process.platform !== "darwin")
throw new Error(
"Asset catalog E2E requires macOS, Xcode actool and assetutil",
);
const exec = promisify(execFile);
await exec("/usr/bin/xcrun", ["--find", "actool"]);
await exec("/usr/bin/xcrun", ["--find", "clang"]);
await exec("/usr/bin/assetutil", ["--version"]);
const root = await mkdtemp(join(tmpdir(), "rea-asset-e2e-"));
try {
const app = join(root, "Fixture.app");
const resources = join(app, "Contents", "Resources");
await mkdir(resources, { recursive: true });
await mkdir(join(app, "Contents", "MacOS"), { recursive: true });
await exec("/usr/bin/xcrun", [
"clang",
fileURLToPath(
new URL(
"../tests/conformance/interface-builder/fixture.c",
import.meta.url,
),
),
"-o",
join(app, "Contents", "MacOS", "Fixture"),
]);
await writeFile(
join(app, "Contents", "Info.plist"),
'<?xml version="1.0"?><plist version="1.0"><dict><key>CFBundleExecutable</key><string>Fixture</string><key>CFBundleIdentifier</key><string>dev.rea.asset-fixture</string></dict></plist>',
);
await exec("/usr/bin/xcrun", [
"actool",
fileURLToPath(
new URL("../tests/conformance/native/Assets.xcassets", import.meta.url),
),
"--compile",
resources,
"--platform",
"macosx",
"--minimum-deployment-target",
"12.0",
]);
const raw = JSON.parse(
(
await exec("/usr/bin/assetutil", [
"--info",
join(resources, "Assets.car"),
])
).stdout,
);
const sha256 = createHash("sha256")
.update(await readFile(join(resources, "Assets.car")))
.digest("hex");
const cli = await artifactCli("inspect-asset-catalog", app);
assert.deepEqual(
cli.records.map(({ metadata }) => metadata),
raw,
);
assert.equal(cli.catalogs[0].sha256, sha256);
assert.ok(
cli.records.some(
({ asset_name, metadata }) =>
asset_name === "FixtureColor" && metadata.AssetType === "Color",
),
);
await withArtifactMcp(app, async (client) => {
assert.deepEqual(
await artifactMcpResult(client, "inspect_asset_catalog"),
cli,
);
const first = await artifactMcpResult(client, "inspect_asset_catalog", {
limit: 1,
});
assert.equal(first.next_offset, 1);
const next = await artifactMcpResult(client, "inspect_asset_catalog", {
offset: first.next_offset,
});
assert.deepEqual([...first.records, ...next.records], cli.records);
const invalid = await client.callTool({
name: "inspect_asset_catalog",
arguments: { offset: -1 },
});
assert.equal(invalid.isError, true);
});
process.stdout.write(
`${JSON.stringify({ ok: true, mocked: false, cli: true, stdio_mcp: true, records: raw.length, pagination: true, malformed_input_rejected: true })}\n`,
);
} finally {
await rm(root, { recursive: true, force: true });
}
+125
View File
@@ -0,0 +1,125 @@
import { execFile } from "node:child_process";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import {
artifactCli,
artifactMcpResult,
withArtifactMcp,
} from "./lib/artifact-e2e.mjs";
if (process.platform !== "darwin")
throw new Error(
"Real Foundation keyed archive verification requires macOS and Xcode Swift tools",
);
const root = await mkdtemp(join(tmpdir(), "rea-keyed-fixture-"));
try {
const archive = join(root, "model.plist");
await promisify(execFile)("/usr/bin/xcrun", [
"swift",
"-module-cache-path",
join(root, "modules"),
fileURLToPath(
new URL(
"../tests/conformance/native/keyed-archive.swift",
import.meta.url,
),
),
archive,
]);
const graph = await artifactCli("inspect-keyed-archive", archive);
const digest = createHash("sha256")
.update(await readFile(archive))
.digest("hex");
assert.equal(graph.archive_sha256, digest);
assert.equal(graph.target_sha256, digest);
await withArtifactMcp(archive, async (client) => {
assert.deepEqual(
await artifactMcpResult(client, "inspect_keyed_archive"),
graph,
);
const page = await artifactMcpResult(client, "inspect_keyed_archive", {
offset: 1,
limit: 1,
});
assert.deepEqual(page.objects, [graph.objects[1]]);
assert.equal(page.next_offset, 2);
const invalid = await client.callTool({
name: "inspect_keyed_archive",
arguments: { path: "../other.plist" },
});
assert.equal(invalid.isError, true);
});
const xml = join(root, "model.xml.plist");
await promisify(execFile)("/usr/bin/plutil", [
"-convert",
"xml1",
"-o",
xml,
archive,
]);
const xmlGraph = await artifactCli("inspect-keyed-archive", xml);
const expected = JSON.parse(
await readFile(
new URL(
"../tests/fixtures/golden/keyed-archive/graph.json",
import.meta.url,
),
"utf8",
),
);
assert.deepEqual(
Object.fromEntries(
Object.entries(xmlGraph).filter(
([key]) =>
!["archive_path", "archive_sha256", "target_sha256"].includes(key),
),
),
expected,
);
assert.deepEqual(
graph.references.map(({ source, path, target, status }) => ({
source,
path,
target,
status,
})),
xmlGraph.references.map(({ source, path, target, status }) => ({
source,
path,
target,
status,
})),
);
if (
graph?.archive_format !== "binary-plist" ||
!graph.references.some(
(link) =>
link.source !== null &&
link.source === link.target &&
link.status === "resolved",
) ||
!graph.objects.some((item) => item.class_name?.includes("ReaArchiveRecord"))
)
throw new Error(
`Real Foundation archive graph drifted: ${JSON.stringify(graph)}`,
);
const record = graph.objects.find((item) =>
item.class_name?.includes("ReaArchiveRecord"),
);
if (
graph.references.filter(
(link) => link.target === record.id && link.source !== record.id,
).length < 2
)
throw new Error("Real shared object identity was not preserved");
process.stdout.write(
`${JSON.stringify({ ok: true, mocked: false, cli: true, stdio_mcp: true, xml_golden: true, format: graph.archive_format, objects: graph.total_objects, references: graph.total_references, shared_identity: true, cyclic_identity: true, target_classes_instantiated_by_reader: false })}\n`,
);
} finally {
await rm(root, { recursive: true, force: true });
}
@@ -0,0 +1,38 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { constants } from "node:buffer";
import { createHash } from "node:crypto";
import { canonicalDigest } from "../dist/domain/comparisonSemantics.js";
import { createEvidence, parseEvidence } from "../dist/domain/evidence.js";
// This opt-in check crosses the actual engine string limit; keep it outside
// routine source tests. Build first, then run this script with the local Node.
const leaf = "x".repeat(1024 * 1024);
const encodedLeaf = JSON.stringify(leaf);
const count = Math.ceil(constants.MAX_STRING_LENGTH / encodedLeaf.length) + 1;
const value = { payload: Array.from({ length: count }, () => leaf) };
const expected = createHash("sha256");
expected.update('{"payload":[');
for (let index = 0; index < count; index += 1) {
if (index > 0) expected.update(",");
expected.update(encodedLeaf);
}
expected.update("]}");
assert.equal(canonicalDigest(value), expected.digest("hex"));
const evidence = createEvidence(
undefined,
{ id: "large-json-fixture", name: "Large JSON fixture", version: "1" },
{ operation: "inspect_fixture", parameters: {}, result: value },
);
assert.equal(parseEvidence(evidence).evidence_id, evidence.evidence_id);
process.stdout.write(
`${JSON.stringify({
verified: true,
serialized_value_bytes: count * encodedLeaf.length + count - 1 + 14,
engine_string_limit: constants.MAX_STRING_LENGTH,
evidence_id: evidence.evidence_id,
})}\n`,
);
+35 -77
View File
@@ -1,4 +1,4 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import assert from "node:assert/strict";
@@ -9,10 +9,8 @@ import { inspectManagedNativeBoundariesBytes } from "../dist/dotnet/ManagedNativ
import { compareManagedMemberPaths } from "../dist/application/ManagedMemberComparisonService.js";
import { verifyManagedNativeBoundariesEvidence } from "../dist/application/ManagedNativeVerificationService.js";
import { importManagedReconstructionEvidence } from "../dist/application/ManagedReconstructionService.js";
import { planManagedRuntimeCorrelationEvidence } from "../dist/application/ManagedRuntimeCorrelationService.js";
import { projectManagedApplicationGraphEvidence } from "../dist/application/ManagedApplicationGraphService.js";
import { traceApplicationFeatureEvidence } from "../dist/application/JavaScriptApplicationWorkflowService.js";
import { createPermissionAuthority } from "../dist/application/PermissionAuthority.js";
import { MANAGED_STATIC_PROVIDER } from "../dist/application/InvestigationProviders.js";
import { createEvidence } from "../dist/domain/evidence.js";
import {
@@ -250,31 +248,8 @@ try {
assert.equal(obfuscatedMembers.types[0]?.full_name, "Fixture.ꙮType");
assert.equal(obfuscatedMembers.methods[0]?.name, "λ⛧");
assert.equal(obfuscatedMembers.fields[0]?.name, "字段");
const runtimeExecutable = join(workspace, "dotnet");
await writeFile(runtimeExecutable, "#!/bin/sh\n");
const runtimeCeiling = {
capability: "managed_runtime",
roots: [workspace],
executables: [runtimeExecutable],
environment_names: [],
network: "none",
mount: false,
};
const runtimeAuthority = await createPermissionAuthority(
[runtimeCeiling],
[
{
...runtimeCeiling,
grant_id: "administrator:managed_runtime",
lifetime: "administrator",
operation_identity: null,
expires_at: null,
},
],
);
assert.equal(runtimeAuthority.ok, true);
const runtimeMethod = obfuscatedMembers.methods[0];
assert.ok(runtimeMethod);
const reconstructionMethod = obfuscatedMembers.methods[0];
assert.ok(reconstructionMethod);
const obfuscatedMembersEvidence = createEvidence(
undefined,
MANAGED_STATIC_PROVIDER,
@@ -297,9 +272,9 @@ try {
},
methods: [
{
token: runtimeMethod.token,
signature_sha256: runtimeMethod.signature.raw_sha256,
normalized_il_sha256: runtimeMethod.body.normalized_il_sha256,
token: reconstructionMethod.token,
signature_sha256: reconstructionMethod.signature.raw_sha256,
normalized_il_sha256: reconstructionMethod.body.normalized_il_sha256,
reconstruction: {
kind: "decompiled-csharp",
language: "csharp",
@@ -316,48 +291,6 @@ try {
false,
);
assert.equal(reconstructionImport.value.confidence, "inferred");
const runtimePlan = await planManagedRuntimeCorrelationEvidence(
{
policy: () => ({
status: "enabled",
roots: [workspace],
executablePath: runtimeExecutable,
}),
authority: runtimeAuthority.value,
},
{
static_members: obfuscatedMembersEvidence,
method: {
token: runtimeMethod.token,
signature_sha256: runtimeMethod.signature.raw_sha256,
normalized_il_sha256: runtimeMethod.body.normalized_il_sha256,
},
requested_effect: "debugger",
host: {
os: "linux",
clr_family: "dotnet",
architecture: "x86_64",
},
bounds: {
timeout_ms: 5_000,
max_threads: 32,
max_output_bytes: 65_536,
allow_network: false,
allow_ui: false,
},
},
);
assert.equal(runtimePlan.ok, true);
assert.equal(runtimePlan.value.normalized_result.executed, false);
assert.equal(
runtimePlan.value.normalized_result.authority_model.capability,
"managed_runtime",
);
assert.equal(
runtimePlan.value.normalized_result.effect_taxonomy.uses_debugger,
true,
);
const left = await fixture("token-drift-left.exe", {
methodName: "StableSemanticSlice",
});
@@ -379,7 +312,10 @@ try {
});
assert.equal(comparison.ok, true);
const comparisonResult = comparison.value.normalized_result;
assert.equal(comparisonResult.algorithm.name_matching, "not-used");
assert.equal(
comparisonResult.algorithm.name_matching,
"exact-signature-fallback",
);
assert.equal(comparisonResult.matching.exact_il_signature, 1);
assert.equal(
comparisonResult.methods.some(
@@ -391,6 +327,30 @@ try {
true,
);
const unavailableBodyLeft = await fixture("partial-body-left.exe", {
methods: [{ name: "PartialBodyTarget", body: Buffer.from([0xff]) }],
});
const unavailableBodyRight = await fixture("partial-body-right.exe", {
methods: [{ name: "PartialBodyTarget", body: defaultIlBody }],
});
const partialBodyComparison = await compareManagedMemberPaths({
leftPath: unavailableBodyLeft.path,
rightPath: unavailableBodyRight.path,
memberLimits: {
maxFileBytes: 1024 * 1024,
...memberLimits,
},
comparisonLimits,
});
assert.equal(partialBodyComparison.ok, true);
const partialBodyMethod =
partialBodyComparison.value.normalized_result.methods.find(
({ left }) => left?.name === "PartialBodyTarget",
);
assert.equal(partialBodyMethod?.status, "unknown");
assert.equal(partialBodyMethod?.match.basis, "exact-signature");
assert.equal(partialBodyMethod?.dimensions.includes("body-coverage"), true);
const nativeOnly = await fixtureBytes(
"native-only.exe",
buildNativePeFixture(),
@@ -436,7 +396,7 @@ try {
process.stdout.write(
`${JSON.stringify({
verified:
12 +
11 +
(operatorManifest === null ? 0 : 1) +
(ilspyOracle === null ? 0 : 1),
managedSurfaces: [
@@ -446,7 +406,6 @@ try {
"compare_managed_members",
"verify_managed_native_boundaries",
"import_managed_reconstruction",
"plan_managed_runtime_correlation",
"project_managed_application_graph",
],
coverage: [
@@ -457,7 +416,6 @@ try {
"managed-application-graph-projection",
"unicode-obfuscated-identifiers",
"decompiler-reconstruction-import",
"runtime-correlation-admission-plan",
"mvid-and-token-drift",
"not-managed",
"malformed-metadata",
+161
View File
@@ -0,0 +1,161 @@
import { execFile, spawn } from "node:child_process";
import { mkdtemp, mkdir, writeFile, readFile, rm } 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 { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import { observeNativeUi } from "../dist/native/NativeUiObservation.js";
if (process.platform !== "darwin")
throw new Error(
"Native UI verification requires macOS, an interactive desktop and Xcode command-line tools",
);
const root = await mkdtemp(join(tmpdir(), "rea-ui-fixture-"));
let child;
let fixturePid;
try {
const contents = join(root, "Fixture.app", "Contents");
await mkdir(join(contents, "MacOS"), { recursive: true });
await writeFile(
join(contents, "Info.plist"),
`<?xml version="1.0"?><plist version="1.0"><dict><key>CFBundleExecutable</key><string>fixture</string><key>CFBundleIdentifier</key><string>io.rea.source-owned-ui-fixture</string><key>CFBundleName</key><string>REA UI Fixture</string><key>CFBundlePackageType</key><string>APPL</string></dict></plist>`,
);
const executable = join(contents, "MacOS", "fixture");
await promisify(execFile)("/usr/bin/xcrun", [
"swiftc",
"-module-cache-path",
join(root, "modules"),
fileURLToPath(
new URL("../tests/conformance/native/ui.swift", import.meta.url),
),
"-o",
executable,
]);
const coordinates = join(root, "scope.json");
await writeFile(coordinates, "");
child = spawn(
"/usr/bin/open",
["-W", "-n", "-g", "--stdout", coordinates, join(root, "Fixture.app")],
{ stdio: "ignore" },
);
let scope;
const deadline = Date.now() + 20000;
while (Date.now() < deadline) {
const output = await readFile(coordinates, "utf8");
if (output.includes("\n")) {
scope = JSON.parse(output.split("\n")[0]);
break;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
if (scope === undefined)
throw new Error(
"Source-owned UI fixture failed to expose one window within 20 seconds",
);
fixturePid = scope.pid;
const target = await parseBinaryTarget(executable);
if (!target.ok) throw target.error;
const observation = await observeNativeUi(target.value, "observe_native_ui", {
...scope,
});
if (
!observation.ok &&
!/accessibility-denied|screen-recording-denied/u.test(
observation.error.message,
)
)
throw observation.error;
if (!observation.ok && !process.argv.includes("--allow-permission-denial"))
throw new Error(
`Native UI E2E requires successful capture; permissions prevented coverage: ${observation.error.message}. Use --allow-permission-denial only for the separate permission diagnostic lane.`,
);
let scenarioStatus = "not-run-permission-denied";
if (observation.ok) {
const button = observation.value.initial.nodes.find(
(node) => node.title === "Increment REA fixture",
);
if (button === undefined)
throw new Error("Source-owned fixture button is absent from AX capture");
const scenario = await observeNativeUi(
target.value,
"capture_native_ui_scenario",
{
...scope,
restore: "leave-as-is",
screenshot: false,
steps: [
{ kind: "click", path: button.path },
{ kind: "wait", milliseconds: 200 },
],
},
);
if (!scenario.ok) throw scenario.error;
if (
scenario.value.steps.some((step) => step.outcome !== "completed") ||
!scenario.value.steps
.at(-1)
?.after?.nodes.some((node) => node.title === "REA fixture incremented")
)
throw new Error(
`Source-owned AX scenario failed: ${JSON.stringify(scenario.value.steps.map(({ outcome, reason }) => ({ outcome, reason })))}`,
);
scenarioStatus = "selected-AX-button-action-observed";
const cli = await promisify(execFile)(
process.execPath,
[
fileURLToPath(new URL("./rea.mjs", import.meta.url)),
"capture-native-ui-scenario",
executable,
"--pid",
String(scope.pid),
"--window-id",
String(scope.window_id),
"--restore",
"leave-as-is",
"--steps",
JSON.stringify([
{ kind: "click", path: button.path },
{ kind: "wait", milliseconds: 200 },
]),
"--json",
],
{ maxBuffer: 32 * 1024 * 1024, timeout: 180000 },
);
const evidence = JSON.parse(cli.stdout);
if (
evidence.normalized_result?.steps?.some(
(step) => step.outcome !== "completed",
) ||
evidence.normalized_result?.steps?.length !== 2
)
throw new Error(
`Native UI CLI did not preserve the selected scenario: ${JSON.stringify(evidence.error ?? evidence.normalized_result?.steps?.map(({ outcome, reason }) => ({ outcome, reason })))}`,
);
scenarioStatus = "selected-AX-button-action-and-CLI-parity-observed";
}
const mismatch = await observeNativeUi(
{ ...target.value, sha256: "0".repeat(64) },
"observe_native_ui",
scope,
);
if (mismatch.ok || !mismatch.error.message.includes("target-mismatch"))
throw new Error("Real helper did not reject changed target bytes");
process.stdout.write(
`${JSON.stringify({ ok: true, verification_status: observation.ok ? "passed" : "permission-boundary-only", positive_e2e: observation.ok, observation: observation.ok ? "captured-selected-fixture-window" : "os-permission-denial-verified", scenario: scenarioStatus, permission_failure: observation.ok ? null : observation.error.message, mismatch_rejected: true, target_owned: true })}\n`,
);
} finally {
if (fixturePid !== undefined) {
try {
process.kill(fixturePid, "SIGTERM");
} catch (error) {
if (error.code !== "ESRCH") throw error;
}
}
if (child !== undefined && child.exitCode === null) {
const exited = new Promise((resolve) => child.once("exit", resolve));
child.kill("SIGTERM");
await exited;
}
await rm(root, { recursive: true, force: true });
}
+19
View File
@@ -0,0 +1,19 @@
/** Derive agent and Hopper setup support from the corresponding doctor checks. */
export function packageSetupSupport(
checks:
| readonly { readonly name: string; readonly ok: boolean }[]
| undefined,
platform: NodeJS.Platform,
): {
supportedSetupHost: boolean;
hopperSetupSupported: boolean;
};
/** Validate --help, --llms, and doctor output and determine host setup support. */
export function verifyPackageDiscovery(input: {
readonly cli: string;
readonly environment: Readonly<Record<string, string | undefined>>;
}): Promise<{
supportedSetupHost: boolean;
hopperSetupSupported: boolean;
}>;
+19 -5
View File
@@ -2,7 +2,7 @@ import { json, run, runWithStatus } from "./lib/verify-package-core.mjs";
const REQUIRED_HELP_COMMANDS = [
"setup",
"upgrade",
"update",
"inspect-artifact",
"extract-artifact",
"import-reference-source",
@@ -27,6 +27,17 @@ const REQUIRED_LLM_TOPICS = [
"analyze-javascript-application",
];
/** Derive agent and Hopper setup support from the corresponding doctor checks. */
export function packageSetupSupport(checks, platform) {
const supportedSetupHost =
checks?.find(({ name }) => name === "node")?.ok === true;
const hopperSetupSupported =
supportedSetupHost &&
checks?.find(({ name }) => name === "host")?.ok === true &&
platform !== "win32";
return { supportedSetupHost, hopperSetupSupported };
}
/** Validate --help, --llms, and doctor output and determine host setup support. */
export async function verifyPackageDiscovery({ cli, environment }) {
const help = await run(cli, ["--help"], environment);
@@ -37,8 +48,11 @@ export async function verifyPackageDiscovery({ cli, environment }) {
environment,
);
const doctor = json(doctorExecution.stdout);
const supportedSetupHost =
doctor.checks?.find(({ name }) => name === "host")?.ok === true;
const { supportedSetupHost, hopperSetupSupported } = packageSetupSupport(
doctor.checks,
process.platform,
);
const hopperReady = doctor.checks?.find(({ name }) => name === "hopper")?.ok;
const expectedDoctorHealth = doctor.checks?.every(({ ok }) => ok) === true;
const missingHelp = REQUIRED_HELP_COMMANDS.filter(
(command) => !help.includes(command),
@@ -51,10 +65,10 @@ export async function verifyPackageDiscovery({ cli, environment }) {
missingLlms.length !== 0 ||
doctor.healthy !== expectedDoctorHealth ||
doctorExecution.status !== (expectedDoctorHealth ? 0 : 1) ||
doctor.checks?.find(({ name }) => name === "hopper")?.ok !== true
(hopperSetupSupported && hopperReady !== true)
)
throw new Error(
`packaged CLI discovery or doctor failed: ${JSON.stringify({ helpSetup: help.includes("setup"), llmsDecompile: llms.includes("decompile"), doctor })}`,
);
return { supportedSetupHost };
return { supportedSetupHost, hopperSetupSupported };
}

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